diff --git a/README.md b/README.md index d5df36d2..2c2ee515 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index e3ad5be2..d8fd8a6d 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -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 diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index cbb6c407..47fd4f03 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -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, []}]}, @@ -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, @@ -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__, []}, @@ -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) @@ -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. @@ -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) diff --git a/lib/sentry/logger_handler.ex b/lib/sentry/logger_handler.ex index d11a517f..96fd11d2 100644 --- a/lib/sentry/logger_handler.ex +++ b/lib/sentry/logger_handler.ex @@ -120,15 +120,11 @@ defmodule Sentry.LoggerHandler do set `:sync_threshold` to `nil`. """ ], - enable_logs: [ - type: {:or, [:boolean, nil]}, - default: nil, - doc: false - ], logs_level: [ type: - {:in, [:emergency, :alert, :critical, :error, :warning, :warn, :notice, :info, :debug]}, - default: :info, + {:in, + [:emergency, :alert, :critical, :error, :warning, :warn, :notice, :info, :debug, nil]}, + default: nil, doc: false ], logs_excluded_domains: [ @@ -155,26 +151,27 @@ defmodule Sentry.LoggerHandler do * **Captured Sentry events** — report crashes and (optionally) `Logger` messages such as `Logger.error("oops")` to Sentry as events, the same way - `Sentry.capture_exception/2` and `Sentry.capture_message/2` do. This is always - active when the handler is attached. + `Sentry.capture_exception/2` and `Sentry.capture_message/2` do. Crashes are always + reported when the handler is attached; standalone messages are reported once + `:capture_log_messages` is `true`. * **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. + UI](https://develop.sentry.dev/sdk/telemetry/logs/) as structured log events. This is + active once a logs level is set — `:level` under the `:logs` option of your Sentry + configuration, which a hand-attached handler can override with `:logs_level`. The two are independent: a single log can become a captured Sentry 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 + > Setting the `:logs` option 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 that same `:logs` option (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. + > ["Configuration"](#module-configuration). > #### When to Use the Handler vs the Backend? {: .info} > @@ -264,13 +261,13 @@ defmodule Sentry.LoggerHandler do ## Sending logs to Sentry - To send structured logs to [Sentry's logs feature](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`: + To send structured logs to [Sentry's logs + feature](https://develop.sentry.dev/sdk/telemetry/logs/), set `:level` under the `:logs` + option in your Sentry configuration. That 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 @@ -281,13 +278,12 @@ defmodule Sentry.LoggerHandler do ### Also capturing `Logger` messages as Sentry events By default the auto-attached handler reports **crashes** as Sentry events but leaves - standalone messages (such as `Logger.error("oops")`) as structured logs only. To also - report those messages as Sentry 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`: + standalone messages (such as `Logger.error("oops")`) alone. To also report those messages + as Sentry 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` to `true` + and tune the other `:capture_*` keys under `:logs`: config :sentry, - enable_logs: true, logs: [ level: :info, # structured logs at :info and above capture_log_messages: true, # also report messages as Sentry events... @@ -371,7 +367,6 @@ defmodule Sentry.LoggerHandler do :rate_limiting, :sync_threshold, :discard_threshold, - :enable_logs, :logs_level, :logs_excluded_domains, :logs_metadata, @@ -387,9 +382,7 @@ defmodule Sentry.LoggerHandler do # The :config key may not be here. sentry_config = Map.get(config, :config, %{}) - handler_config = cast_config(%__MODULE__{}, sentry_config) - - handler_config = put_backends(handler_config) + handler_config = %__MODULE__{} |> cast_config(sentry_config) |> put_backends() config = Map.put(config, :config, handler_config) @@ -432,12 +425,7 @@ defmodule Sentry.LoggerHandler do end updated_config = - old_config - |> update_in([:config], fn config -> - config - |> cast_config(new_sentry_config) - |> put_backends() - end) + update_in(old_config, [:config], &(&1 |> cast_config(new_sentry_config) |> put_backends())) _ignored = cond do @@ -540,12 +528,22 @@ defmodule Sentry.LoggerHandler do |> Keyword.drop([:level, :excluded_domains, :metadata]) end + # Structured logs are enabled by the presence of a logs level, which a handler attached + # by hand inherits from the global config when it doesn't set one of its own. The + # effective level is resolved here, at setup time, so the backend never reads the config + # on the logging hot path. ErrorBackend is always present: it reports crashes, and + # standalone messages once :capture_log_messages is on. defp put_backends(%__MODULE__{} = config) do - backends = [ErrorBackend] ++ if enable_logs?(config), do: [LogsBackend], else: [] - - %{config | backends: backends} + case config.logs_level || global_logs_level() do + nil -> %{config | backends: [ErrorBackend]} + level -> %{config | logs_level: level, backends: [ErrorBackend, LogsBackend]} + end end - defp enable_logs?(%__MODULE__{enable_logs: nil}), do: Config.enable_logs?() - defp enable_logs?(%__MODULE__{enable_logs: enable_logs?}), do: enable_logs? + defp global_logs_level do + case Config.logs() do + nil -> nil + logs -> Keyword.get(logs, :level) + end + end end diff --git a/lib/sentry/logger_handler/logs_backend.ex b/lib/sentry/logger_handler/logs_backend.ex index 84f33dee..b705ac82 100644 --- a/lib/sentry/logger_handler/logs_backend.ex +++ b/lib/sentry/logger_handler/logs_backend.ex @@ -3,8 +3,8 @@ defmodule Sentry.LoggerHandler.LogsBackend do # Backend that sends log events to Sentry's Logs Protocol. # - # This backend is enabled at handler setup time when `enable_logs: true` is set - # in Sentry configuration. Its configuration (level, excluded_domains, metadata) is + # This backend is enabled at handler setup time when a logs level is set. Its + # configuration (level, excluded_domains, metadata) is # frozen into the `%Sentry.LoggerHandler{}` config struct when the handler is set up, # so this backend reads those settings from the config it is given for each event # rather than reading `Sentry.Config` on the logging hot path. diff --git a/test/sentry/application_test.exs b/test/sentry/application_test.exs index 64c787f7..97c1cbae 100644 --- a/test/sentry/application_test.exs +++ b/test/sentry/application_test.exs @@ -5,7 +5,7 @@ defmodule Sentry.ApplicationTest do require Logger - describe "auto logger handler when enable_logs is true" do + describe "auto logger handler" do setup do on_exit(fn -> _ = :logger.remove_handler(:sentry_log_handler) @@ -13,20 +13,16 @@ defmodule Sentry.ApplicationTest do end test "attaches :sentry_log_handler with defaults" do - restart_sentry_with(dsn: "https://public@sentry.example.com/1", enable_logs: true) + restart_sentry_with(dsn: "https://public@sentry.example.com/1", logs: []) assert {:ok, handler} = :logger.get_handler_config(:sentry_log_handler) assert handler.module == Sentry.LoggerHandler - assert Sentry.Config.logs_level() == :info - assert Sentry.Config.logs_excluded_domains() == [] - assert Sentry.Config.logs_metadata() == [] - assert handler.config.capture_log_messages == false assert handler.config.capture_level == :error assert handler.config.capture_metadata == [] assert handler.config.capture_excluded_domains == [:cowboy] - assert handler.config.logs_level == :info + assert handler.config.logs_level == nil assert handler.config.logs_excluded_domains == [] assert handler.config.logs_metadata == [] end @@ -34,7 +30,6 @@ defmodule Sentry.ApplicationTest do 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] ) @@ -46,24 +41,20 @@ defmodule Sentry.ApplicationTest do test "respects logs.level config" do restart_sentry_with( dsn: "https://public@sentry.example.com/1", - enable_logs: true, logs: [level: :warning] ) assert {:ok, handler} = :logger.get_handler_config(:sentry_log_handler) - assert Sentry.Config.logs_level() == :warning assert handler.config.logs_level == :warning end test "respects logs.excluded_domains config" do restart_sentry_with( dsn: "https://public@sentry.example.com/1", - enable_logs: true, logs: [excluded_domains: [:cowboy, :ranch]] ) assert {:ok, handler} = :logger.get_handler_config(:sentry_log_handler) - assert Sentry.Config.logs_excluded_domains() == [:cowboy, :ranch] # :excluded_domains is for the logs feature; captured Sentry event exclusions are # governed by the separate :capture_excluded_domains option. assert handler.config.capture_excluded_domains == [:cowboy] @@ -73,7 +64,6 @@ defmodule Sentry.ApplicationTest do 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]] ) @@ -84,12 +74,10 @@ defmodule Sentry.ApplicationTest do test "respects logs.metadata config" do restart_sentry_with( dsn: "https://public@sentry.example.com/1", - enable_logs: true, logs: [metadata: [:request_id, :user_id]] ) assert {:ok, handler} = :logger.get_handler_config(:sentry_log_handler) - assert Sentry.Config.logs_metadata() == [:request_id, :user_id] # :metadata is for the logs feature; it must not leak into captured event metadata, # which is governed by the separate :capture_metadata option. assert handler.config.capture_metadata == [] @@ -99,7 +87,6 @@ defmodule Sentry.ApplicationTest do 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]] ) @@ -110,7 +97,6 @@ defmodule Sentry.ApplicationTest do 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: [ level: :info, excluded_domains: [:cowboy], @@ -131,7 +117,6 @@ defmodule Sentry.ApplicationTest do # the start path must re-sync the handler's frozen options to the new config. restart_sentry_with( dsn: "https://public@sentry.example.com/1", - enable_logs: true, logs: [ level: :warning, excluded_domains: [:ranch], @@ -149,18 +134,18 @@ defmodule Sentry.ApplicationTest do assert handler.config.capture_excluded_domains == [:ranch] end - test "does not attach handler when enable_logs is false" do - restart_sentry_with(enable_logs: false) + test "does not attach the handler when :logs is not configured" do + restart_sentry_with(dsn: "https://public@sentry.example.com/1") assert {:error, {:not_found, :sentry_log_handler}} = :logger.get_handler_config(:sentry_log_handler) end - test "removes auto-handler when enable_logs becomes false" do - restart_sentry_with(dsn: "https://public@sentry.example.com/1", enable_logs: true) + test "removes the auto-handler when :logs becomes nil" do + restart_sentry_with(dsn: "https://public@sentry.example.com/1", logs: []) assert {:ok, _} = :logger.get_handler_config(:sentry_log_handler) - restart_sentry_with(dsn: "https://public@sentry.example.com/1", enable_logs: false) + restart_sentry_with(dsn: "https://public@sentry.example.com/1", logs: nil) assert {:error, {:not_found, :sentry_log_handler}} = :logger.get_handler_config(:sentry_log_handler) @@ -178,7 +163,7 @@ defmodule Sentry.ApplicationTest do _ = :logger.remove_handler(existing_handler) end) - restart_sentry_with(dsn: "https://public@sentry.example.com/1", enable_logs: true) + restart_sentry_with(dsn: "https://public@sentry.example.com/1", logs: []) assert {:error, {:not_found, :sentry_log_handler}} = :logger.get_handler_config(:sentry_log_handler) @@ -187,7 +172,7 @@ defmodule Sentry.ApplicationTest do end test "removes auto-handler when a user adds their own Sentry.LoggerHandler after startup" do - restart_sentry_with(dsn: "https://public@sentry.example.com/1", enable_logs: true) + restart_sentry_with(dsn: "https://public@sentry.example.com/1", logs: []) assert {:ok, _} = :logger.get_handler_config(:sentry_log_handler) user_handler = :"user_sentry_handler_#{System.unique_integer([:positive])}" @@ -212,7 +197,7 @@ defmodule Sentry.ApplicationTest do end test "keeps auto-handler when a user adds a Sentry.LoggerHandler with invalid config" do - restart_sentry_with(dsn: "https://public@sentry.example.com/1", enable_logs: true) + restart_sentry_with(dsn: "https://public@sentry.example.com/1", logs: []) assert {:ok, _} = :logger.get_handler_config(:sentry_log_handler) user_handler = :"user_sentry_handler_#{System.unique_integer([:positive])}" @@ -227,7 +212,7 @@ defmodule Sentry.ApplicationTest do end test "auto-handler captures logs to the buffer" do - restart_sentry_with(dsn: "https://public@sentry.example.com/1", enable_logs: true) + restart_sentry_with(dsn: "https://public@sentry.example.com/1", logs: [level: :info]) assert {:ok, _} = :logger.get_handler_config(:sentry_log_handler) diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index bc39f46f..4f445c74 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -83,8 +83,18 @@ defmodule Sentry.ConfigTest do end end + test ":logs is nil by default" do + assert Config.validate!([])[:logs] == nil + assert Config.validate!(logs: nil)[:logs] == nil + end + + test ":logs level is nil by default" do + assert Config.validate!(logs: [])[:logs][:level] == nil + assert Config.validate!(logs: [level: :info])[:logs][:level] == :info + end + test ":logs capture options" do - defaults = Config.validate!([])[:logs] + defaults = Config.validate!(logs: [])[:logs] assert defaults[:capture_log_messages] == false assert defaults[:capture_level] == :error assert defaults[:capture_metadata] == [] diff --git a/test/sentry/logger_handler/logs_test.exs b/test/sentry/logger_handler/logs_test.exs index cce28326..35f420da 100644 --- a/test/sentry/logger_handler/logs_test.exs +++ b/test/sentry/logger_handler/logs_test.exs @@ -13,7 +13,7 @@ defmodule Sentry.LoggerHandler.LogsTest do @moduletag :capture_log setup do - SentryTest.setup_sentry(enable_logs: true, logs: [level: :info]) + SentryTest.setup_sentry(logs: [level: :info]) end setup :add_logs_handler @@ -155,135 +155,61 @@ defmodule Sentry.LoggerHandler.LogsTest do assert_sentry_log(:warn, "Warning message should be captured") end - test "does not send logs when enable_logs is false at handler setup time", %{ - handler_name: handler_name - } do - # Remove the main handler first so we can test with enable_logs: false + test "a manually-added handler sends structured logs", %{handler_name: handler_name} do :ok = :logger.remove_handler(handler_name) - disabled_handler_name = - :"sentry_logs_handler_disabled_#{System.unique_integer([:positive])}" - - # Set enable_logs to false BEFORE adding a new handler - put_test_config(enable_logs: false) - - handler_config = %{config: %{}} - - # Add handler with enable_logs: false - LogsBackend should NOT be included - assert :ok = - :logger.add_handler(disabled_handler_name, Sentry.LoggerHandler, handler_config) - - on_exit(fn -> - _ = :logger.remove_handler(disabled_handler_name) - end) - - initial_size = TelemetryProcessor.buffer_size(:log) - - Logger.info("Test message") - - # Give some time for the log to be processed - Process.sleep(100) - - # Buffer should still be at initial size because LogsBackend was not enabled - assert TelemetryProcessor.buffer_size(:log) == initial_size - end - - test "handler-level enable_logs: false overrides global enable_logs: true", %{ - handler_name: handler_name - } do - :ok = :logger.remove_handler(handler_name) - - override_handler_name = - :"sentry_logs_handler_override_#{System.unique_integer([:positive])}" - - # Global config says logs are on; handler-level override forces them off. - put_test_config(enable_logs: true) + manual_handler_name = :"sentry_logs_handler_manual_#{System.unique_integer([:positive])}" assert :ok = - :logger.add_handler(override_handler_name, Sentry.LoggerHandler, %{ - config: %{enable_logs: false} - }) - - on_exit(fn -> _ = :logger.remove_handler(override_handler_name) end) - - initial_size = TelemetryProcessor.buffer_size(:log) + :logger.add_handler(manual_handler_name, Sentry.LoggerHandler, %{config: %{}}) - Logger.info("Test message — should be ignored by overridden handler") + on_exit(fn -> _ = :logger.remove_handler(manual_handler_name) end) - Process.sleep(100) + Logger.info("Manually-added handler message") - assert TelemetryProcessor.buffer_size(:log) == initial_size + assert_sentry_log(:info, "Manually-added handler message") end - test "manually-added handler with no enable_logs inherits global enable_logs: true", %{ + test "does not send structured logs when logs.level is not set", %{ handler_name: handler_name } do :ok = :logger.remove_handler(handler_name) - inherit_handler_name = - :"sentry_logs_handler_inherit_#{System.unique_integer([:positive])}" + put_test_config(logs: [capture_log_messages: true]) - put_test_config(enable_logs: true) + disabled_handler_name = + :"sentry_logs_handler_disabled_#{System.unique_integer([:positive])}" assert :ok = - :logger.add_handler(inherit_handler_name, Sentry.LoggerHandler, %{config: %{}}) + :logger.add_handler(disabled_handler_name, Sentry.LoggerHandler, %{config: %{}}) - on_exit(fn -> _ = :logger.remove_handler(inherit_handler_name) end) - - Logger.info("Inherited enable_logs message") - - assert_sentry_log(:info, "Inherited enable_logs message") - end - - test "runtime handler config update disables structured logs", %{handler_name: handler_name} do - assert :ok = - :logger.update_handler_config(handler_name, :config, %{enable_logs: false}) + on_exit(fn -> _ = :logger.remove_handler(disabled_handler_name) end) initial_size = TelemetryProcessor.buffer_size(:log) - Logger.info("Runtime disabled message") + Logger.info("Message with logs disabled") wait_for_buffer_stable(nil, initial_size) assert TelemetryProcessor.buffer_size(:log) == initial_size end - test "runtime handler config update enables structured logs", %{handler_name: handler_name} do + test "a manually-added handler sets its own logs level", %{handler_name: handler_name} do :ok = :logger.remove_handler(handler_name) - disabled_handler_name = - :"sentry_logs_handler_runtime_enable_#{System.unique_integer([:positive])}" - - put_test_config(enable_logs: false) - - assert :ok = - :logger.add_handler(disabled_handler_name, Sentry.LoggerHandler, %{ - config: %{enable_logs: false} - }) + put_test_config(logs: [capture_log_messages: true]) - on_exit(fn -> _ = :logger.remove_handler(disabled_handler_name) end) + own_level_handler_name = :"sentry_logs_handler_own_#{System.unique_integer([:positive])}" assert :ok = - :logger.update_handler_config(disabled_handler_name, :config, %{ - enable_logs: true + :logger.add_handler(own_level_handler_name, Sentry.LoggerHandler, %{ + config: %{logs_level: :info} }) - Logger.info("Runtime enabled message") + on_exit(fn -> _ = :logger.remove_handler(own_level_handler_name) end) - assert_sentry_log(:info, "Runtime enabled message") - end - - test "rejects non-boolean :enable_logs in handler config", %{handler_name: handler_name} do - :ok = :logger.remove_handler(handler_name) - - invalid_handler_name = - :"sentry_logs_handler_invalid_#{System.unique_integer([:positive])}" - - assert {:error, {:handler_not_added, {:callback_crashed, {:error, error, _stack}}}} = - :logger.add_handler(invalid_handler_name, Sentry.LoggerHandler, %{ - config: %{enable_logs: "true"} - }) + Logger.info("Handler-level logs level message") - assert %NimbleOptions.ValidationError{key: :enable_logs} = error + assert_sentry_log(:info, "Handler-level logs level message") end test "generates trace_id when no trace context is available" do @@ -604,7 +530,7 @@ defmodule Sentry.LoggerHandler.LogsTest do defp reconfigure_logs_handler(handler_name, logs_config) do :ok = :logger.remove_handler(handler_name) - put_test_config(logs: logs_config) + put_test_config(logs: Keyword.put_new(logs_config, :level, :info)) new_handler_name = :"sentry_logs_handler_#{System.unique_integer([:positive])}" @@ -622,7 +548,6 @@ defmodule Sentry.LoggerHandler.LogsTest do logs = Sentry.Config.logs() [ - enable_logs: true, capture_log_messages: Keyword.fetch!(logs, :capture_log_messages), capture_level: Keyword.fetch!(logs, :capture_level), capture_metadata: Keyword.fetch!(logs, :capture_metadata), diff --git a/test/sentry/telemetry_processor_integration_test.exs b/test/sentry/telemetry_processor_integration_test.exs index a4ae3fd9..32ad8ba5 100644 --- a/test/sentry/telemetry_processor_integration_test.exs +++ b/test/sentry/telemetry_processor_integration_test.exs @@ -408,7 +408,7 @@ defmodule Sentry.TelemetryProcessorIntegrationTest do @tag :capture_log test "a log_byte limit stops further logs and reports paired outcomes", ctx do - put_test_config(enable_logs: true, logs: [level: :info]) + put_test_config(logs: [level: :info]) attach_sentry_logs_handler() ref = install_rate_limit_response(ctx.bypass, "log_byte") @@ -628,7 +628,7 @@ defmodule Sentry.TelemetryProcessorIntegrationTest do # emitting process and so reads this test's isolated rate limiter table. @tag :capture_log test "a log_byte rate limit drops logs emitted via Logger with paired outcomes", ctx do - put_test_config(enable_logs: true, logs: [level: :info]) + put_test_config(logs: [level: :info]) attach_sentry_logs_handler() log_buffer = TelemetryProcessor.get_buffer(ctx.processor, :log) @@ -850,7 +850,6 @@ defmodule Sentry.TelemetryProcessorIntegrationTest do logs = Sentry.Config.logs() config = [ - enable_logs: true, capture_log_messages: Keyword.fetch!(logs, :capture_log_messages), capture_level: Keyword.fetch!(logs, :capture_level), capture_metadata: Keyword.fetch!(logs, :capture_metadata), diff --git a/test/sentry/test_auto_processor_test.exs b/test/sentry/test_auto_processor_test.exs index 602bf605..9fb68320 100644 --- a/test/sentry/test_auto_processor_test.exs +++ b/test/sentry/test_auto_processor_test.exs @@ -50,15 +50,11 @@ defmodule Sentry.TestAutoProcessorTest do @describetag :capture_log setup do - ctx = SentryTest.setup_sentry(enable_logs: true, logs: [level: :info]) + ctx = SentryTest.setup_sentry(logs: [level: :info]) handler_name = :"sentry_auto_processor_logs_#{System.unique_integer([:positive])}" - handler_config = %{ - config: %{ - enable_logs: true - } - } + handler_config = %{config: %{}} :ok = :logger.add_handler(handler_name, Sentry.LoggerHandler, handler_config) diff --git a/test/sentry/test_test.exs b/test/sentry/test_test.exs index 79c065b6..feccb51d 100644 --- a/test/sentry/test_test.exs +++ b/test/sentry/test_test.exs @@ -423,15 +423,11 @@ defmodule Sentry.TestTest do @describetag :capture_log setup do - ctx = SentryTest.setup_sentry(enable_logs: true, logs: [level: :info]) + ctx = SentryTest.setup_sentry(logs: [level: :info]) handler_name = :"sentry_logs_test_#{System.unique_integer([:positive])}" - handler_config = %{ - config: %{ - enable_logs: true - } - } + handler_config = %{config: %{}} :ok = :logger.add_handler(handler_name, Sentry.LoggerHandler, handler_config) @@ -491,15 +487,11 @@ defmodule Sentry.TestTest do @describetag :capture_log setup do - ctx = SentryTest.setup_sentry(enable_logs: true, logs: [level: :info]) + ctx = SentryTest.setup_sentry(logs: [level: :info]) handler_name = :"sentry_allow_logs_test_#{System.unique_integer([:positive])}" - handler_config = %{ - config: %{ - enable_logs: true - } - } + handler_config = %{config: %{}} :ok = :logger.add_handler(handler_name, Sentry.LoggerHandler, handler_config) diff --git a/test_integrations/phoenix_app/config/dev.exs b/test_integrations/phoenix_app/config/dev.exs index 4802cdd4..0206b416 100644 --- a/test_integrations/phoenix_app/config/dev.exs +++ b/test_integrations/phoenix_app/config/dev.exs @@ -90,7 +90,6 @@ config :sentry, enable_source_code_context: true, send_result: :sync, traces_sample_rate: 1.0, - enable_logs: true, logs: [ level: :info, metadata: :all diff --git a/test_integrations/phoenix_app/config/test.exs b/test_integrations/phoenix_app/config/test.exs index 7f7fb1bb..ff912508 100644 --- a/test_integrations/phoenix_app/config/test.exs +++ b/test_integrations/phoenix_app/config/test.exs @@ -40,7 +40,6 @@ config :sentry, send_result: :sync, traces_sample_rate: 1.0, enable_metrics: true, - enable_logs: true, logs: [ level: :info, excluded_domains: [:cowboy, :ranch],