@@ -4,7 +4,9 @@ defmodule Sentry.LoggerHandler do
44
55 This module is similar to `Sentry.LoggerBackend`, but it implements a
66 [`:logger` handler](https://erlang.org/doc/man/logger_chapter.html#handlers) rather
7- than an Elixir's `Logger` backend.
7+ than an Elixir's `Logger` backend. It provides additional functionality compared to
8+ the `Logger` backend, such as rate-limiting of reported messages, better fingerprinting,
9+ and better handling of crashes.
810
911 *This module is available since v9.0.0 of this library*.
1012
@@ -96,23 +98,38 @@ defmodule Sentry.LoggerHandler do
9698 send **crash reports**, which are messages with metadata that has the
9799 shape of an exit reason and a stacktrace.
98100
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.
112+
99113 """
100114
101115 @ moduledoc since: "9.0.0"
102116
103117 alias Sentry.LoggerUtils
118+ alias Sentry.LoggerHandler.RateLimiter
104119
105120 # The config for this logger handler.
106121 defstruct level: :error ,
107122 excluded_domains: [ :cowboy ] ,
108123 metadata: [ ] ,
109- capture_log_messages: false
124+ capture_log_messages: false ,
125+ rate_limiting: nil
110126
111127 @ valid_config_keys [
112128 :excluded_domains ,
113129 :capture_log_messages ,
114130 :metadata ,
115- :level
131+ :level ,
132+ :rate_limiting
116133 ]
117134
118135 ## Logger handler callbacks
@@ -122,28 +139,73 @@ defmodule Sentry.LoggerHandler do
122139 @ spec adding_handler ( :logger . handler_config ( ) ) :: { :ok , :logger . handler_config ( ) }
123140 def adding_handler ( config ) do
124141 config = Map . put_new ( config , :config , % __MODULE__ { } )
125- { :ok , update_in ( config . config , & cast_config ( __MODULE__ , & 1 ) ) }
142+ config = update_in ( config . config , & cast_config ( __MODULE__ , & 1 ) )
143+
144+ if rate_limiting_config = config . config . rate_limiting do
145+ _ = RateLimiter . start_under_sentry_supervisor ( config . id , rate_limiting_config )
146+ { :ok , config }
147+ else
148+ { :ok , config }
149+ end
126150 end
127151
128152 # Callback for :logger handlers
129153 @ doc false
130154 @ spec changing_config ( :update , :logger . handler_config ( ) , :logger . handler_config ( ) ) ::
131155 { :ok , :logger . handler_config ( ) }
132156 def changing_config ( :update , old_config , new_config ) do
157+ _ignored =
158+ cond do
159+ new_config . config . rate_limiting == old_config . config . rate_limiting ->
160+ :ok
161+
162+ # 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 )
165+
166+ # Turn on rate limiting.
167+ is_nil ( old_config . config . rate_limiting ) && new_config . config . rate_limiting ->
168+ RateLimiter . start_under_sentry_supervisor (
169+ new_config . id ,
170+ new_config . config . rate_limiting
171+ )
172+
173+ # The config changed, so restart the rate limiter with the new config.
174+ true ->
175+ :ok = RateLimiter . terminate_and_delete ( new_config . id )
176+
177+ RateLimiter . start_under_sentry_supervisor (
178+ new_config . id ,
179+ new_config . config . rate_limiting
180+ )
181+ end
182+
133183 { :ok , update_in ( old_config . config , & cast_config ( & 1 , new_config . config ) ) }
134184 end
135185
186+ # Callback for :logger handlers
187+ @ doc false
188+ def removing_handler ( % { id: id } ) do
189+ :ok = RateLimiter . terminate_and_delete ( id )
190+ end
191+
136192 # Callback for :logger handlers
137193 @ doc false
138194 @ spec log ( :logger . log_event ( ) , :logger . handler_config ( ) ) :: :ok
139- def log ( % { level: log_level , meta: log_meta } = log_event , % { config: % __MODULE__ { } = config } ) do
195+ def log ( % { level: log_level , meta: log_meta } = log_event , % {
196+ config: % __MODULE__ { } = config ,
197+ id: handler_id
198+ } ) do
140199 cond do
141200 Logger . compare_levels ( log_level , config . level ) == :lt ->
142201 :ok
143202
144203 LoggerUtils . excluded_domain? ( Map . get ( log_meta , :domain , [ ] ) , config . excluded_domains ) ->
145204 :ok
146205
206+ config . rate_limiting && RateLimiter . increment ( handler_id ) == :rate_limited ->
207+ :ok
208+
147209 true ->
148210 # Logger handlers run in the process that logs, so we already read all the
149211 # necessary Sentry context from the process dictionary (when creating the event).
0 commit comments