Skip to content

Commit

Permalink
Add rate limiting
Browse files Browse the repository at this point in the history
  • Loading branch information
eproxus authored and kivra-adalin committed Oct 9, 2023
1 parent 3ab4749 commit 4949ffd
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 20 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ To add the raven backend:
Advanced Usage
==============

### Manual Logging

You can log directly events to sentry using the ```raven:capture/2``` function, for example:

```erlang
Expand All @@ -110,3 +112,21 @@ raven:capture("Test Event", [
]}
]).
```

### Rate Limiting

The Raven application can rate limit the number of events sent to Sentry. By
default rate limiting is disabled. To enable, configure the application
environment variable `rate_limit` with `{Intensity, Period}` where
`Intensity` is the maximum number of error reports in the time `Period` in
milliseconds.

Example `sys.config`:

```erlang
[
{raven, [
{rate_limit, {250, 60_000}} % No more than 250 reports per minute
]}
]
```
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
warn_export_all
]}.

{deps, [jsx]}.
{deps, [jsx, fuse]}.

{profiles, [
{test, [
Expand Down
5 changes: 4 additions & 1 deletion rebar.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{"1.2.0",
[{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}]}.
[{<<"fuse">>,{pkg,<<"fuse">>,<<"2.5.0">>},0},
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}]}.
[
{pkg_hash,[
{<<"fuse">>, <<"71AFA90BE21DA4E64F94ABBA9D36472FAA2D799C67FEDC3BD1752A88EA4C4753">>},
{<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}]},
{pkg_hash_ext,[
{<<"fuse">>, <<"7F52A1C84571731AD3C91D569E03131CC220EBAA7E2A11034405F0BAC46A4FEF">>},
{<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}]}
].
6 changes: 4 additions & 2 deletions src/raven.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
crypto,
public_key,
ssl,
inets
inets,
fuse
]},
{mod, {raven_app, []}},
{env, [
Expand All @@ -17,6 +18,7 @@
{public_key, ""},
{private_key, ""},
{error_logger, false},
{ipfamily, inet}
{ipfamily, inet},
{rate_limit, false}
]}
]}.
23 changes: 12 additions & 11 deletions src/raven.erl
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
capture(Message, Params) when is_list(Message) ->
capture(unicode:characters_to_binary(Message), Params);
capture(Message, Params) ->
{ok, Body} = capture_prepare(Message, Params),
{ok, _Backoff} = capture_with_backoff_send(Body),
ok.
raven_rate_limit:run(fun() ->
{ok, Body} = capture_prepare(Message, Params),
capture_with_backoff_send(Body)
end).

capture_prepare(Message, Params) ->
Cfg = get_config(),
Expand Down Expand Up @@ -102,14 +103,14 @@ capture_with_backoff_send(Body) ->
[{body_format, binary}],
?RAVEN_HTTPC_PROFILE
),
{ok, extract_backoff(Result)}.

extract_backoff({StatusLine, Headers, _Body}) ->
{_,ResponseCode, _} = StatusLine,
case ResponseCode of
429 -> list_to_integer(proplists:get_value("retry-after", Headers));
200 -> 0
end.
extract_backoff(Result),
ok.

extract_backoff({{_HTTP, 429, _Message}, Headers, _Body}) ->
DelaySeconds = list_to_integer(proplists:get_value("retry-after", Headers)),
raven_rate_limit:delay(DelaySeconds);
extract_backoff(_Response) ->
ok.

-spec user_agent() -> iolist().
user_agent() ->
Expand Down
4 changes: 3 additions & 1 deletion src/raven_app.erl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ start(_StartType, _StartArgs) ->
end,
case raven_sup:start_link() of
{ok, Pid} ->
raven_rate_limit:setup(),
{ok, Pid};
Error ->
Error
Expand All @@ -67,7 +68,8 @@ stop(_State) ->
ok
end,
ok = inets:stop(httpc, ?RAVEN_HTTPC_PROFILE),
true = persistent_term:erase(?RAVEN_SSL_PERSIST_KEY).
true = persistent_term:erase(?RAVEN_SSL_PERSIST_KEY),
raven_rate_limit:teardown().

%% @private
ensure_started(App) ->
Expand Down
9 changes: 5 additions & 4 deletions src/raven_logger_backend.erl
Original file line number Diff line number Diff line change
Expand Up @@ -164,22 +164,23 @@ test_setup() ->
meck:expect(raven_send_sentry_safe, capture, 2, fun mock_capture/2),
meck:expect(httpc, set_options, 1, fun(_) -> ok end),
meck:expect(httpc, request, 5, fun mock_request/5),
ok = application:start(raven),
{ok, Apps} = application:ensure_all_started(raven),
application:set_env(raven, ipfamily, dummy),
application:set_env(raven, uri, "http://foo"),
application:set_env(raven, public_key, <<"hello">>),
application:set_env(raven, private_key, <<"there">>),
application:set_env(raven, project, "terraform mars").
application:set_env(raven, project, "terraform mars"),
Apps.

test_teardown(_) ->
test_teardown(Apps) ->
meck:unload([raven_send_sentry_safe]),
meck:unload([httpc]),
application:unset_env(raven, ipfamily),
application:unset_env(raven, uri),
application:unset_env(raven, public_key),
application:unset_env(raven, private_key),
application:unset_env(raven, project),
ok = application:stop(raven),
[ok = application:stop(A) || A <- lists:reverse(Apps)],
error_logger:tty(true),
ok.

Expand Down
55 changes: 55 additions & 0 deletions src/raven_rate_limit.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-module(raven_rate_limit).

% API
-export([setup/0]).
-export([teardown/0]).
-export([run/1]).
-export([delay/1]).

%--- API -----------------------------------------------------------------------

setup() ->
Result = case application:get_env(raven, rate_limit, false) of
{Intensity, Period} ->
FuseConfig = {{standard, Intensity, Period}, {reset, Period}},
ok = fuse:install(?MODULE, FuseConfig),
{enabled, atomics:new(1, [{signed, false}])};
false ->
disabled
end,
persistent_term:put(?MODULE, Result).

teardown() ->
case persistent_term:get(?MODULE) of
{enabled, _} -> fuse:remove(?MODULE);
disabled -> ok
end,
persistent_term:erase(?MODULE).

run(Fun) ->
case persistent_term:get(?MODULE) of
disabled ->
Fun();
{enabled, Atomics} ->
RetryAfter = atomics:get(Atomics, 1),
case erlang:system_time(second) > RetryAfter of
true ->
case fuse:ask(?MODULE, async_dirty) of
blown ->
{error, {rate_limit, local}};
_ ->
ok = fuse:melt(?MODULE),
Fun()
end;
false ->
{error, {rate_limit, remote}}
end
end.

delay(Seconds) when Seconds > 0 ->
case persistent_term:get(?MODULE) of
disabled -> ok;
{enabled, Atomics} ->
RetryAfter = erlang:system_time(second) + Seconds + 1,
atomics:put(Atomics, 1, RetryAfter)
end.

0 comments on commit 4949ffd

Please sign in to comment.