🌲 Timber - Great Elixir Logging Made Easy
Timber for Elixir is a drop in backend for the Elixir Logger
that
unobtrusively augments your
logs with rich metadata and context
making them easier to search, use, and read. It pairs with the
Timber console to deliver a tailored Elixir logging experience designed to make
you more productive.
- Installation
- Usage - Simple yet powerful API
- Integrations - Automatic context and metadata for your existing logs
- The Timber Console - Designed for Elixir developers
- Get things done with your logs
Installation
Add
timber
as a dependency inmix.exs
:def deps do [ {:timber, "~> 3.0.0-alpha.1"}, # ... ] end
In your shell, run
mix deps.get
.Now, add the appropriate Mix configuration.
- To send your logs over HTTP (recommended and simplest), follow this example:
config :logger, backends: [Timber.LoggerBackends.HTTP], utc_log: true config :timber, api_key: "TimberAPIKey"
- Alternatively, if you’d like logs to be printed out to
STDOUT
, follow this example:
config :logger, backends: [:console], utc_log: true config :logger, :console, format: {Timber.Formatter, :format}, metadata: [:timber_context, :event, :application, :file, :function, :line, :module, :meta]
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:
Logger.debug("My log statement")
Logger.info("My log statement")
Logger.warn("My log statement")
Logger.error("My log statement")
- Search it with queries like:
error message
- Alert on it with threshold based alerts
- View this event’s metadata and context
Logging events (structured data)
Log structured data without sacrificing readability:
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_rejected
orpayment_rejected.amount:>100
- Alert on it with threshold based alerts
- View this event’s data and context
Setting context
Add shared structured data across your logs:
Timber.add_context(build: %{version: "1.0.0"})
Logger.info("My log message")
- Search it with queries like:
job.id:123
- View this context when viewing a log’s metadata
Pro-tips đź’Ş
Timings & Metrics
Time code blocks:
timer = Timber.start_timer()
# ... code to time ...
Logger.info("Processed background job", event: %{background_job: %{time_ms: timer}})
Log generic metrics:
Logger.info("Processed background job", event: %{background_job: %{time_ms: 45.6}})
- 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
Tracking background jobs
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
:
%Timber.Contexts.JobContext{queue_name: "my_queue", id: "abcd1234", attempt: 1}
|> Timber.add_context()
Logger.info("Background job execution started")
# ...
Logger.info("Background job 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
Track communication with external services
We use this trick internally at Timber to track communication with external services. It logs requests and responses to external services, giving us insight into response times and failed requests.
Below is a contrived example of submitting an invoice to Stripe.
alias Timber.Events.HTTPRequestEvent
alias Timber.Events.HTTPResponseEvent
method = :get
url = "https://api.stripe.com/v1/invoices"
body = "{\"customer\": \"cus_BHhZyYRirFrPkz\"}"
headers = %{}
Logger.info fn ->
event = HTTPRequestEvent.new(direction: "outgoing", service_name: "stripe", method: method, url: url, headers: headers, body: body)
message = HTTPRequestEvent.message(event)
{message, [event: event]}
end
timer = Timber.start_timer()
case :hackney.request(method, url, headers, body, with_body: true) do
{:ok, status, resp_headers, resp_body} ->
Logger.info fn ->
event = HTTPResponseEvent.new(direction: "incoming", service_name: "stripe", status: status, headers: resp_headers, body: resp_body, time_ms: Timber.duration_ms(timer))
message = HTTPResponseEvent.message(event)
{message, [event: event]}
end
{:error, error} ->
message = Exception.message(error)
Logger.error(message, event: error)
{:error, error}
end
Note: Only method
is required for HTTPRequestEvent
, and status
for HTTPResponseEvent
.
body
, if logged, will be truncated to 2048
bytes for efficiency reasons. This can be adjusted
with Timber.Config.http_body_size_limit/0
.
- 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
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
)
- 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
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.LocalContext.get()
Task.async fn ->
Timber.LocalContext.put(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.LocalContext.put/1
copies that context into the new process dictionary.
- 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
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.
Integrations
Timber provides integrations with popular libraries to easily capture context and metadata. This automatically upgrades logs produced by these libraries, making them easier to search and use.
Frameworks & Libraries
- Phoenix via Timber Phoenix
- Ecto via Timber Ecto
- Plug via Timber Plug
Platforms
- Exceptions via Timber Exceptions
- System / Server
…more coming soon! Make a request by opening an issue
Get things done with your logs
Logging features every developer needs:
- Powerful searching. - Find what you need faster.
- Live tail users. - Easily solve customer issues.
- View logs per HTTP request. - See the full story without the noise.
- Inspect HTTP request parameters. - Quickly reproduce issues.
- Threshold based alerting. - Know when things break.
…and more! Checkout our the Timber application docs