🌲 Timber - Master your Elixir apps with structured logging
Timber solves Elixir insight by leveraging what you already have: the Logger. There are
no agents or special APIs; just better logging. It's a complete set of events
so you'll always have the data you need to efficiently resolve issues. Never feel lost again.
Overview
Timber for Elixir pairs with the Timber console to provide a complete structured logging system. Automatically turning your raw text logs into rich structured events. This gives you complete insight into your application, allowing you to query, graph, and alert on log data.
For example, it 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:
context.request_id:abcd1234- View all logs generated for a specific requestcontext.user.id:1- View logs generated by a specific user.type:exception- View all exceptions with the ability to zoom out and view them in context (request, user, etc).http_server_response.time_ms:>=1000- View slow responses with the ability to zoom out and view them in context (request, user, etc).
Installation
Add
timberas a dependency inmix.exs:# Mix.exsdef application do[applications: [:timber]]enddef deps do[{:timber, "~> 1.0"}]endRun
mix deps.getin your shell.Run
mix timber.install your-timber-app-api-key- You can obtain your API key by adding your application within Timber. Each app has it's own unique 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": {...}}
- In the Timber console use the query:
tags:tag.
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": {...}}
- In the Timber console use the query:
tags:my_task time_ms>500
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.
- 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": {...}}
- 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": {...}}
- In the Timber console use the query:
payment_rejected.customer_id:xiaus1934orpayment_rejected.amount>100
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": {...}}
- In the Timber console use the query:
key:value
Custom contexts
Context is additional data shared across log lines. Think of it like log join data.
- 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"}}}
- 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"}}}
- In the Timber console use the query:
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?
- 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.
- 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.
- 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. - Sane prices, long retention. Logging is notoriously expensive with low retention. Timber is affordable and offers 6 months of retention by default.
- 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).