11defmodule 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