๐ŸŒฒ Timber - Master your Elixir apps with structured logging

ISC License Hex.pm Documentation Build Status

Overview

Timber is structured logging solution, giving you complete insight into your applications without the need for agents, or special APIs. Itโ€™s just better logging, like it always should have been. ๐Ÿ˜„

Timber works by automatically by turning your raw text logs into rich structured events. It pairs with the Timber Console to maximize your productivity, allowing you to quickly find what you need so that you get back to focusing on your core competencies. Never feel lost again, always have the data you need.

To provide an example, Timber 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:

  1. context.request_id:abcd1234 - View all logs generated for a specific request.
  2. context.user.id:1 - View logs generated by a specific user.
  3. type:exception - View all exceptions with the ability to zoom out and view them in context (request, user, etc).
  4. http_server_response.time_ms:>=1000 - View slow responses with the ability to zoom out and view them in context (request, user, etc).
  5. level:error - Levels in your logs, imagine that!

Installation

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

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

  3. In your sheel, run mix timber.install your-timber-app-api-key.

Usage

Basic logging

No special API, Timber works directly with [`Logger`](https://hexdocs.pm/logger/Logger.html): ```elixir 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: ```elixir Logger.info("My log message", tags: ["tag"]) # My log message @metadata {"level": "info", "tags": ["tag"], "context": {...}} ``` * In the [Timber console](https://app.timber.io) use the query: `tags:tag`. ---

Timings

Timings allow you to capture code execution time: ```elixir 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](https://app.timber.io) 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`](lib/timber/events) to make sure it doesn't already exist. 1. Log a map (simplest) ```elixir 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": {...}} ``` 2. Or, log a struct (recommended) ```elixir 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](https://app.timber.io) use the query: `payment_rejected.customer_id:xiaus1934` or `payment_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). ```ruby 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](https://app.timber.io) use the query: `key:value` ---

Custom contexts

Context is additional data shared across log lines. Think of it like log join data. 1. Add a map (simplest) ```elixir 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"}}} ``` 2. Add a struct (recommended) ```elixir 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](https://app.timber.io) 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`](lib/timber/events) namespace. We also add context to every log, everything in the [`Timber.Contexts`](lib/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`](https://hexdocs.pm/logger/Logger.html) 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](#usage).

How is Timber different?

1. **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. 2. **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. 3. **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](https://app.timber.io), you'll see the human friendly messages with the ability to view the associated metadata. 4. **Sane prices, long retention**. Logging is notoriously expensive with low retention. Timber is affordable and offers _6 months_ of retention by default. 5. **Normalized schema.** Have multiple apps? All of Timber's libraries adhere to our [JSON schema](https://github.com/timberio/log-event-json-schema). This means queries, alerts, and graphs for your ruby app can also be applied to your elixir app (for example). ---

---