Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,41 @@ config :sentry,

### Usage

This library comes with a [`:logger` handler][logger-handlers] to capture error messages coming from process crashes. To enable this, add [`Sentry.LoggerHandler`](https://hexdocs.pm/sentry/Sentry.LoggerHandler.html) to your production configuration:
This library comes with a [`:logger` handler][logger-handlers],
[`Sentry.LoggerHandler`](https://hexdocs.pm/sentry/Sentry.LoggerHandler.html), that does two
things: it reports crashes (and, optionally, `Logger` messages) to Sentry as **error events**,
and it forwards log entries to [Sentry's Logs UI](https://develop.sentry.dev/sdk/telemetry/logs/)
as **structured logs**.

The recommended way to enable it is to set `enable_logs: true` in your Sentry config. The SDK
then **attaches the handler automatically** — you don't need to touch your `:logger`
configuration or your `application.ex`:

```elixir
# config/prod.exs
config :sentry,
# ...your other Sentry config...
enable_logs: true,
logs: [
# Structured logs sent to Sentry's Logs UI:
level: :info,
metadata: [:request_id],
# Also turn standalone Logger messages into Sentry error events.
# Omit these to only report crashes as error events.
capture_log_messages: true,
capture_level: :error,
capture_metadata: [:request_id]
]
```

With the configuration above, `Logger.info/1` and higher are sent to the Logs UI, while
`Logger.error/1` and higher are *also* captured as error events (crashes are always reported).

#### Advanced: configuring the handler manually

If you want full control over the handler's options (such as `:rate_limiting` or
`:tags_from_metadata`), or you want error reporting *without* structured logs, you can add the
handler yourself instead of using `enable_logs`:

```elixir
# config/prod.exs
Expand All @@ -78,7 +112,6 @@ config :my_app, :logger, [
}
}}
]

```

And then add your logger when your application starts:
Expand Down
16 changes: 15 additions & 1 deletion lib/sentry/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,21 @@ defmodule Sentry.Application do
defp maybe_add_logger_handler do
if Config.enable_logs?() do
unless sentry_logger_handler_registered?() do
case :logger.add_handler(:sentry_log_handler, Sentry.LoggerHandler, %{config: %{}}) do
# The :logs config drives both backends of the auto-attached handler: LogsBackend
# reads its options (level, excluded_domains, metadata) from Config at runtime,
# while the ErrorBackend options are passed here at attach time. The error-event
# options come from the separate :capture_* keys so they stay independent from the
# Logs UI ones (e.g. error-event metadata/excluded_domains are opt-in).
handler_config = %{
level: Config.logs_capture_level(),
capture_log_messages: Config.logs_capture_log_messages?(),
metadata: Config.logs_capture_metadata(),
excluded_domains: Config.logs_capture_excluded_domains()
}

case :logger.add_handler(:sentry_log_handler, Sentry.LoggerHandler, %{
config: handler_config
}) do
:ok ->
:ok

Expand Down
76 changes: 72 additions & 4 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,10 @@ defmodule Sentry.Config do
Whether to enable sending log events to Sentry. When enabled, the SDK will
automatically attach a `Sentry.LoggerHandler` to capture and send structured
log events according to the [Sentry Logs Protocol](https://develop.sentry.dev/sdk/telemetry/logs/).
The auto-attached handler also reports **crashes** to Sentry as error events, and
can be configured (via the `:capture_log_messages` and `:capture_level` keys of the
`:logs` option) to report standalone `Logger` messages as error events too, so you
do not need to add `Sentry.LoggerHandler` manually.
The handler is not added if a `Sentry.LoggerHandler` is already registered.
Use the `:logs` option to configure the auto-attached handler.
*Available since 12.0.0*.
Expand All @@ -422,7 +426,11 @@ defmodule Sentry.Config do
default: [],
doc: """
Configuration for the auto-attached logger handler. Only used when `:enable_logs`
is `true`. *Available since 12.0.0*.
is `true`. The `:level`, `:excluded_domains`, and `:metadata` keys configure the
**structured logs** sent to Sentry's Logs Protocol, while the `:capture_*` keys
(`:capture_log_messages`, `:capture_level`, `:capture_metadata`, and
`:capture_excluded_domains`) configure whether (and how) `Logger` messages are also
reported as **error events**. *Available since 12.0.0*.
""",
keys: [
level: [
Expand All @@ -440,16 +448,64 @@ defmodule Sentry.Config do
default: [],
type_doc: "list of `t:atom/0`",
doc: """
Domains to exclude from logs sent to Sentry's Logs Protocol.
Domains to exclude from logs sent to Sentry's Logs Protocol. This does not affect
error events; use `:capture_excluded_domains` for those.
"""
],
metadata: [
type: {:or, [{:list, :atom}, {:in, [:all]}]},
default: [],
type_doc: "list of `t:atom/0`, or `:all`",
doc: """
Logger metadata keys to include as attributes in log events. If set to `:all`,
all metadata will be included.
Logger metadata keys to include as attributes in log events sent to Sentry's Logs
Protocol. If set to `:all`, all metadata will be included. This does not affect
error events; use `:capture_metadata` for those.
"""
],
capture_log_messages: [
type: :boolean,
default: false,
doc: """
When `true`, the auto-attached handler also reports standalone log messages
(such as `Logger.error("oops")`) to Sentry as **error events**, on top of the
always-on crash reports. Messages are filtered by `:capture_level`. This mirrors
the `:capture_log_messages` option of `Sentry.LoggerHandler`. *Available since
13.2.0*.
"""
],
capture_level: [
type:
{:in,
[:emergency, :alert, :critical, :error, :warning, :warn, :notice, :info, :debug]},
default: :error,
type_doc: "`t:Logger.level/0`",
doc: """
The minimum Logger level for messages captured as **error events** when
`:capture_log_messages` is `true`. This is independent of `:level`, which controls
the level for structured logs sent to Sentry's Logs Protocol. *Available since
13.2.0*.
"""
],
capture_metadata: [
type: {:or, [{:list, :atom}, {:in, [:all]}]},
default: [],
type_doc: "list of `t:atom/0`, or `:all`",
doc: """
Logger metadata keys to include in **error events** captured by the auto-attached
handler, added under `:extra` as `logger_metadata`. If set to `:all`, all metadata
will be included. This is independent of `:metadata`, which controls metadata for
structured logs sent to Sentry's Logs Protocol. *Available since 13.2.0*.
"""
],
capture_excluded_domains: [
type: {:list, :atom},
default: [:cowboy, :bandit],
type_doc: "list of `t:atom/0`",
doc: """
Domains to exclude from **error events** captured by the auto-attached handler.
Defaults to `[:cowboy, :bandit]` to avoid double-reporting events already captured
by `Sentry.PlugCapture`. This is independent of `:excluded_domains`, which controls
structured logs sent to Sentry's Logs Protocol. *Available since 13.2.0*.
"""
]
]
Expand Down Expand Up @@ -1034,6 +1090,18 @@ defmodule Sentry.Config do
@spec logs_metadata() :: [atom()] | :all
def logs_metadata, do: Keyword.fetch!(logs(), :metadata)

@spec logs_capture_log_messages?() :: boolean()
def logs_capture_log_messages?, do: Keyword.fetch!(logs(), :capture_log_messages)

@spec logs_capture_level() :: Logger.level()
def logs_capture_level, do: Keyword.fetch!(logs(), :capture_level)

@spec logs_capture_metadata() :: [atom()] | :all
def logs_capture_metadata, do: Keyword.fetch!(logs(), :capture_metadata)

@spec logs_capture_excluded_domains() :: [atom()]
def logs_capture_excluded_domains, do: Keyword.fetch!(logs(), :capture_excluded_domains)

@spec telemetry_buffer_capacities() :: %{Sentry.Telemetry.Category.t() => pos_integer()}
def telemetry_buffer_capacities, do: fetch!(:telemetry_buffer_capacities)

Expand Down
75 changes: 75 additions & 0 deletions lib/sentry/logger_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,31 @@ defmodule Sentry.LoggerHandler do

*This module is available since v9.0.0 of this library*.

This handler can do **two distinct things** with the messages it receives:

* **Error events** — report crashes and (optionally) `Logger` messages such as
`Logger.error("oops")` to Sentry as **errors/messages**, the same way
`Sentry.capture_exception/2` and `Sentry.capture_message/2` do. This is always
active when the handler is attached.

* **Structured logs** — forward log entries to [Sentry's Logs
UI](https://develop.sentry.dev/sdk/telemetry/logs/) as structured log events. This
is active when `:enable_logs` is `true` in your Sentry configuration.

The two are independent: a single log can become an error event, a structured log, both,
or neither, depending on configuration.

> #### You usually don't add this handler manually {: .tip}
>
> Setting `config :sentry, enable_logs: true` makes the SDK **automatically attach**
> this handler at startup — you do **not** need to call `:logger.add_handler/3` or
> `Logger.add_handlers/1` yourself. Configure it through the `:logs` option of your
> Sentry config (see the [Sentry configuration](Sentry.html#module-configuration) and the
> ["Sending logs to Sentry"](#module-sending-logs-to-sentry) section below). Add the
> handler manually only when you want full control over the options documented under
> ["Configuration"](#module-configuration), or when you want error reporting **without**
> structured logs.

> #### When to Use the Handler vs the Backend? {: .info}
>
> Sentry's Elixir SDK also ships with `Sentry.LoggerBackend`, an Elixir `Logger`
Expand Down Expand Up @@ -205,6 +230,56 @@ defmodule Sentry.LoggerHandler do
# ...
end

## Sending logs to Sentry

To send structured logs to [Sentry's Logs UI](https://develop.sentry.dev/sdk/telemetry/logs/),
enable logs in your Sentry configuration. This auto-attaches the handler — there is
**no need** to configure `:logger` or call `:logger.add_handler/3`:

config :sentry,
# ...
enable_logs: true,
logs: [level: :info, metadata: [:request_id]]

With this configuration, every `Logger` call at `:info` or above becomes a structured log
event in Sentry, and crashes are still reported as **error events** (just like the manual
setup above). The `:logs` options are documented in the
[Sentry configuration](Sentry.html#module-configuration).

### Also capturing `Logger` messages as error events

By default the auto-attached handler reports **crashes** as error events but leaves
standalone messages (such as `Logger.error("oops")`) as structured logs only. To also
report those messages as error events — for example, to turn `Logger.error/1` calls into
Sentry issues while keeping `Logger.info/1` out of your issues stream — use the `:capture_*`
keys under `:logs`:

config :sentry,
enable_logs: true,
logs: [
level: :info, # structured logs at :info and above -> Logs UI
capture_log_messages: true, # also report messages as error events...
capture_level: :error, # ...but only at :error and above
capture_metadata: :all, # include Logger metadata in those error events
capture_excluded_domains: [:cowboy] # domains to exclude from those error events
]

The `:capture_*` keys configure the **error-event** side and are independent from the
matching Logs-UI keys (`:metadata`/`:capture_metadata`,
`:excluded_domains`/`:capture_excluded_domains`).

> #### Including `Logger` metadata {: .info}
>
> For the auto-attached handler, metadata for the two destinations is configured
> separately. The `:metadata` key **under `:logs`** lists the `Logger` metadata attached
> as attributes on **structured logs** (shown in the Logs UI). The `:capture_metadata`
> key lists the metadata attached under `:extra` (as `logger_metadata`) on **error
> events**. Both accept a list of keys or `:all`, and both default to `[]`. So if your
> custom metadata is missing from a captured *error event*, set
> `config :sentry, logs: [capture_metadata: [...]]` (or `:all`). When you add the handler
> manually instead, use this module's own `:metadata`
> [configuration option](#module-configuration).

## Configuration

This handler supports the following configuration options:
Expand Down
49 changes: 47 additions & 2 deletions test/sentry/application_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ defmodule Sentry.ApplicationTest do
assert Sentry.Config.logs_level() == :info
assert Sentry.Config.logs_excluded_domains() == []
assert Sentry.Config.logs_metadata() == []

assert config.config.capture_log_messages == false
assert config.config.level == :error
assert config.config.metadata == []
assert config.config.excluded_domains == [:cowboy, :bandit]
end

test "respects logs.capture_log_messages and logs.capture_level config" do
restart_sentry_with(
dsn: "https://public@sentry.example.com/1",
enable_logs: true,
logs: [capture_log_messages: true, capture_level: :warning]
)

assert {:ok, config} = :logger.get_handler_config(:sentry_log_handler)
assert config.config.capture_log_messages == true
assert config.config.level == :warning
end

test "respects logs.level config" do
Expand All @@ -40,8 +57,22 @@ defmodule Sentry.ApplicationTest do
logs: [excluded_domains: [:cowboy, :ranch]]
)

assert {:ok, _config} = :logger.get_handler_config(:sentry_log_handler)
assert {:ok, config} = :logger.get_handler_config(:sentry_log_handler)
assert Sentry.Config.logs_excluded_domains() == [:cowboy, :ranch]
# :excluded_domains is for the Logs UI only; error-event exclusions are governed by
# the separate :capture_excluded_domains option (defaults to [:cowboy, :bandit]).
assert config.config.excluded_domains == [:cowboy, :bandit]
end

test "respects logs.capture_excluded_domains config" do
restart_sentry_with(
dsn: "https://public@sentry.example.com/1",
enable_logs: true,
logs: [capture_excluded_domains: [:cowboy, :ranch]]
)

assert {:ok, config} = :logger.get_handler_config(:sentry_log_handler)
assert config.config.excluded_domains == [:cowboy, :ranch]
end

test "respects logs.metadata config" do
Expand All @@ -51,8 +82,22 @@ defmodule Sentry.ApplicationTest do
logs: [metadata: [:request_id, :user_id]]
)

assert {:ok, _config} = :logger.get_handler_config(:sentry_log_handler)
assert {:ok, config} = :logger.get_handler_config(:sentry_log_handler)
assert Sentry.Config.logs_metadata() == [:request_id, :user_id]
# :metadata is for the Logs UI only; it must not leak into error-event metadata,
# which is governed by the separate :capture_metadata option.
assert config.config.metadata == []
end

test "respects logs.capture_metadata config" do
restart_sentry_with(
dsn: "https://public@sentry.example.com/1",
enable_logs: true,
logs: [capture_metadata: [:request_id, :user_id]]
)

assert {:ok, config} = :logger.get_handler_config(:sentry_log_handler)
assert config.config.metadata == [:request_id, :user_id]
end

test "does not attach handler when enable_logs is false" do
Expand Down
31 changes: 31 additions & 0 deletions test/sentry/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,37 @@ defmodule Sentry.ConfigTest do
end
end

test ":logs capture options" do
defaults = Config.validate!([])[:logs]
assert defaults[:capture_log_messages] == false
assert defaults[:capture_level] == :error
assert defaults[:capture_metadata] == []
assert defaults[:capture_excluded_domains] == [:cowboy, :bandit]

configured =
Config.validate!(
logs: [
capture_log_messages: true,
capture_level: :warning,
capture_metadata: :all,
capture_excluded_domains: [:cowboy, :ranch]
]
)[:logs]

assert configured[:capture_log_messages] == true
assert configured[:capture_level] == :warning
assert configured[:capture_metadata] == :all
assert configured[:capture_excluded_domains] == [:cowboy, :ranch]

assert_raise ArgumentError, ~r/invalid value for :capture_level option/, fn ->
Config.validate!(logs: [capture_level: :invalid])
end

assert_raise ArgumentError, ~r/invalid value for :capture_metadata option/, fn ->
Config.validate!(logs: [capture_metadata: "all"])
end
end

test ":source_code_path_pattern" do
assert Config.validate!(source_code_path_pattern: "*.ex")[:source_code_path_pattern] ==
"*.ex"
Expand Down
Loading
Loading