🌲 Timber - Log Better. Solve Problems Faster.
Overview
Timber is a logging platform with one major difference: Instead of parsing, which relies on unreadable, unpredictable, hard to use text logs, Timber integrates directly with your application, augmenting your logs metadata and context you couldn't capture otherwise. This transforms your logs into rich structured events, fundamentally changing the way you use your logs.
- Easy setup -
mix timber.install - Seamlessly integrates with popular libraries and frameworks
- Modern fast console, designed specifically for your application
Installation
Add
timberas a dependency inmix.exs:# Mix.exsdef application do[applications: [:timber]]enddef deps do[{:timber, "~> 2.1"}]endIn your
shell, runmix deps.get.In your
shell, runmix timber.install.
Usage
Basic text logging
No special API, Timber works directly with Logger:
Logger.info("My log message")
# => My log message @metadata {"level": "info", "context": {...}}
Structured logging (metadata)
Simply use Elixir's native Logger metadata:
Logger.info("Payment rejected", meta: %{customer_id: "abcd1234", amount: 100, currency: "USD"})
# => My log message @metadata {"level": "info", "meta": {"customer_id": "abcd1234", "amount": 100}}
- In the Timber console use the queries like
customer_id:abcd1234oramount:>100. - Warning: metadata keys must use consistent types as the values. If
customer_idkey was sent an integer, it would not be indexed because it was first sent a string. See the "Custom events" example below if you'd like to avoid this. See when to use metadata or events. - Note: the
:metakey is necessary until this recent change to the Elixir logger makes it into an official release.
Custom events
Events are just defined structures with a namespace. They are more formal and avoid type collisions.
Custom events, specifically, allow you to extend beyond events already defined in
the Timber.Events namespace.
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": {...}}
- In the Timber console use the queries like
type:payment_rejectedorpayment_rejected.amount:>100. - See when to use metadata or events
Custom contexts
Context is additional data shared across log lines. Think of it like log join data.
It's stored in the local process dictionary and is incldued in every log written
within that process. Custom contexts allow you to extend beyond contexts already
defined in the Timber.Contexts namespace.
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"}}}
- Notice the
:buildroot key. Timber will classify this context as such. - In the Timber console use the query
build.version:1.0.0
Pro-tips 💪
Timings & Metrics
Logging metrics is accomplished by logging custom events. Please see our metrics docs page for a more detailed explanation with examples.
Tracking background jobs and tasks
Note: This tip refers to traditional background jobs backed by a queue. For native Elixir
processes we capture the context.runtime.vm_pid automatically. So calls like spawn/1 and
Task.async/1 will automatially have their pid included in the context.
For traditional background jobs / tasks backed by a queue you'll want to capture relevant
job context. Most importantly, the id:
%Timber.Contexts.JobContext{queue_name: "my_queue", id: "abcd1234", attempt: 1}
|> Timber.add_context()
Logger.info("Task execution started")
# ...
Logger.info("Task execution completed")
# => Task execution started @metadata {"context": {"job": {"queue_name": "my_queue", "id": "abcd1234", "attempt": 1}}}
Adding metadata to errors
By default, Timber will capture and structure all of your errors and exceptions, there
is nothing additional you need to do. You'll get the exception message, name, and backtrace.
But, in many cases you need additional context and data. Timber supports additional fields
in your exceptions, simply add fields as you would any other struct:
defmodule StripeCommunicationError do
defexception [:message, :customer_id, :card_token, :stripe_response]
end
raise(
StripeCommunicationError,
message: "Bad response #{response} from Stripe!",
customer_id: "xiaus1934",
card_token: "mwe42f64",
stripe_response: response_body
)
- These fields will be available in the
event.error.metadata_jsonfield. - Run the query
type:errorto view all errors. - Within the Timber console you can click the log to view all of this data.
Sharing context between processes
The Timber.Context is local to each process, this is by design as it prevents processes from
conflicting with each other as they maintain their contexts. But many times you'll want to share
context between processes because they are related (such as processes created by Task or Flow).
In these instances copying the context is easy:
current_context = Timber.CurrentContext.load()
Task.async fn ->
Timber.CurrentContext.save(current_context)
Logger.info("Logs from a separate process")
end
Searching, graphing, alerting, etc
Checkout the official Timber console docs. It walks you through everything from our search syntax to alerting and graphin.
Configuration
Below are a few popular configuration settings. A comprehensive list can be found in the Timber.Config documentation.
Only log slow Ecto SQL queries
Logging SQL queries can be useful but noisy. To reduce the volume of SQL queries you can limit your logging to queries that surpass an execution time threshold:
config :timber, Timber.Integrations.EctoLogger,
query_time_ms_threshold: 2_000 # 2 seconds
In the above example, only queries that exceed 2 seconds in execution time will be logged.
Integrations
Phoenix
The Phoenix integration
structures your existing Phoenix logs into
controller_call,
template_render,
channel_join, and
channel_receive events.
Pro-tip: this integration captures the parameters sent to your controller, making it easy to debug issues by understanding exactly which data was sent to your controller.
Installation
To install this integration, please run the mix timber.install command as noted in the
installation section.
For manual installation, please see the
Timber.Integrations.PhoenixInstrumenter docs.
Ecto
The Ecto integration
structures your existing Ecto logs into structured
sql_query events.
Pro-tip: this integration captures SQL query times, making it easy to visualize SQL query performance and find slow queries.
Installation
To install this integration, please run the mix timber.install command as noted in the
installation section.
For manual installation, please see the
Timber.Integrations.EctoLogger docs.
Plug
The Plug integration
structures your existing Plug logs into
http_request and
http_response events.
Pro-tip: this integration captures HTTP response codes and times, making it easy to visualize the performance of your application.
Installation
To install this integration, please run the mix timber.install command as noted in the
installation section.
For manual installation, please see the
Timber.Integrations.EventPlug,
Timber.Integrations.HTTPContextPlug,
and Timber.Integrations.SessionContextPlug
docs. We highly recommend using the installer!
ExAws
The ExAws integration
logs and structures outgoing AWS HTTP communication via the
http_request and
http_response events.
This gives you complete insight into how your application is communicating with AWS services,
including timings, errors, etc.
By default this will only log change requests (POST, PUT, DELETE, PATCH). This reduces
noise while still logging requests that are meaningful. Please see the
Timber.Integrations.ExAwsHTTPClient docs
docs for more configuration options.
Installation
config :ex_aws,
http_client: Timber.Integrations.ExAwsHTTPClient
For more details, please see the
Timber.Integrations.ExAwsHTTPClient docs.
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.
When to use metadata or events?
At it's basic level, both metadata and events serve the same purpose: they add structured data to your logs. And anyone that's implemented structured logging know's this can quickly get out of hand. This is why we created events. Here's how we recommend using them:
- Use
eventswhen the log cleanly maps to an event that you'd like to alert on, graph, or use in a meaningful way. Typically something that is core to your business or application. - Use metadata for debugging purposes; when you simply want additional insight without polluting the message.
Example 1: Logging that a payment was rejected
This is clearly an event that is meaningful to your business. You'll probably want to alert and graph this data. So let's log it as an official event:
event_data = %{customer_id: "xiaus1934", amount: 1900, currency: "USD"}
Logger.info("Payment rejected", event: %{payment_rejected: event_data})
Example 2: Logging that an email was changed
This is definitely log worthy, but not something that is core to your business or application. Instead of an event, use metadata:
Logger.info("Email successfully changed", meta: %{old_email: old_email, new_email: new_email})