🌲 Timber - Master your Elixir apps with structured logging

ISC LicenseHex.pmDocumentationBuild Status

Overview

Timber is structured logging solution, giving you complete insight into your applications without the need for agents, or special APIs. It's just better logging, like it always should have been. 😄

Timber works by automatically by turning your raw text logs into rich structured events. It pairs with the Timber Console to maximize your productivity, allowing you to quickly find what you need so that you get back to focusing on your core competencies. Never feel lost again, always have the data you need.

To provide an example, Timber turns this:

Sent 200 in 45.ms

Into this:

Sent 200 in 45.2ms @metadata {"dt": "2017-02-02T01:33:21.154345Z", "level": "info", "context": {"user": {"id": 1}, "http": {"method": "GET", "host": "timber.io", "path": "/path"}}, "event": {"http_response": {"status": 200, "time_ms": 45.2}}}

Allowing you to run queries like:

  1. context.request_id:abcd1234 - View all logs generated for a specific request.
  2. context.user.id:1 - View logs generated by a specific user.
  3. type:exception - View all exceptions with the ability to zoom out and view them in context (request, user, etc).
  4. http_server_response.time_ms:>=1000 - View slow responses with the ability to zoom out and view them in context (request, user, etc).
  5. level:error - Levels in your logs, imagine that!

Installation

  1. Add timber as a dependency in mix.exs:

    # Mix.exs
    def application do
    [applications: [:timber]]
    end
    def deps do
    [{:timber, "~> 1.0"}]
    end
  2. In your shell, run mix deps.get.

  3. In your sheel, run mix timber.install your-timber-app-api-key.

Usage

Basic logging

No special API, Timber works directly with Logger:

Logger.info("My log message")
# My log message @metadata {"level": "info", "context": {...}}

Tagging logs

Tags provide a quick way to categorize logs and make them easier to search:

Logger.info("My log message", tags: ["tag"])
# My log message @metadata {"level": "info", "tags": ["tag"], "context": {...}}

Timings

Timings allow you to capture code execution time:

timer = Timber.start_timer()
# ... code to time ...
time_ms = Timber.duration_ms(timer)
Logger.info("Task complete", tags: ["my_task"] time_ms: time_ms)
# Task complete @metadata {"level": "info", "tags": ["my_task"], "time_ms": 56.4324, "context": {...}}

Custom events

Custom events allow you to capture events central to your line of business like receiving credit card payments, saving a draft of a post, or changing a user's password.

Note: before logging a custom event, checkout Timber.Events to make sure it doesn't already exist.

  1. Log a map (simplest)
event_data = %{customer_id: "xiaus1934", amount: 1900, currency: "USD"}
Logger.info("Payment rejected", event: %{payment_rejected: event_data})
# Payment rejected @metadata {"level": "warn", "event": {"payment_rejected": {"customer_id": "xiaus1934", "amount": 100, "reason": "Card expired"}}, "context": {...}}
  1. Or, log a struct (recommended)
def PaymentRejectedEvent do
use Timber.Events.CustomEvent, type: :payment_rejected
@enforce_keys [:customer_id, :amount, :currency]
defstruct [:customer_id, :amount, :currency]
def message(%__MODULE__{customer_id: customer_id}) do
"Payment rejected for #{customer_id}"
end
end
event = %PaymentRejectedEvent{customer_id: "xiaus1934", amount: 1900, currency: "USD"}
message = PaymentRejectedEvent.message(event)
Logger.info(message, event: event)
# Payment rejected @metadata {"level": "warn", "event": {"payment_rejected": {"customer_id": "xiaus1934", "amount": 100, "reason": "Card expired"}}, "context": {...}}

What about regular Hashes, JSON, or logfmt?

Go for it! Timber will parse the data server side. If the event is meaningful in any way we highly recommend using custom events (see above).

Logger.info(%{key: "value"})
# {"key": "value"} @metadata {"level": "info", "context": {...}}
Logger.info('{"key": "value"}')
# {"key": "value"} @metadata {"level": "info", "context": {...}}
Logger.info("key=value")
# key=value @metadata {"level": "info", "context": {...}}

Custom contexts

Context is additional data shared across log lines. Think of it like log join data.

  1. Add a map (simplest)
Timber.add_context(%{build: %{version: "1.0.0"}})
Logger.info("My log message")
# My log message @metadata {"level": "info", "context": {"build": {"version": "1.0.0"}}}
  1. Add a struct (recommended)
def BuildContext do
use Timber.Contexts.CustomContext, type: :build
@enforce_keys [:version]
defstruct [:version]
end
Timber.add_context(%BuildContext{version: "1.0.0"})
Loger.info("My log message")
# My log message @metadata {"level": "info", "context": {"build": {"version": "1.0.0"}}}

Jibber-Jabber

Which log events does Timber structure for me?

Out of the box you get everything in the Timber.Events namespace.

We also add context to every log, everything in the Timber.Contexts namespace. Context is structured data representing the current environment when the log line was written. It is included in every log line. Think of it like join data for your logs.


What about my current log statements?

They'll continue to work as expected. Timber adheres strictly to the default Logger interface and will never deviate in any way.

In fact, traditional log statements for non-meaningful events, debug statements, etc, are encouraged. In cases where the data is meaningful, consider logging a custom event.

How is Timber different?

  1. No lock-in. Timber is just better logging. There are no agents or special APIs. This means no risk of vendor lock-in, code debt, or performance issues.
  2. Data quality. Instead of relying on parsing alone, Timber ships libraries that structure and augment your logs from within your application. Improving your log data at the source.
  3. Human readability. Structuring your logs doesn't have to mean losing readability. Instead, Timber augments your logs. For example: log message @metadata {...}. And when you view your logs in the Timber console, you'll see the human friendly messages with the ability to view the associated metadata.
  4. Sane prices, long retention. Logging is notoriously expensive with low retention. Timber is affordable and offers 6 months of retention by default.
  5. Normalized schema. Have multiple apps? All of Timber's libraries adhere to our JSON schema. This means queries, alerts, and graphs for your ruby app can also be applied to your elixir app (for example).