Skip to content

Commit 80e32f0

Browse files
authored
Use nimble_options in Sentry.LoggerHandler (#719)
1 parent 581410b commit 80e32f0

File tree

1 file changed

+104
-59
lines changed

1 file changed

+104
-59
lines changed

lib/sentry/logger_handler.ex

Lines changed: 104 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,76 @@
11
defmodule Sentry.LoggerHandler do
2+
rate_limiting_options_schema = [
3+
max_events: [
4+
type: :non_neg_integer,
5+
required: true,
6+
doc: "The maximum number of events to send to Sentry in the `:interval` period."
7+
],
8+
interval: [
9+
type: :non_neg_integer,
10+
required: true,
11+
doc: "The interval (in *milliseconds*) to send `:max_events` events."
12+
]
13+
]
14+
15+
options_schema = [
16+
level: [
17+
type:
18+
{:in,
19+
[:emergency, :alert, :critical, :error, :warning, :warn, :notice, :info, :debug, nil]},
20+
default: :error,
21+
type_doc: "`t:Logger.level/0`",
22+
doc: """
23+
The minimum [`Logger`
24+
level](https://hexdocs.pm/logger/Logger.html#module-levels) to send events for.
25+
"""
26+
],
27+
excluded_domains: [
28+
type: {:list, :atom},
29+
default: [:cowboy],
30+
type_doc: "list of `t:atom/0`",
31+
doc: """
32+
Any messages with a domain in the configured list will not be sent. The default is so as
33+
to avoid double-reporting events from `Sentry.PlugCapture`.
34+
"""
35+
],
36+
metadata: [
37+
type: {:or, [{:list, :atom}, {:in, [:all]}]},
38+
default: [],
39+
type_doc: "list of `t:atom/0`, or `:all`",
40+
doc: """
41+
Use this to include non-Sentry logger metadata in reports. If it's a list of keys, metadata
42+
in those keys will be added in the `:extra` context (see
43+
`Sentry.Context.set_extra_context/1`) under the `:logger_metadata` key.
44+
If set to `:all`, all metadata will be included.
45+
"""
46+
],
47+
capture_log_messages: [
48+
type: :boolean,
49+
default: false,
50+
doc: """
51+
When `true`, this module will report all logged messages to Sentry (provided they're not
52+
filtered by `:excluded_domains` and `:level`). The default of `false` means that the
53+
handler will only send **crash reports**, which are messages with metadata that has the
54+
shape of an exit reason and a stacktrace.
55+
"""
56+
],
57+
rate_limiting: [
58+
type: {:or, [{:in, [nil]}, {:non_empty_keyword_list, rate_limiting_options_schema}]},
59+
doc: """
60+
*since v10.4.0* - If present, enables rate
61+
limiting of reported messages. This can help avoid "spamming" Sentry with
62+
repeated log messages. To disable rate limiting, set this to `nil` or don't
63+
pass it altogether.
64+
65+
#{NimbleOptions.docs(rate_limiting_options_schema)}
66+
""",
67+
type_doc: "`t:keyword/0` or `nil`",
68+
default: nil
69+
]
70+
]
71+
72+
@options_schema NimbleOptions.new!(options_schema)
73+
274
@moduledoc """
375
`:logger` handler to report logged events to Sentry.
476
@@ -78,37 +150,7 @@ defmodule Sentry.LoggerHandler do
78150
79151
This handler supports the following configuration options:
80152
81-
* `:excluded_domains` (list of `t:atom/0`) - any messages with a domain in
82-
the configured list will not be sent. Defaults to `[:cowboy]` to avoid
83-
double-reporting events from `Sentry.PlugCapture`.
84-
85-
* `:metadata` (list of `t:atom/0`, or `:all`) - use this to include
86-
non-Sentry logger metadata in reports. If it's a list of keys, metadata in
87-
those keys will be added in the `:extra` context (see
88-
`Sentry.Context.set_extra_context/1`) under the `:logger_metadata` key.
89-
If set to `:all`, all metadata will be included. Defaults to `[]`.
90-
91-
* `:level` (`t:Logger.level/0`) - the minimum [`Logger`
92-
level](https://hexdocs.pm/logger/Logger.html#module-levels) to send events for.
93-
Defaults to `:error`.
94-
95-
* `:capture_log_messages` (`t:boolean/0`) - when `true`, this module will
96-
report all logged messages to Sentry (provided they're not filtered by
97-
`:excluded_domains` and `:level`). Defaults to `false`, which will only
98-
send **crash reports**, which are messages with metadata that has the
99-
shape of an exit reason and a stacktrace.
100-
101-
* `:rate_limiting` (`t:keyword/0`, since *v10.4.0*) - if present, enables rate
102-
limiting of reported messages. This can help avoid "spamming" Sentry with
103-
repeated log messages. To disable rate limiting, set this to `nil` or don't
104-
pass it altogether (which is the default). If this option is present, these
105-
nested options are **required**:
106-
107-
* `:max_events` (`t:non_neg_integer/0`) - the maximum number of events
108-
to send to Sentry in the `:interval` period.
109-
110-
* `:interval` (`t:non_neg_integer/0`) - the interval (in *milliseconds*)
111-
to send `:max_events` events.
153+
#{NimbleOptions.docs(@options_schema)}
112154
113155
"""
114156

@@ -118,28 +160,18 @@ defmodule Sentry.LoggerHandler do
118160
alias Sentry.LoggerHandler.RateLimiter
119161

120162
# The config for this logger handler.
121-
defstruct level: :error,
122-
excluded_domains: [:cowboy],
123-
metadata: [],
124-
capture_log_messages: false,
125-
rate_limiting: nil
126-
127-
@valid_config_keys [
128-
:excluded_domains,
129-
:capture_log_messages,
130-
:metadata,
131-
:level,
132-
:rate_limiting
133-
]
163+
defstruct [:level, :excluded_domains, :metadata, :capture_log_messages, :rate_limiting]
134164

135165
## Logger handler callbacks
136166

137167
# Callback for :logger handlers
138168
@doc false
139169
@spec adding_handler(:logger.handler_config()) :: {:ok, :logger.handler_config()}
140170
def adding_handler(config) do
141-
config = Map.put_new(config, :config, %__MODULE__{})
142-
config = update_in(config.config, &cast_config(__MODULE__, &1))
171+
# The :config key may not be here.
172+
sentry_config = Map.get(config, :config, %{})
173+
174+
config = Map.put(config, :config, cast_config(%__MODULE__{}, sentry_config))
143175

144176
if rate_limiting_config = config.config.rate_limiting do
145177
_ = RateLimiter.start_under_sentry_supervisor(config.id, rate_limiting_config)
@@ -154,33 +186,42 @@ defmodule Sentry.LoggerHandler do
154186
@spec changing_config(:update, :logger.handler_config(), :logger.handler_config()) ::
155187
{:ok, :logger.handler_config()}
156188
def changing_config(:update, old_config, new_config) do
189+
new_sentry_config =
190+
if is_struct(new_config.config, __MODULE__) do
191+
Map.from_struct(new_config.config)
192+
else
193+
new_config.config
194+
end
195+
196+
updated_config = update_in(old_config.config, &cast_config(&1, new_sentry_config))
197+
157198
_ignored =
158199
cond do
159-
new_config.config.rate_limiting == old_config.config.rate_limiting ->
200+
updated_config.config.rate_limiting == old_config.config.rate_limiting ->
160201
:ok
161202

162203
# Turn off rate limiting.
163-
old_config.config.rate_limiting && is_nil(new_config.config.rate_limiting) ->
164-
:ok = RateLimiter.terminate_and_delete(new_config.id)
204+
old_config.config.rate_limiting && is_nil(updated_config.config.rate_limiting) ->
205+
:ok = RateLimiter.terminate_and_delete(updated_config.id)
165206

166207
# Turn on rate limiting.
167-
is_nil(old_config.config.rate_limiting) && new_config.config.rate_limiting ->
208+
is_nil(old_config.config.rate_limiting) && updated_config.config.rate_limiting ->
168209
RateLimiter.start_under_sentry_supervisor(
169-
new_config.id,
170-
new_config.config.rate_limiting
210+
updated_config.id,
211+
updated_config.config.rate_limiting
171212
)
172213

173214
# The config changed, so restart the rate limiter with the new config.
174215
true ->
175-
:ok = RateLimiter.terminate_and_delete(new_config.id)
216+
:ok = RateLimiter.terminate_and_delete(updated_config.id)
176217

177218
RateLimiter.start_under_sentry_supervisor(
178-
new_config.id,
179-
new_config.config.rate_limiting
219+
updated_config.id,
220+
updated_config.config.rate_limiting
180221
)
181222
end
182223

183-
{:ok, update_in(old_config.config, &cast_config(&1, new_config.config))}
224+
{:ok, updated_config}
184225
end
185226

186227
# Callback for :logger handlers
@@ -276,9 +317,13 @@ defmodule Sentry.LoggerHandler do
276317

277318
## Helpers
278319

279-
defp cast_config(existing_config, new_config) do
280-
config_attrs = Map.take(new_config, @valid_config_keys)
281-
struct!(existing_config, config_attrs)
320+
defp cast_config(%__MODULE__{} = existing_config, %{} = new_config) do
321+
validated_config =
322+
new_config
323+
|> Map.to_list()
324+
|> NimbleOptions.validate!(@options_schema)
325+
326+
struct!(existing_config, validated_config)
282327
end
283328

284329
defp log_from_crash_reason(

0 commit comments

Comments
 (0)