๐ฒ Timber - Master your Elixir apps with structured logging
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:
context.request_id:abcd1234
- View all logs generated for a specific request.context.user.id:1
- View logs generated by a specific user.type:exception
- View all exceptions with the ability to zoom out and view them in context (request, user, etc).http_server_response.time_ms:>=1000
- View slow responses with the ability to zoom out and view them in context (request, user, etc).level:error
- Levels in your logs, imagine that!
Installation
Add
timber
as a dependency inmix.exs
:# Mix.exs def application do [applications: [:timber]] end def deps do [{:timber, "~> 1.0"}] end
In your shell, run
mix deps.get
.In your sheel, run
mix timber.install your-timber-app-api-key
.- You can obtain your API key by adding your application within Timber. Each app has itโs own unique 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`
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). ---