From 0932fac718bf35268b07a68c60619b4ce8e450ae Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 9 Jun 2026 12:43:31 +0000 Subject: [PATCH 1/6] feat(logs): support for base capture_* config --- lib/sentry/application.ex | 9 +++- lib/sentry/config.ex | 39 +++++++++++++- lib/sentry/logger_handler.ex | 66 ++++++++++++++++++++++++ test/sentry/application_test.exs | 15 ++++++ test/sentry/config_test.exs | 16 ++++++ test/sentry/logger_handler/logs_test.exs | 45 ++++++++++++++++ 6 files changed, 188 insertions(+), 2 deletions(-) diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index d5629297..4faab1bc 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -135,7 +135,14 @@ 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 + handler_config = %{ + level: Config.logs_capture_level(), + capture_log_messages: Config.logs_capture_log_messages?() + } + + case :logger.add_handler(:sentry_log_handler, Sentry.LoggerHandler, %{ + config: handler_config + }) do :ok -> :ok diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index fb66eee5..a8d49b36 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -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*. @@ -422,7 +426,10 @@ 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 `:capture_log_messages` + and `:capture_level` configure whether `Logger` messages are also reported as + **error events**. *Available since 12.0.0*. """, keys: [ level: [ @@ -451,6 +458,30 @@ defmodule Sentry.Config do Logger metadata keys to include as attributes in log events. If set to `:all`, all metadata will be included. """ + ], + 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*. + """ ] ] ], @@ -1034,6 +1065,12 @@ 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 telemetry_buffer_capacities() :: %{Sentry.Telemetry.Category.t() => pos_integer()} def telemetry_buffer_capacities, do: fetch!(:telemetry_buffer_capacities) diff --git a/lib/sentry/logger_handler.ex b/lib/sentry/logger_handler.ex index 4d3b7f5f..dbf2e802 100644 --- a/lib/sentry/logger_handler.ex +++ b/lib/sentry/logger_handler.ex @@ -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` @@ -205,6 +230,47 @@ 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 — set + `:capture_log_messages` and `:capture_level` 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 + ] + + > #### `:logs` metadata vs handler metadata {: .info} + > + > The `:metadata` key **under `:logs`** controls which `Logger` metadata is attached to + > **structured log** events (as attributes shown in the Logs UI). The `:metadata` + > [configuration option](#module-configuration) of this handler is different: it controls + > metadata attached to **error events** (under `:extra`). If your custom metadata shows up + > in your server logs but not in Sentry's Logs UI, make sure it is listed under + > `config :sentry, logs: [metadata: [...]]` (or use `:all`). + ## Configuration This handler supports the following configuration options: diff --git a/test/sentry/application_test.exs b/test/sentry/application_test.exs index bc49c3e8..6efa689f 100644 --- a/test/sentry/application_test.exs +++ b/test/sentry/application_test.exs @@ -20,6 +20,21 @@ 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 + 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 diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index b0780868..32321769 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -83,6 +83,22 @@ 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 + + configured = + Config.validate!(logs: [capture_log_messages: true, capture_level: :warning])[:logs] + + assert configured[:capture_log_messages] == true + assert configured[:capture_level] == :warning + + assert_raise ArgumentError, ~r/invalid value for :capture_level option/, fn -> + Config.validate!(logs: [capture_level: :invalid]) + end + end + test ":source_code_path_pattern" do assert Config.validate!(source_code_path_pattern: "*.ex")[:source_code_path_pattern] == "*.ex" diff --git a/test/sentry/logger_handler/logs_test.exs b/test/sentry/logger_handler/logs_test.exs index 0b28173d..9a84e7b9 100644 --- a/test/sentry/logger_handler/logs_test.exs +++ b/test/sentry/logger_handler/logs_test.exs @@ -224,6 +224,51 @@ defmodule Sentry.LoggerHandler.LogsTest do end end + describe "capturing Logger messages as error events (logs.capture_log_messages)" do + setup %{handler_name: handler_name} do + :ok = :logger.remove_handler(handler_name) + + put_test_config(logs: [level: :info, capture_log_messages: true, capture_level: :error]) + + name = :"sentry_capture_handler_#{System.unique_integer([:positive])}" + + handler_config = %{ + level: Sentry.Config.logs_capture_level(), + capture_log_messages: Sentry.Config.logs_capture_log_messages?() + } + + assert :ok = :logger.add_handler(name, Sentry.LoggerHandler, %{config: handler_config}) + + on_exit(fn -> _ = :logger.remove_handler(name) end) + + %{handler_name: name} + end + + test "Logger.error is sent as both an error event and a structured log" do + Logger.error("boom from logger") + + assert_sentry_report(:event, message: %{formatted: "boom from logger"}) + assert_sentry_log(:error, "boom from logger") + end + + test "messages below :capture_level are sent as logs but not as error events" do + Logger.info("just an info line") + Logger.warning("a warning line") + + assert_sentry_log(:info, "just an info line") + assert_sentry_log(:warn, "a warning line") + + assert SentryTest.pop_sentry_reports() == [] + end + + test "structured log keyword data is reported as an error event too" do + Logger.error(some: "structured", value: 42) + + event = assert_sentry_report(:event, []) + assert event.message.formatted =~ "structured" + end + end + describe "OpenTelemetry integration with opentelemetry_logger_metadata" do setup do :ok = OpentelemetryLoggerMetadata.setup() From a883528516e993d49919a7b0c6458dc0521f37e4 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 10 Jun 2026 10:40:30 +0000 Subject: [PATCH 2/6] feat(logs): support for capture_metadata setting --- lib/sentry/application.ex | 8 +++- lib/sentry/config.ex | 25 ++++++++--- lib/sentry/logger_handler.ex | 20 +++++---- test/sentry/application_test.exs | 17 ++++++- test/sentry/config_test.exs | 10 ++++- test/sentry/logger_handler/logs_test.exs | 57 +++++++++++++++++++++++- 6 files changed, 119 insertions(+), 18 deletions(-) diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index 4faab1bc..bb7b50fa 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -135,9 +135,15 @@ defmodule Sentry.Application do defp maybe_add_logger_handler do if Config.enable_logs?() do unless sentry_logger_handler_registered?() 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. ErrorBackend's + # :metadata comes from the separate :capture_metadata option so that error-event + # metadata is opt-in and independent from the Logs UI :metadata. handler_config = %{ level: Config.logs_capture_level(), - capture_log_messages: Config.logs_capture_log_messages?() + capture_log_messages: Config.logs_capture_log_messages?(), + metadata: Config.logs_capture_metadata() } case :logger.add_handler(:sentry_log_handler, Sentry.LoggerHandler, %{ diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index a8d49b36..97c7d203 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -427,9 +427,9 @@ defmodule Sentry.Config do doc: """ Configuration for the auto-attached logger handler. Only used when `:enable_logs` is `true`. The `:level`, `:excluded_domains`, and `:metadata` keys configure the - **structured logs** sent to Sentry's Logs Protocol, while `:capture_log_messages` - and `:capture_level` configure whether `Logger` messages are also reported as - **error events**. *Available since 12.0.0*. + **structured logs** sent to Sentry's Logs Protocol, while `:capture_log_messages`, + `:capture_level`, and `:capture_metadata` configure whether (and how) `Logger` + messages are also reported as **error events**. *Available since 12.0.0*. """, keys: [ level: [ @@ -455,8 +455,9 @@ defmodule Sentry.Config do 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: [ @@ -482,6 +483,17 @@ defmodule Sentry.Config do 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*. + """ ] ] ], @@ -1071,6 +1083,9 @@ defmodule Sentry.Config do @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 telemetry_buffer_capacities() :: %{Sentry.Telemetry.Category.t() => pos_integer()} def telemetry_buffer_capacities, do: fetch!(:telemetry_buffer_capacities) diff --git a/lib/sentry/logger_handler.ex b/lib/sentry/logger_handler.ex index dbf2e802..0afa120f 100644 --- a/lib/sentry/logger_handler.ex +++ b/lib/sentry/logger_handler.ex @@ -259,17 +259,21 @@ defmodule Sentry.LoggerHandler do 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_level: :error, # ...but only at :error and above + capture_metadata: :all # include Logger metadata in those error events ] - > #### `:logs` metadata vs handler metadata {: .info} + > #### Including `Logger` metadata {: .info} > - > The `:metadata` key **under `:logs`** controls which `Logger` metadata is attached to - > **structured log** events (as attributes shown in the Logs UI). The `:metadata` - > [configuration option](#module-configuration) of this handler is different: it controls - > metadata attached to **error events** (under `:extra`). If your custom metadata shows up - > in your server logs but not in Sentry's Logs UI, make sure it is listed under - > `config :sentry, logs: [metadata: [...]]` (or use `:all`). + > 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 diff --git a/test/sentry/application_test.exs b/test/sentry/application_test.exs index 6efa689f..2e916842 100644 --- a/test/sentry/application_test.exs +++ b/test/sentry/application_test.exs @@ -23,6 +23,7 @@ defmodule Sentry.ApplicationTest do assert config.config.capture_log_messages == false assert config.config.level == :error + assert config.config.metadata == [] end test "respects logs.capture_log_messages and logs.capture_level config" do @@ -66,8 +67,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 diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index 32321769..01544a91 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -87,16 +87,24 @@ defmodule Sentry.ConfigTest do defaults = Config.validate!([])[:logs] assert defaults[:capture_log_messages] == false assert defaults[:capture_level] == :error + assert defaults[:capture_metadata] == [] configured = - Config.validate!(logs: [capture_log_messages: true, capture_level: :warning])[:logs] + Config.validate!( + logs: [capture_log_messages: true, capture_level: :warning, capture_metadata: :all] + )[:logs] assert configured[:capture_log_messages] == true assert configured[:capture_level] == :warning + assert configured[:capture_metadata] == :all 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 diff --git a/test/sentry/logger_handler/logs_test.exs b/test/sentry/logger_handler/logs_test.exs index 9a84e7b9..dd8421fd 100644 --- a/test/sentry/logger_handler/logs_test.exs +++ b/test/sentry/logger_handler/logs_test.exs @@ -228,13 +228,22 @@ defmodule Sentry.LoggerHandler.LogsTest do setup %{handler_name: handler_name} do :ok = :logger.remove_handler(handler_name) - put_test_config(logs: [level: :info, capture_log_messages: true, capture_level: :error]) + put_test_config( + logs: [ + level: :info, + metadata: :all, + capture_log_messages: true, + capture_level: :error, + capture_metadata: :all + ] + ) name = :"sentry_capture_handler_#{System.unique_integer([:positive])}" handler_config = %{ level: Sentry.Config.logs_capture_level(), - capture_log_messages: Sentry.Config.logs_capture_log_messages?() + capture_log_messages: Sentry.Config.logs_capture_log_messages?(), + metadata: Sentry.Config.logs_capture_metadata() } assert :ok = :logger.add_handler(name, Sentry.LoggerHandler, %{config: handler_config}) @@ -267,6 +276,50 @@ defmodule Sentry.LoggerHandler.LogsTest do event = assert_sentry_report(:event, []) assert event.message.formatted =~ "structured" end + + test "includes custom Logger metadata in the captured error event" do + Logger.error("Hello Buggy Bug", some_info: "boom!") + + event = assert_sentry_report(:event, message: %{formatted: "Hello Buggy Bug"}) + assert event.extra.logger_metadata.some_info == "boom!" + end + + test "logs.metadata feeds the Logs UI but not error events (capture_metadata governs that)", + %{handler_name: handler_name} do + :ok = :logger.remove_handler(handler_name) + + # Metadata is configured for the Logs UI, but capture_metadata is left at its + # default ([]), so error events must not include the metadata. + put_test_config( + logs: [ + level: :info, + metadata: :all, + capture_log_messages: true, + capture_level: :error, + capture_metadata: [] + ] + ) + + name = :"sentry_no_capture_meta_#{System.unique_integer([:positive])}" + + handler_config = %{ + level: Sentry.Config.logs_capture_level(), + capture_log_messages: Sentry.Config.logs_capture_log_messages?(), + metadata: Sentry.Config.logs_capture_metadata() + } + + assert :ok = :logger.add_handler(name, Sentry.LoggerHandler, %{config: handler_config}) + on_exit(fn -> _ = :logger.remove_handler(name) end) + + Logger.error("no meta in event", secret_info: "hidden") + + event = assert_sentry_report(:event, message: %{formatted: "no meta in event"}) + assert event.extra.logger_metadata == %{} + + # The structured log still carries the metadata, since :metadata is :all. + log = assert_sentry_log(:error, "no meta in event") + assert log.attributes[:secret_info] == "hidden" + end end describe "OpenTelemetry integration with opentelemetry_logger_metadata" do From e1920251c2b0f57f8122064554013153f5dab4cd Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 10 Jun 2026 10:55:13 +0000 Subject: [PATCH 3/6] feat(logs): support for capture_excluded_domains setting --- lib/sentry/application.ex | 9 ++++--- lib/sentry/config.ex | 24 ++++++++++++++--- lib/sentry/logger_handler.ex | 17 +++++++----- test/sentry/application_test.exs | 17 +++++++++++- test/sentry/config_test.exs | 9 ++++++- test/sentry/logger_handler/logs_test.exs | 34 ++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 16 deletions(-) diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index bb7b50fa..23f2f4f9 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -137,13 +137,14 @@ defmodule Sentry.Application do unless sentry_logger_handler_registered?() 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. ErrorBackend's - # :metadata comes from the separate :capture_metadata option so that error-event - # metadata is opt-in and independent from the Logs UI :metadata. + # 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() + metadata: Config.logs_capture_metadata(), + excluded_domains: Config.logs_capture_excluded_domains() } case :logger.add_handler(:sentry_log_handler, Sentry.LoggerHandler, %{ diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index 97c7d203..7aade0dc 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -427,9 +427,10 @@ defmodule Sentry.Config do doc: """ Configuration for the auto-attached logger handler. Only used when `:enable_logs` is `true`. The `:level`, `:excluded_domains`, and `:metadata` keys configure the - **structured logs** sent to Sentry's Logs Protocol, while `:capture_log_messages`, - `:capture_level`, and `:capture_metadata` configure whether (and how) `Logger` - messages are also reported as **error events**. *Available since 12.0.0*. + **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: [ @@ -447,7 +448,8 @@ 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: [ @@ -494,6 +496,17 @@ defmodule Sentry.Config do 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*. + """ ] ] ], @@ -1086,6 +1099,9 @@ defmodule Sentry.Config do @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) diff --git a/lib/sentry/logger_handler.ex b/lib/sentry/logger_handler.ex index 0afa120f..36445381 100644 --- a/lib/sentry/logger_handler.ex +++ b/lib/sentry/logger_handler.ex @@ -251,18 +251,23 @@ defmodule Sentry.LoggerHandler do 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 — set - `:capture_log_messages` and `:capture_level` under `:logs`: + 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 + 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 diff --git a/test/sentry/application_test.exs b/test/sentry/application_test.exs index 2e916842..68f40be8 100644 --- a/test/sentry/application_test.exs +++ b/test/sentry/application_test.exs @@ -24,6 +24,7 @@ defmodule Sentry.ApplicationTest do 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 @@ -56,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 diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index 01544a91..fe3baada 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -88,15 +88,22 @@ defmodule Sentry.ConfigTest do 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] + 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]) diff --git a/test/sentry/logger_handler/logs_test.exs b/test/sentry/logger_handler/logs_test.exs index dd8421fd..a990020a 100644 --- a/test/sentry/logger_handler/logs_test.exs +++ b/test/sentry/logger_handler/logs_test.exs @@ -320,6 +320,40 @@ defmodule Sentry.LoggerHandler.LogsTest do log = assert_sentry_log(:error, "no meta in event") assert log.attributes[:secret_info] == "hidden" end + + test "capture_excluded_domains drops error events but keeps the structured log", + %{handler_name: handler_name} do + :ok = :logger.remove_handler(handler_name) + + # The domain is excluded from error events but not from the Logs UI. + put_test_config( + logs: [ + level: :info, + excluded_domains: [], + capture_log_messages: true, + capture_level: :error, + capture_excluded_domains: [:myapp] + ] + ) + + name = :"sentry_excluded_domain_#{System.unique_integer([:positive])}" + + handler_config = %{ + level: Sentry.Config.logs_capture_level(), + capture_log_messages: Sentry.Config.logs_capture_log_messages?(), + excluded_domains: Sentry.Config.logs_capture_excluded_domains() + } + + assert :ok = :logger.add_handler(name, Sentry.LoggerHandler, %{config: handler_config}) + on_exit(fn -> _ = :logger.remove_handler(name) end) + + Logger.error("error from excluded domain", domain: [:myapp]) + + # The structured log is still captured (Logs UI :excluded_domains is []). + assert_sentry_log(:error, "error from excluded domain") + # But no error event, because the domain is in :capture_excluded_domains. + assert SentryTest.pop_sentry_reports() == [] + end end describe "OpenTelemetry integration with opentelemetry_logger_metadata" do From 59d93ecfa8734cd73b83147beb69bc5254d91493 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 9 Jun 2026 12:44:04 +0000 Subject: [PATCH 4/6] chore(docs): update log handler section --- README.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 49b5bf70..63e70dd7 100644 --- a/README.md +++ b/README.md @@ -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 @@ -78,7 +112,6 @@ config :my_app, :logger, [ } }} ] - ``` And then add your logger when your application starts: From eabaea98645e7ef86073c7502e316dff82eb3a97 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 26 Jun 2026 09:42:16 +0000 Subject: [PATCH 5/6] fix(logs): resync config upon app restart --- lib/sentry/application.ex | 63 +++++++++++++++++++++----------- test/sentry/application_test.exs | 24 ++++++++++++ 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index 23f2f4f9..59df5a8f 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -134,28 +134,43 @@ defmodule Sentry.Application do defp maybe_add_logger_handler do if Config.enable_logs?() do - unless sentry_logger_handler_registered?() 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 - - {:error, reason} -> - Logger.warning("[Sentry] Failed to add logger handler: #{inspect(reason)}") - end + # 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() + } + + cond do + # The auto handler is still registered, which happens when the :sentry application + # is stopped and restarted within the same VM: the handler lives in :logger, not in + # our supervision tree, so it survives the stop. Re-sync its config so updated :logs + # settings reach the ErrorBackend, whose options are frozen at attach time and would + # otherwise stay stale across the restart. + auto_logger_handler_registered?() -> + _ = :logger.update_handler_config(:sentry_log_handler, :config, handler_config) + :ok + + # A user registered their own Sentry.LoggerHandler; don't attach the auto one to + # avoid duplicate capture. + sentry_logger_handler_registered?() -> + :ok + + true -> + case :logger.add_handler(:sentry_log_handler, Sentry.LoggerHandler, %{ + config: handler_config + }) do + :ok -> + :ok + + {:error, reason} -> + Logger.warning("[Sentry] Failed to add logger handler: #{inspect(reason)}") + end end else _ = :logger.remove_handler(:sentry_log_handler) @@ -164,6 +179,10 @@ defmodule Sentry.Application do :ok end + defp auto_logger_handler_registered? do + match?({:ok, _config}, :logger.get_handler_config(:sentry_log_handler)) + end + defp sentry_logger_handler_registered? do :logger.get_handler_config() |> Enum.any?(fn %{module: module} -> module == Sentry.LoggerHandler end) diff --git a/test/sentry/application_test.exs b/test/sentry/application_test.exs index 68f40be8..be270c0b 100644 --- a/test/sentry/application_test.exs +++ b/test/sentry/application_test.exs @@ -100,6 +100,30 @@ defmodule Sentry.ApplicationTest do assert config.config.metadata == [:request_id, :user_id] end + test "re-syncs the handler's capture config when restarted while already registered" do + restart_sentry_with( + dsn: "https://public@sentry.example.com/1", + enable_logs: true, + logs: [capture_metadata: [:request_id], capture_excluded_domains: [:cowboy]] + ) + + assert {:ok, config} = :logger.get_handler_config(:sentry_log_handler) + assert config.config.metadata == [:request_id] + assert config.config.excluded_domains == [:cowboy] + + # Restart again WITHOUT removing the handler first. The handler survives the stop, so + # the start path must re-sync the ErrorBackend's frozen options to the new config. + restart_sentry_with( + dsn: "https://public@sentry.example.com/1", + enable_logs: true, + logs: [capture_metadata: [:request_id, :user_id], capture_excluded_domains: [:ranch]] + ) + + assert {:ok, config} = :logger.get_handler_config(:sentry_log_handler) + assert config.config.metadata == [:request_id, :user_id] + assert config.config.excluded_domains == [:ranch] + end + test "does not attach handler when enable_logs is false" do restart_sentry_with(enable_logs: false) From 39f2b8f83361cf63e49516b8a134584fd45f8637 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 29 Jun 2026 12:46:55 +0000 Subject: [PATCH 6/6] fix(docs): clarify capture_level handling --- lib/sentry/config.ex | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index 7aade0dc..a96eb375 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -467,10 +467,11 @@ defmodule Sentry.Config do 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*. + (such as `Logger.error("oops")`) to Sentry as **error events**, in addition to + crash reports. Crash reports are sent whether or not this option is enabled, so + you do not need to turn it on to capture crashes. Both crashes and messages are + gated by `:capture_level`. This mirrors the `:capture_log_messages` option of + `Sentry.LoggerHandler`. *Available since 13.2.0*. """ ], capture_level: [ @@ -480,10 +481,13 @@ defmodule Sentry.Config do 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*. + The minimum Logger level for **error events**, including crashes. At the default + `:error`, crashes (which are logged at `:error`) are reported; raising this above + `:error` suppresses crashes too, mirroring the `:level` option of + `Sentry.LoggerHandler`. When `:capture_log_messages` is `true`, this also gates + which standalone `Logger` messages become error events. 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: [