🌲 Timber - Great Elixir Logging Made Easy
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.
- Easy setup -
mix timber.install - Seamlessly integrates with popular libraries and frameworks
- Do amazing things with your Elixir logs
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
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})
- Search it with queries like:
type:payment_rejectedorpayment_rejected.amount:>100 - Alert on it with threshold based alerts.
- Graph & visualize it
- View this event's data and context
- Facet on this event type
- ...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")
- Search it with queries like:
build.version:1.0.0 - View this context when viewing a log's metadata
- ...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}})
- Search it with queries like:
background_job.time_ms:>500 - Alert on it with threshold based alerts
- View this log's metadata in the console
- ...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")
- Search it with queries like:
background_job.time_ms:>500 - Alert on it with threshold based alerts
- View this log's metadata in the console
- ...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
)
- Search it with queries like:
background_job.time_ms:>500 - Alert on it with threshold based alerts
- View this log's metadata in the console
- ...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.
- Phoenix
- Ecto
- Plug
- ...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:
- Live tail users on your app
- Trace HTTP requests
- Inspect HTTP request parameters
- Powerful searching
- Threshold based alerting
- ...and more! Checkout our the Timber application docs