🌲 Timber - Great Elixir Logging Made Easy

ISC LicenseHex.pmDocumentationBuild Status

Overview

Timber for Elixir is a drop-in upgrade for your Elixir logs that unobtrusively structures your logs through augmentation. It's clean structured logging without the effort. When paired with the Timber console, Timber will fundamentally change the way you use your logs.

  1. Easy setup - mix timber.install
  2. Seamlessly integrates with popular libraries and frameworks
  3. Do amazing things with your Elixir logs

Installation

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

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

  3. In your shell, run mix timber.install.

Usage

Basic text logging

The Timber library works directly with the standard Elixir Logger and installs itself as a backend during the setup process. In this way, basic logging is no different than logging without Timber.

In fact, standard logging messages are encouraged for debug statements and non-meaningful events. Timber does not require you to structure every log!

Logger.debug("My log statement")
Logger.info("My log statement")
Logger.warn("My log statement")
Logger.error("My log statement")

Custom events

Custom events allow you to extend beyond events already defined in the Timber.Events namespace. If you aren't sure what an event is, please read the "Metdata, Context, and Events" doc.

How to use it

event_data = %{customer_id: "xiaus1934", amount: 1900, currency: "USD"}
Logger.info("Payment rejected", event: %{payment_rejected: event_data})
  1. Search it with queries like: type:payment_rejected or payment_rejected.amount:>100
  2. Alert on it with threshold based alerts.
  3. Graph & visualize it
  4. View this event's data and context
  5. Facet on this event type
  6. ...read more in our docs

Custom contexts

Custom contexts allow you to extend beyond contexts already defined in the Timber.Contexts namespace. If you aren't sure what context is, please read the "Metdata, Context, and Events" doc.

How to use it

Timber.add_context(build: %{version: "1.0.0"})
Logger.info("My log message")
  1. Search it with queries like: build.version:1.0.0
  2. View this context when viewing a log's metadata
  3. ...read more in our docs

Pro-tips 💪

Timings & Metrics

Aggregates destroy details, events tell stories. With Timber, logging metrics and timings is simply logging an event. Timber is based on modern big-data principles and can aggregate inordinately large data sets in seconds. Logging events (raw data as it exists), gives you the flexibility in the future to segment and aggregate your data any way you see fit. This is superior to choosing specific paradigms before hand, when you are unsure how you'll need to use your data in the future.

How to use it

Below is a contrived example of timing a background job:

timer = Timber.start_timer()
# ... code to time ...
Logger.info("Processed background job", event: %{background_job: %{time_ms: timer}})

And of course, time_ms can also take a Float or Fixnum:

Logger.info("Processed background job", event: %{background_job: %{time_ms: 45.6}})

Lastly, metrics aren't limited to timings. You can capture any metric you want:

:ogger.info("Credit card charged", event: %{credit_card_charge: %{amount: 123.23}})
  1. Search it with queries like: background_job.time_ms:>500
  2. Alert on it with threshold based alerts
  3. View this log's metadata in the console
  4. ...read more in our docs

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. Calls like spawn/1 and Task.async/1 will automatially have their pid included in the context.

For traditional background jobs backed by a queue you'll want to capture relevant job context. This allows you to segement logs by specific jobs, making it easy to debug and monitor your job executions. The most important attribute to capture is the id:

How to use it

%Timber.Contexts.JobContext{queue_name: "my_queue", id: "abcd1234", attempt: 1}
|> Timber.add_context()
Logger.info("Task execution started")
# ...
Logger.info("Task execution completed")
  1. Search it with queries like: background_job.time_ms:>500
  2. Alert on it with threshold based alerts
  3. View this log's metadata in the console
  4. ...read more in our docs

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.

How to use it

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
)
  1. Search it with queries like: background_job.time_ms:>500
  2. Alert on it with threshold based alerts
  3. View this log's metadata in the console
  4. ...read more in our docs

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.

How to use it

current_context = Timber.CurrentContext.load()
Task.async fn ->
Timber.CurrentContext.save(current_context)
Logger.info("Logs from a separate process")
end

current_context in the above example is captured in the parent process, and because Elixir's variable scope is lexical, you can pass the referenced context into the newly created process. Timber.CurrentContext.save/1 copies that context into the new process dictionary.


Configuration

Below are a few popular configuration options, for a comprehensive list see Timber.Config.

Capture user context

Capturing user context is a powerful feature that allows you to associate logs with users in your application. This is great for support as you can quickly narrow logs to a specific user, making it easy to identify user reported issues.

How to use it

Simply add the UserContext immediately after you authenticate the user:

%Timber.Contexts.UserContext{id: "my_user_id", name: "John Doe", email: "john@doe.com"}
|> Timber.add_context()

All of the UserContext attributes are optional, but at least one much be supplied.

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:

How to use it

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

Timber for Elixir extends beyond your basic logging functionality and integrates with popular libraries and frameworks. This makes structured quality logging effortless. Below is a list of integrations we offer and the various events and contexts they create.

  1. Phoenix
  2. Ecto
  3. Plug
  4. ...more coming soon! Make a request by opening an issue

Do amazing things with your logs

What does all of this mean? Doing amazing things with your logs! Being more productive, solving problems faster, and actually enjoying using your logs for application insight:

  1. Live tail users on your app
  2. Trace HTTP requests
  3. Inspect HTTP request parameters
  4. Powerful searching
  5. Threshold based alerting
  6. ...and more! Checkout our the Timber application docs

The Timber Console

Timber Console

Learn more about our app.

Your Moment of Zen