From c5653901ff4de2782278ec48752b95e8aa939d96 Mon Sep 17 00:00:00 2001 From: Ilia Borovitinov Date: Wed, 25 Dec 2024 19:24:19 +0300 Subject: [PATCH] Add :tags_from_metadata option to Sentry.LoggerHandler (#840) Adds a new option :tags_from_metadata that allows specifying sentry tags based on already-set Logger metadata keys. Co-authored-by: Andrea Leopardi --- lib/sentry/logger_backend.ex | 9 ++++++++- lib/sentry/logger_handler.ex | 12 +++++++++++- lib/sentry/logger_utils.ex | 22 ++++++++++++++++++++-- test/sentry/logger_handler_test.exs | 29 +++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/lib/sentry/logger_backend.ex b/lib/sentry/logger_backend.ex index b7a72b8b..2b3800f2 100644 --- a/lib/sentry/logger_backend.ex +++ b/lib/sentry/logger_backend.ex @@ -150,7 +150,14 @@ defmodule Sentry.LoggerBackend do # The context in the Logger backend process is not the same as the one in the process # that did the logging. This behavior is different than the one in Sentry.LoggerHandler, # since Logger handlers run in the caller process. - opts = LoggerUtils.build_sentry_options(level, sentry_context, Map.new(meta), state.metadata) + opts = + LoggerUtils.build_sentry_options( + level, + sentry_context, + Map.new(meta), + state.metadata, + _tags_from_metadata = [] + ) case meta[:crash_reason] do # If the crash reason is an exception, we want to report the exception itself diff --git a/lib/sentry/logger_handler.ex b/lib/sentry/logger_handler.ex index 21117b80..5e0fe3ee 100644 --- a/lib/sentry/logger_handler.ex +++ b/lib/sentry/logger_handler.ex @@ -44,6 +44,14 @@ defmodule Sentry.LoggerHandler do If set to `:all`, all metadata will be included. """ ], + tags_from_metadata: [ + type: {:list, :atom}, + default: [], + doc: """ + Use this to include logger metadata as tags in reports. Metadata under the specified keys + in those keys will be added as tags to the event. *Available since v10.9.0*. + """ + ], capture_log_messages: [ type: :boolean, default: false, @@ -220,6 +228,7 @@ defmodule Sentry.LoggerHandler do :level, :excluded_domains, :metadata, + :tags_from_metadata, :capture_log_messages, :rate_limiting, :sync_threshold @@ -321,7 +330,8 @@ defmodule Sentry.LoggerHandler do log_level, log_meta[:sentry], log_meta, - config.metadata + config.metadata, + config.tags_from_metadata ) log_unfiltered(log_event, sentry_opts, config) diff --git a/lib/sentry/logger_utils.ex b/lib/sentry/logger_utils.ex index 441482f1..095b6078 100644 --- a/lib/sentry/logger_utils.ex +++ b/lib/sentry/logger_utils.ex @@ -10,18 +10,27 @@ defmodule Sentry.LoggerUtils do require Logger - @spec build_sentry_options(Logger.level(), keyword() | nil, map(), [atom()] | :all) :: + @spec build_sentry_options( + Logger.level(), + keyword() | nil, + map(), + [atom()] | :all, + [atom()] + ) :: keyword() - def build_sentry_options(level, sentry_context, meta, allowed_meta) do + def build_sentry_options(level, sentry_context, meta, allowed_meta, tags_from_metadata) do default_extra = Map.merge( %{logger_metadata: logger_metadata(meta, allowed_meta), logger_level: level}, Map.take(meta, [:domain]) ) + default_tags = logger_tags_from_metadata(meta, tags_from_metadata) + (sentry_context || get_sentry_options_from_callers(meta[:callers]) || %{}) |> Map.new() |> Map.update(:extra, default_extra, &Map.merge(&1, default_extra)) + |> Map.update(:tags, default_tags, &Map.merge(&1, default_tags)) |> Map.merge(%{ event_source: :logger, level: elixir_logger_level_to_sentry_level(level), @@ -79,6 +88,15 @@ defmodule Sentry.LoggerUtils do :maps.map(fn _key, val -> attempt_to_convert_iodata(val) end, meta) end + defp logger_tags_from_metadata(meta, tags_from_metadata) do + for {key, value} <- meta, + key in tags_from_metadata, + is_binary(value), + into: %{} do + {key, value} + end + end + defp attempt_to_convert_iodata(list) when is_list(list) do IO.chardata_to_string(list) rescue diff --git a/test/sentry/logger_handler_test.exs b/test/sentry/logger_handler_test.exs index c665c39a..4246ece2 100644 --- a/test/sentry/logger_handler_test.exs +++ b/test/sentry/logger_handler_test.exs @@ -206,6 +206,35 @@ defmodule Sentry.LoggerHandlerTest do end end + describe "with logger metadata as tags" do + @tag handler_config: %{capture_log_messages: true, tags_from_metadata: [:string, :number]} + test "includes configured Logger metadata as tags, but only if strings", %{sender_ref: ref} do + Logger.metadata(string: "value", number: 42, other: "ignored") + Logger.error("Testing error") + + assert_receive {^ref, event} + assert event.tags == %{string: "value"} + end + + @tag handler_config: %{capture_log_messages: true, tags_from_metadata: []} + test "does not include Logger metadata as tags when disabled", + %{sender_ref: ref} do + Logger.error("Testing error", string: "value", number: 42) + + assert_receive {^ref, event} + assert event.tags == %{} + end + + @tag handler_config: %{capture_log_messages: true, tags_from_metadata: [:string, :number]} + test "merges configured tags with explicitly set tags", %{sender_ref: ref} do + Logger.metadata(string: "value", number: 42) + Logger.error("Testing error", sentry: [tags: %{explicit: "tag", number: 44}]) + + assert_receive {^ref, event} + assert event.tags == %{string: "value", number: 44, explicit: "tag"} + end + end + describe "with a crashing GenServer" do setup do %{test_genserver: start_supervised!(TestGenServer, restart: :temporary)}