Skip to content
Open
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
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,18 @@ things: it reports crashes (and, optionally, `Logger` messages) to Sentry as **e
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`:
Set the `:logs` option in your Sentry config and the SDK **attaches the handler
automatically** — you don't need to touch your `:logger` configuration or your
`application.ex`. Without `:logs`, the handler is not attached at all. The two features
have separate opt-ins: `:level` turns on structured logs, and `:capture_log_messages`
turns on reporting standalone `Logger` messages as error events:

```elixir
# config/prod.exs
config :sentry,
# ...your other Sentry config...
enable_logs: true,
logs: [
# Structured logs sent to Sentry's Logs UI:
# Structured logs sent to Sentry's Logs UI.
level: :info,
metadata: [:request_id],
# Also turn standalone Logger messages into Sentry error events.
Expand All @@ -95,8 +96,8 @@ With the configuration above, `Logger.info/1` and higher are sent to the Logs UI
#### 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`:
`:tags_from_metadata`), you can add the handler yourself. Doing so replaces the
auto-attached one:

```elixir
# config/prod.exs
Expand Down
65 changes: 34 additions & 31 deletions lib/sentry/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -132,37 +132,40 @@ defmodule Sentry.Application do
end

defp maybe_add_logger_handler do
if Config.enable_logs?() do
handler_config = logger_handler_config(Config.logs())

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 handler, 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} ->
LoggerUtils.warning("[Sentry] Failed to add logger handler: #{inspect(reason)}")
end
end
else
_ = :logger.remove_handler(:sentry_log_handler)
case Config.logs() do
nil -> _ = :logger.remove_handler(:sentry_log_handler)
logs -> add_logger_handler(logger_handler_config(logs))
end

:ok
end

defp add_logger_handler(handler_config) do
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 handler, 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} ->
LoggerUtils.warning("[Sentry] Failed to add logger handler: #{inspect(reason)}")
end
end

:ok
Expand Down
242 changes: 111 additions & 131 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,89 @@ defmodule Sentry.Config do
]
]

logs_schema = [
level: [
type:
{:in,
[:emergency, :alert, :critical, :error, :warning, :warn, :notice, :info, :debug, nil]},
default: nil,
type_doc: "`t:Logger.level/0` or `nil`",
doc: """
The minimum Logger level for log events sent to Sentry's Logs Protocol. Setting this
is what **enables structured logs**: when it is `nil` (the default), no logs are sent
to the Logs Protocol, regardless of the other keys here.
"""
],
excluded_domains: [
type: {:list, :atom},
default: [],
type_doc: "list of `t:atom/0`",
doc: """
Domains to exclude from logs sent to Sentry's Logs Protocol. This does not affect
captured Sentry 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 sent to Sentry's Logs
Protocol. If set to `:all`, all metadata will be included. This does not affect
captured Sentry events; use `:capture_metadata` for those.
"""
],
capture_log_messages: [
type: :boolean,
default: false,
doc: """
Setting this to `true` is what **enables reporting standalone log messages** (such as
`Logger.error("oops")`) to Sentry as captured 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 v13.3.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 captured Sentry 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 captured events. This is independent of
`:level`, which controls the level for structured logs sent to Sentry's Logs
Protocol. *Available since v13.3.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 captured Sentry events from 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 v13.3.0*.
"""
],
capture_excluded_domains: [
type: {:list, :atom},
default: [:cowboy],
type_doc: "list of `t:atom/0`",
doc: """
Domains to exclude from **error events** captured by the auto-attached handler.
Defaults to `[:cowboy]` 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 v13.3.0*.
"""
]
]

basic_opts_schema = [
dsn: [
type: {:or, [nil, {:custom, Sentry.DSN, :parse, []}]},
Expand Down Expand Up @@ -421,22 +504,6 @@ defmodule Sentry.Config do
default: [],
keys: integrations_schema
],
enable_logs: [
type: :boolean,
default: false,
doc: """
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 captured events, and
can be configured (via the `:capture_log_messages` and `:capture_level` keys of the
`:logs` option) to report standalone `Logger` messages as captured 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*.
"""
],
enable_metrics: [
type: :boolean,
default: true,
Expand All @@ -449,97 +516,27 @@ defmodule Sentry.Config do
"""
],
logs: [
type: :keyword_list,
default: [],
type: {:or, [{:in, [nil]}, {:keyword_list, logs_schema}]},
type_doc: "`t:keyword/0` or `nil`",
default: nil,
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 the `:capture_*` keys
(`:capture_log_messages`, `:capture_level`, `:capture_metadata`, and
`:capture_excluded_domains`) configure whether (and how) `Logger` messages are also
reported as captured Sentry events. *Available since 12.0.0*.
""",
keys: [
level: [
type:
{:in,
[:emergency, :alert, :critical, :error, :warning, :warn, :notice, :info, :debug]},
default: :info,
type_doc: "`t:Logger.level/0`",
doc: """
The minimum Logger level for log events sent to Sentry's Logs Protocol.
"""
],
excluded_domains: [
type: {:list, :atom},
default: [],
type_doc: "list of `t:atom/0`",
doc: """
Domains to exclude from logs sent to Sentry's Logs Protocol. This does not affect
captured Sentry 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 sent to Sentry's Logs
Protocol. If set to `:all`, all metadata will be included. This does not affect
captured Sentry 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 captured 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 v13.3.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 captured Sentry 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 captured events. This is independent of
`:level`, which controls the level for structured logs sent to Sentry's Logs
Protocol. *Available since v13.3.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 captured Sentry events from 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 v13.3.0*.
"""
],
capture_excluded_domains: [
type: {:list, :atom},
default: [:cowboy],
type_doc: "list of `t:atom/0`",
doc: """
Domains to exclude from **error events** captured by the auto-attached handler.
Defaults to `[:cowboy]` 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 v13.3.0*.
"""
]
]
Configuration for the logger handler that the SDK attaches automatically at startup.
When this is `nil` (the default), the handler is not attached at all. Set it to a
keyword list to attach it.

The keys configure **two independent features**, each with its own opt-in:

* **Structured logs** sent to Sentry's Logs Protocol, enabled by setting `:level`
and further configured by `:excluded_domains` and `:metadata`.

* **`Logger` messages reported as captured Sentry events**, enabled by setting
`:capture_log_messages` to `true` and further configured by `:capture_level`,
`:capture_metadata`, and `:capture_excluded_domains`.

Crashes are reported as captured Sentry events whenever the handler is attached,
independently of both opt-ins. The keys are documented under **Logs Options** below.
*Available since 12.0.0*.
"""
],
org_id: [
type: {:custom, __MODULE__, :__validate_org_id__, []},
Expand Down Expand Up @@ -899,6 +896,7 @@ defmodule Sentry.Config do
]

@basic_opts_schema NimbleOptions.new!(basic_opts_schema)
@logs_opts_schema NimbleOptions.new!(logs_schema)
@transport_opts_schema NimbleOptions.new!(transport_opts_schema)
@source_code_context_opts_schema NimbleOptions.new!(source_code_context_opts_schema)
@hook_opts_schema NimbleOptions.new!(hook_opts_schema)
Expand Down Expand Up @@ -965,6 +963,12 @@ defmodule Sentry.Config do

#{NimbleOptions.docs(@basic_opts_schema)}

#### Logs Options

These are the keys of the `:logs` option above.

#{NimbleOptions.docs(@logs_opts_schema)}

#### Hook Options

These options control hooks that this SDK can call before or after sending events.
Expand Down Expand Up @@ -1103,36 +1107,12 @@ defmodule Sentry.Config do
@spec test_mode?() :: boolean()
def test_mode?, do: fetch!(:test_mode)

@spec enable_logs?() :: boolean()
def enable_logs?, do: fetch!(:enable_logs)

@spec enable_metrics?() :: boolean()
def enable_metrics?, do: fetch!(:enable_metrics)

@spec logs() :: keyword()
@spec logs() :: keyword() | nil
def logs, do: fetch!(:logs)

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

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

@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
Loading
Loading