diff --git a/.gitignore b/.gitignore index 30b607d..c5c162d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +*~ .* /deps/ /ebin/ +/_build +rebar.lock diff --git a/Makefile b/Makefile index e058979..57d4789 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PROJECT = raven DIALYZER = dialyzer -REBAR = rebar +REBAR = rebar3 all: app diff --git a/rebar.config b/rebar.config index 4d7f566..990da36 100644 --- a/rebar.config +++ b/rebar.config @@ -1,10 +1,17 @@ %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ts=4 sw=4 noet syntax=erlang -{erl_opts, [ - warnings_as_errors, - warn_export_all, - {platform_define, "^R14", no_callbacks} -]}. -{deps, [ - {jiffy, ".*", {git, "git://github.com/davisp/jiffy.git", {tag, "0.13.1"}}} -]}. +{erl_opts, [ warnings_as_errors, + warn_export_all, + {platform_define, "^R14", no_callbacks} + ] +}. + +{deps, [ {jsx, ".*", {git, "git@github.com:kivra/jsx.git", {tag, "2.9.0"}}} ] +}. + +{profiles, [ {test, [ {deps, [ {meck, {git, "git@github.com:kivra/meck.git", {tag, "0.8.13"}}}]}, + {extra_src_dirs, [{"test", [{recursive, true}]}]} + ] + } + ] +}. diff --git a/src/raven.app.src b/src/raven.app.src index 54ee74f..98e8c0f 100644 --- a/src/raven.app.src +++ b/src/raven.app.src @@ -8,8 +8,7 @@ crypto, public_key, ssl, - inets, - jiffy + inets ]}, {mod, {raven_app, []}}, {env, [ diff --git a/src/raven.erl b/src/raven.erl index 685a1fe..399c1b4 100644 --- a/src/raven.erl +++ b/src/raven.erl @@ -1,9 +1,13 @@ -module(raven). -export([ capture/2, + capture_prepare/2, + capture_with_backoff_send/2, user_agent/0 ]). +-include("raven.hrl"). + -define(SENTRY_VERSION, "2.0"). -record(cfg, { @@ -11,7 +15,8 @@ public_key :: string(), private_key :: string(), project :: string(), - ipfamily :: atom() + ipfamily :: atom(), + release :: binary() | undefined }). -type cfg_rec() :: #cfg{}. @@ -27,34 +32,61 @@ capture(Message, Params) when is_list(Message) -> capture(unicode:characters_to_binary(Message), Params); capture(Message, Params) -> + {ok, Body} = capture_prepare(Message, Params), + capture_with_backoff_send(Body, false). + +capture_prepare(Message, Params) -> Cfg = get_config(), - Document = {[ + Document = [ {event_id, event_id_i()}, {project, unicode:characters_to_binary(Cfg#cfg.project)}, {platform, erlang}, {server_name, node()}, {timestamp, timestamp_i()}, + {release, Cfg#cfg.release}, {message, term_to_json_i(Message)} | lists:map(fun ({stacktrace, Value}) -> - {'sentry.interfaces.Stacktrace', {[ + {'sentry.interfaces.Stacktrace', [ {frames,lists:reverse([frame_to_json_i(Frame) || Frame <- Value])} - ]}}; + ]}; ({exception, {Type, Value}}) -> - {'sentry.interfaces.Exception', {[ - {type, Type}, + {'sentry.interfaces.Exception', [ + {type, term_to_json_i(Type)}, + {value, term_to_json_i(Value)} + ]}; + ({exception, Value}) -> + {'sentry.interfaces.Exception', [ + {type, error}, {value, term_to_json_i(Value)} - ]}}; + ]}; + ({http_request, {Method, Url, Headers}}) -> + {'sentry.interfaces.Http', [ + {method, Method}, + {url, Url}, + {headers, Headers} + ]}; + % Reserved keys are 'id', 'username', 'email' and 'ip_address' out + % of which ONE needs to be supplied. Additional arbitrary keys may + % also be sent. + ({user, KVs}) when is_list(KVs) -> + {'sentry.interfaces.User', KVs}; ({tags, Tags}) -> - {tags, {[{Key, term_to_json_i(Value)} || {Key, Value} <- Tags]}}; + {tags, [{Key, term_to_json_i(Value)} || {Key, Value} <- Tags]}; ({extra, Tags}) -> - {extra, {[{Key, term_to_json_i(Value)} || {Key, Value} <- Tags]}}; + {extra, [{Key, term_to_json_i(Value)} || {Key, Value} <- Tags]}; ({Key, Value}) -> {Key, term_to_json_i(Value)} end, Params) - ]}, + ], + Body = base64:encode(zlib:compress(jsx:encode(Document))), + {ok, Body}. + +%Synchronized set to true returns backoff +%otherwise, it is not returned +capture_with_backoff_send(Body, Synchronized) -> + Cfg = get_config(), Timestamp = integer_to_list(unix_timestamp_i()), - Body = base64:encode(zlib:compress(jiffy:encode(Document, [force_utf8]))), UA = user_agent(), Headers = [ {"X-Sentry-Auth", @@ -65,18 +97,39 @@ capture(Message, Params) -> {"User-Agent", UA} ], ok = httpc:set_options([{ipfamily, Cfg#cfg.ipfamily}]), - httpc:request(post, + {ok, Result} = httpc:request(post, {Cfg#cfg.uri ++ "/api/store/", Headers, "application/octet-stream", Body}, - [], - [{body_format, binary}, {sync, false}] + [ssl_options()], + [{body_format, binary}, {sync, Synchronized}], + ?RAVEN_HTTPC_PROFILE ), - ok. + case Synchronized of + false -> ok; + true -> {ok, extract_backoff(Result)} + end. + +extract_backoff(Result) when is_reference(Result) -> + io:format("~nHTTP return was reference ~p~n", [Result]), + 0; +extract_backoff({StatusLine, Headers, _Body}) -> + {_,ResponseCode, _} = StatusLine, + case ResponseCode of + 429 -> + Backoff = list_to_integer(proplists:get_value("retry-after", Headers)), + io:format(" retry: ~p~n", [Backoff]), + Backoff; + _ -> + 0 + end. -spec user_agent() -> iolist(). user_agent() -> {ok, Vsn} = application:get_key(raven, vsn), ["raven-erlang/", Vsn]. +ssl_options() -> + persistent_term:get(?RAVEN_SSL_PERSIST_KEY). + %% @private -spec get_config() -> cfg_rec(). get_config() -> @@ -85,6 +138,7 @@ get_config() -> -spec get_config(App :: atom()) -> cfg_rec(). get_config(App) -> {ok, IpFamily} = application:get_env(App, ipfamily), + Release = application:get_env(App, release, undefined), case application:get_env(App, dsn) of {ok, Dsn} -> {match, [_, Protocol, PublicKey, SecretKey, Uri, Project]} = @@ -93,7 +147,8 @@ get_config(App) -> public_key = PublicKey, private_key = SecretKey, project = Project, - ipfamily = IpFamily}; + ipfamily = IpFamily, + release = Release}; undefined -> {ok, Uri} = application:get_env(App, uri), {ok, PublicKey} = application:get_env(App, public_key), @@ -103,16 +158,17 @@ get_config(App) -> public_key = PublicKey, private_key = PrivateKey, project = Project, - ipfamily = IpFamily} + ipfamily = IpFamily, + release = Release} end. event_id_i() -> - U0 = crypto:rand_uniform(0, (2 bsl 32) - 1), - U1 = crypto:rand_uniform(0, (2 bsl 16) - 1), - U2 = crypto:rand_uniform(0, (2 bsl 12) - 1), - U3 = crypto:rand_uniform(0, (2 bsl 32) - 1), - U4 = crypto:rand_uniform(0, (2 bsl 30) - 1), + U0 = rand:uniform((2 bsl 32) - 1) - 1, + U1 = rand:uniform((2 bsl 16) - 1) - 1, + U2 = rand:uniform((2 bsl 12) - 1) - 1, + U3 = rand:uniform((2 bsl 32) - 1) - 1, + U4 = rand:uniform((2 bsl 30) - 1) - 1, <> = <>, iolist_to_binary(io_lib:format("~32.16.0b", [UUID])). @@ -136,7 +192,6 @@ frame_to_json_i({Module, Function, Arguments, Location}) -> false -> -1; {line, L} -> L end, - { case is_list(Arguments) of true -> [{vars, [iolist_to_binary(io_lib:format("~w", [Argument])) || Argument <- Arguments]}]; false -> [] @@ -148,10 +203,9 @@ frame_to_json_i({Module, Function, Arguments, Location}) -> false -> <<(atom_to_binary(Module, utf8))/binary, ".erl">>; {file, File} -> list_to_binary(File) end} - ] - }. + ]. term_to_json_i(Term) when is_binary(Term); is_atom(Term) -> Term; term_to_json_i(Term) -> - iolist_to_binary(io_lib:format("~120p", [Term])). + iolist_to_binary(raven_io:format("~120p", [Term])). \ No newline at end of file diff --git a/src/raven.hrl b/src/raven.hrl new file mode 100644 index 0000000..6c352a6 --- /dev/null +++ b/src/raven.hrl @@ -0,0 +1,7 @@ + +%% The httpc profile used by raven +-define(RAVEN_HTTPC_PROFILE, raven). + +%% Key of persistent term for httpc ssl options +-define(RAVEN_SSL_PERSIST_KEY, {raven, ssl}). + diff --git a/src/raven_app.erl b/src/raven_app.erl index 65cc056..089ef6c 100644 --- a/src/raven_app.erl +++ b/src/raven_app.erl @@ -10,6 +10,8 @@ stop/1 ]). +-include("raven.hrl"). + -spec start() -> ok | {error, term()}. start() -> ensure_started(raven). @@ -21,13 +23,39 @@ stop() -> %% @hidden start(_StartType, _StartArgs) -> + case application:get_env(ssl) of + {ok, Options} -> + persistent_term:put(?RAVEN_SSL_PERSIST_KEY, {ssl, Options}); + _ -> + logger:notice("Raven not configured with httpc ssl options"), + persistent_term:put(?RAVEN_SSL_PERSIST_KEY, {ssl, []}) + end, + {ok, _ProfilePid} = inets:start(httpc, [{profile, ?RAVEN_HTTPC_PROFILE}]), case application:get_env(uri) of {ok, _} -> case application:get_env(error_logger) of - {ok, true} -> error_logger:add_report_handler(raven_error_logger); + {ok, true} -> + error_logger:add_report_handler(raven_error_logger); _ -> ok end, - raven_sup:start_link(); + case application:get_env(otp_logger) of + {ok, true} -> + logger:add_handler(raven_otp_logger, raven_logger_backend, #{level => warning + , filter_default => log + , filters => [{ssl, {fun logger_filters:domain/2, {stop, sub, [ssl]}}} + ,{progress, {fun logger_filters:domain/2, {stop, equal, [progress]}}} + ,{raven, {fun logger_filters:domain/2, {stop, sub, [raven]}}} + ,{sasl, {fun logger_filters:domain/2, {stop, sub, [otp, sasl]}}} + ]}); + _ -> + ok + end, + case raven_sup:start_link() of + {ok, Pid} -> + {ok, Pid}; + Error -> + Error + end; _ -> {error, missing_configuration} end. @@ -40,7 +68,9 @@ stop(_State) -> ok; _ -> ok - end. + end, + inets:stop(httpc, ?RAVEN_HTTPC_PROFILE), + ok. %% @private ensure_started(App) -> diff --git a/src/raven_error_logger.erl b/src/raven_error_logger.erl index f1bd0e5..03f8467 100644 --- a/src/raven_error_logger.erl +++ b/src/raven_error_logger.erl @@ -10,6 +10,7 @@ handle_info/2 ]). +-export([extract_user/1]). init(_) -> {ok, []}. @@ -65,6 +66,41 @@ parse_message(error = Level, Pid, "** Generic server " ++ _, [Name, LastMessage, {reason, Reason} ]} ]}; +%% OTP 20 crash reports where the client pid is dead don't include the stacktrace +parse_message(error = Level, Pid, "** Generic server " ++ _, [Name, LastMessage, State, Reason, Client]) -> + %% gen_server terminate + {Exception, Stacktrace} = parse_reason(Reason), + {format_exit(gen_server, Name, Reason), [ + {level, Level}, + {exception, Exception}, + {stacktrace, Stacktrace}, + {extra, [ + {name, Name}, + {pid, Pid}, + {last_message, LastMessage}, + {state, State}, + {reason, Reason}, + {client, Client} + ]} + ]}; +%% OTP 20 crash reports contain the pid of the client and stacktrace +parse_message(error = Level, Pid, "** Generic server " ++ _, [Name, LastMessage, State, Reason, Client, ClientStacktrace]) -> + %% gen_server terminate + {Exception, Stacktrace} = parse_reason(Reason), + {format_exit(gen_server, Name, Reason), [ + {level, Level}, + {exception, Exception}, + {stacktrace, Stacktrace}, + {extra, [ + {name, Name}, + {pid, Pid}, + {last_message, LastMessage}, + {state, State}, + {reason, Reason}, + {client, Client}, + {client_stacktrace, ClientStacktrace} + ]} + ]}; parse_message(error = Level, Pid, "** State machine " ++ _, [Name, LastMessage, StateName, State, Reason]) -> %% gen_fsm terminate {Exception, Stacktrace} = parse_reason(Reason), @@ -81,6 +117,39 @@ parse_message(error = Level, Pid, "** State machine " ++ _, [Name, LastMessage, {reason, Reason} ]} ]}; +parse_message(error = Level, Pid, "** State machine " ++ _, [Name, LastEvent, {StateName, StateData}, Class, Reason, CallbackMode, Stacktrace]) -> + %% gen_statem terminate + {format_exit(gen_statem, Name, Reason), [ + {level, Level}, + {exception, {Class, Reason}}, + {stacktrace, Stacktrace}, + {extra, [ + {name, Name}, + {pid, Pid}, + {last_event, LastEvent}, + {state_name, StateName}, + {state_data, StateData}, + {callback_mode, CallbackMode}, + {reason, Reason} + ]} + ]}; +parse_message(error = Level, Pid, "** State machine " ++ _, [Name, LastEvent, [{StateName, StateData}], Class, Reason, CallbackMode, Stacktrace]) -> + %% gen_statem terminate + %% sometimes gen_statem wraps its statename/data in a list for some reason??? + {format_exit(gen_statem, Name, Reason), [ + {level, Level}, + {exception, {Class, Reason}}, + {stacktrace, Stacktrace}, + {extra, [ + {name, Name}, + {pid, Pid}, + {last_event, LastEvent}, + {state_name, StateName}, + {state_data, StateData}, + {callback_mode, CallbackMode}, + {reason, Reason} + ]} + ]}; parse_message(error = Level, Pid, "** gen_event handler " ++ _, [ID, Name, LastMessage, State, Reason]) -> %% gen_event terminate {Exception, Stacktrace} = parse_reason(Reason), @@ -112,6 +181,23 @@ parse_message(error = Level, Pid, "** Generic process " ++ _, [Name, LastMessage {reason, Reason} ]} ]}; +parse_message(error = Level, Pid, "Error in process " ++ _, + [Name, Node, [ {reason, Reason} + , {mfa, {Handler, _, _}} + , {stacktrace, Stacktrace} + | Extras ]]) -> + %% cowboy_handler terminate + {format_exit(process, Name, {Reason, Stacktrace}), [ + {level, Level}, + {exception, {exit, Reason}}, + {stacktrace, Stacktrace}, + {extra, [ + {name, Name}, + {pid, Pid}, + {node, Node}, + {handler, Handler} | Extras + ]} + ]}; parse_message(error = Level, Pid, "Error in process " ++ _, [Name, Node, Reason]) -> %% process terminate {Exception, Stacktrace} = parse_reason(Reason), @@ -128,6 +214,374 @@ parse_message(error = Level, Pid, "Error in process " ++ _, [Name, Node, Reason] ]}; parse_message(_Level, _Pid, "Ranch listener " ++ _, _Data) -> mask; +%% Start of Kivra specific +%% --- rest_prelude ---- +parse_message(error = Level, Pid, "Unhandled error: ~p~n~p", + [[{method, Method}, {url, Url}, {headers, Headers}], + {unknown_error, Error}] = Data) -> + {format("Unhandled error: ~p", [Error]), [ + {level, Level}, + {http_request, {Method, Url, Headers}}, + {extra, [ + {pid, Pid}, + {data, Data} + ]} | + case Error of + {lifted_exn, Exception, Stacktrace} -> + [{exception, Exception}, + {stacktrace, Stacktrace}]; + _ -> + [] + end + ]}; +parse_message(Level, Pid, "Too Many Requests: ~s ~p\n" + "Rate Limit Key: ~s\n" + "Raven User: ~p", + [Method, Resource, RateLimitKey, RavenUser] = _Data) when is_list(RavenUser) -> + {format("Too Many Requests: ~s ~p\n" + "Rate Limit Key: ~s", [Method, Resource, RateLimitKey]), [ + {level, Level}, + {exception, {too_many_requests, {Method, Resource}}}, + {user, RavenUser}, + {extra, [ + {pid, Pid} + ]} + ]}; +%% --- General --- +parse_message(Level, Pid, "Error: ~p" ++ _ = Format, [{failed, _Reason} = Exception | _] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, Exception}, + {extra, [ + {pid, Pid} + ]} + ]}; +parse_message(Level, Pid, "Error: ~p" ++ _ = Format, [{failed, Reason, Extras} | Rest]) + when is_list(Extras) -> + {User, ExtrasWithoutUser} = extract_user(Extras), + {format(Format, [{failed, Reason} | Rest]), [ + {level, Level}, + {exception, {failed, Reason}}, + {extra, [ + {pid, Pid} | + [ {Key, Value} || {Key, Value} <- ExtrasWithoutUser, is_atom(Key) ] + ]} + | User + ]}; +parse_message(Level, Pid, "Warning: ~p~n" ++ Format, [{extras, Extras} | Data]) + when is_list(Extras) -> + {User, ExtrasWithoutUser} = extract_user(Extras), + {format(Format, Data), [ + {level, Level}, + {extra, [ + {pid, Pid} | + [ {Key, Value} || {Key, Value} <- ExtrasWithoutUser, is_atom(Key) ] + ]} + | User + ]}; +parse_message(Level, Pid, "Exception: ~p\n" + "Extras: ~p" = Format, + [{{Class, Reason}, [{_, _, _, _} | _] = Stacktrace}, Extras]) + when Class =:= exit; Class =:= error; Class =:= throw -> + {User, ExtrasWithoutUser} = extract_user(Extras), + {format(Format, [{Class, Reason}, Extras]), [ + {level, proplists:get_value(level, Extras, Level)}, + {exception, {Class, Reason}}, + {stacktrace, Stacktrace}, + {extra, [ + {pid, Pid} | [ {Key, Value} || {Key, Value} <- ExtrasWithoutUser, is_atom(Key) ] + ]} + | User + ]}; +%% Cybertron +parse_message(Level, Pid, "~p failed for 5 minutes: ~p" = Format, + [cybertron_email, {error, Status, _Headers, _Body}] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, {cybertron_email, Status}}}, + {extra, [ + {pid, Pid} + ]} + ]}; +%% ulogc +parse_message(Level, Pid, "ULog error: ~p/~p" ++ _ = Format, + [C, R | _] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {C, R}}, + {extra, [ + {pid, Pid} + ]} + ]}; +parse_message(Level, Pid, "ULog error: ~p~n" ++ _ = Format, + [{{Class, Reason}, [{_, _, _, _} | _] = Stacktrace} | Rest]) + when Class =:= exit; Class =:= error; Class =:= throw -> + {format(Format, [{Class, Reason} | Rest]), [ + {level, Level}, + {exception, {Class, Reason}}, + {stacktrace, Stacktrace}, + {extra, [ + {pid, Pid} + ]} + ]}; +parse_message(Level, Pid, "ULog error: ~p~n" ++ _ = Format, + [Rsn | _] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {ulog_error, Rsn}}, + {extra, [ + {pid, Pid} + ]} + ]}; +%% --- kivra_core_periodic --- +parse_message(Level, Pid, "[~p] " ++ _ = Format, [Operation | _] = Data) when is_atom(Operation) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, Operation}}, + {extra, [ + {pid, Pid} + ]} + ]}; +%% --- Mechanus --- +parse_message(Level, Pid, "~p: ~p no transition for ~p" = Format, [ID, Name, Event] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, + {mechanus_modron, transition, [{state, Name}, {event, Event}]}}}, + {extra, [ + {pid, Pid}, + {state, Name}, + {event, Event}, + {modron_id, ID} + ]} + ]}; +parse_message(Level, Pid, "~p: action ~p failed: ~p", + [ID, Action, {lifted_exn, Exception, Stacktrace}]) -> + {format("~p: action ~p failed", [ID, Action]), [ + {level, Level}, + {exception, Exception}, + {stacktrace, Stacktrace}, + {extra, [ + {pid, Pid}, + {action, Action}, + {modron_id, ID} + ]} + ]}; +parse_message(Level, Pid, "~p: action ~p failed: ~p" = Format, [ID, Action, Rsn] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, {mechanus_modron, action, Action, Rsn}}}, + {extra, [ + {pid, Pid}, + {action, Action}, + {modron_id, ID} + ]} + ]}; +%% --- Brod --- +parse_message(_Level, Pid, "Produce error ~s-~B Offset: ~B Error: ~p" = Format, + [Topic, _Partition, _Offset, ErrorCode] = Data) -> + Extra = "\nRetriable errors will be retried, actual failures will result in " + "an exit. Look for 'producer_down'.", + {format(Format ++ Extra, Data), [ + {level, warning}, + {exception, {failed, {brod, produce, Topic, ErrorCode}}}, + {extra, [ + {pid, Pid}, + {data, Data} + ]} + ]}; +parse_message(Level, Pid, "~p [~p] ~p is terminating\nreason: ~p~n" = Format, + [_Module, _Pid, _ClientId, Reason] = Data) -> + {Exception, Stacktrace} = parse_reason(Reason), + {format(Format, Data), [ + {level, Level}, + {exception, Exception}, + {stacktrace, Stacktrace}, + {extra, [ + {pid, Pid}, + {data, Data} + ]} + ]}; +parse_message(Level, Pid, "~p ~p terminating, reason:\n~p" = Format, + [_Module, _Pid, Reason] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {exit, Reason}}, + {extra, [ + {pid, Pid}, + {data, Data} + ]} + ]}; +%% --- KRC --- +parse_message(_Level, Pid, "{~p, ~p} error: ~p, attempt ~p of ~p" = Format, + [B, _K, Rsn, Attempt, MaxAttempts] = Data) when Attempt < MaxAttempts -> + {format(Format, Data), [ + {level, warning}, + {exception, {krc_error, {B, Rsn}}}, + {extra, [ + {pid, Pid}, + {data, Data} + ]} + ]}; +parse_message(Level, Pid, "Krc EXIT ~p: ~p" = Format, [_Pid, Rsn] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {krc_exit, Rsn}}, + {extra, [ + {pid, Pid}, + {data, Data} + ]} + ]}; +%% --- Pacioli calls from Kivra Core --- +parse_message(Level, Pid, "unable to fetch payments from pacioli for user ~p: ~p" = Format, + [UKey, Rsn] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, {pacioli, payment_collection, Rsn}}}, + {extra, [ + {pid, Pid}, + {user_key, UKey} + ]} + ]}; +parse_message(Level, Pid, "unable to pay using pacioli for user ~p: ~p" = Format, + [UKey, Rsn] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, {pacioli, pay, Rsn}}}, + {extra, [ + {pid, Pid}, + {user_key, UKey} + ]} + ]}; +parse_message(Level, Pid, "unable to delete mandate from pacioli for user ~p: ~p" = Format, + [UKey, Rsn] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, {pacioli, del_mandate, Rsn}}}, + {extra, [ + {pid, Pid}, + {user_key, UKey} + ]} + ]}; +parse_message(Level, Pid, "unable to cancel payments from pacioli for user ~p: ~p" = Format, + [UKey, Rsn] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, {pacioli, cancel_payment, Rsn}}}, + {extra, [ + {pid, Pid}, + {user_key, UKey} + ]} + ]}; +parse_message(Level, Pid, "unable to onboard payments from pacioli for user ~p: ~p" = Format, + [UKey, Rsn] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {failed, {pacioli, new_mandate, Rsn}}}, + {extra, [ + {pid, Pid}, + {user_key, UKey} + ]} + ]}; +%% --- Pacioli --- +parse_message(Level, Pid, "** Exception: ~p~n" + "** Reason: ~p~n" + "** Stacktrace: ~p~n" ++ _ = Format, + [ {badmatch, {rollback, function_clause, [{M, F, Args, _} | _]}} + , _Rsn + , Stacktrace + | _ + ] = Data) -> + ExceptionValue = + case Args of + [Arg1|_] when is_atom(Arg1) -> {M, F, [Arg1, '...']}; + _ -> {M, F, length(Args)} + end, + {format(Format, Data), [ + {level, Level}, + {exception, {{badmatch, {rollback, function_clause, '...'}}, ExceptionValue}}, + {stacktrace, Stacktrace}, + {extra, [ + {pid, Pid} + ]} + ]}; +parse_message(Level, Pid, "** Exception: ~p~n" + "** Reason: ~p~n" + "** Stacktrace: ~p~n" ++ _ = Format, + [ {badmatch, {rollback, Exception, [{M, F, Arity, _} | _]}} + , _Rsn + , Stacktrace + | _ + ] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {{badmatch, {rollback, Exception, '...'}}, {M, F, Arity}}}, + {stacktrace, Stacktrace}, + {extra, [ + {pid, Pid} + ]} + ]}; +parse_message(Level, Pid, "** Exception: ~p~n" + "** Reason: ~p~n" + "** Stacktrace: ~p~n" ++ _ = Format, + [ { { { badmatch + , {error, {Exception, [{_,_,_,_}|_] = InnerStacktrace}} + } + , [{_,_,_,_}|_] = MiddleStacktrace + } + , _ + } + , _Rsn + , _Stacktrace + | _ + ] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {badmatch, {error, Exception, '...'}}}, + {stacktrace, InnerStacktrace ++ MiddleStacktrace}, + {extra, [ + {pid, Pid} + ]} + ]}; +parse_message(Level, Pid, "** Exception: ~p~n" + "** Reason: ~p~n" + "** Stacktrace: ~p~n" ++ _ = Format, + [ {ShutdownOrNoproc, {gen_server, CallOrCast, _}} + , _Rsn + , Stacktrace + | _ + ] = Data) -> + {format(Format, Data), [ + {level, Level}, + {exception, {ShutdownOrNoproc, {gen_server, CallOrCast, '...'}}}, + {stacktrace, Stacktrace}, + {extra, [ + {pid, Pid} + ]} + ]}; +parse_message(Level, Pid, "** Exception: ~p~n" + "** Reason: ~p~n" + "** Stacktrace: ~p~n" ++ _ = Format, + [ {assert, AssertData} + , _Rsn + , Stacktrace + | _ + ] = Data) -> + Expression = proplists:get_value(expression, AssertData), + {format(Format, Data), [ + {level, Level}, + {exception, {assert, format_string(Expression)}}, + {stacktrace, Stacktrace}, + {extra, [ + {pid, Pid} + ]} + ]}; +%% --- KKng --- +% Mask warnings for failed tasks in KKng +parse_message(warning = _Level, _Pid, "failed task: ~w", [_Tid]) -> + mask; + +%% End of Kivra specific parse_message(Level, Pid, Format, Data) -> {format(Format, Data), [ {level, Level}, @@ -188,24 +642,6 @@ parse_report(Level, Pid, supervisor_report, [{errorContext, Context}, {offender, {shutdown, proplists:get_value(shutdown, Offender)} ]} ]}; -parse_report(info, Pid, progress, [{started, Started}, {supervisor, Supervisor}]) -> - Message = case proplists:get_value(name, Started, []) of - [] -> format("Supervisor ~s started child", [format_name(Supervisor)]); - Name -> format("Supervisor ~s started ~s", [format_name(Supervisor), format_name(Name)]) - end, - {Message, [ - {level, info}, - {logger, supervisors}, - {extra, [ - {supervisor, Supervisor}, - {pid, Pid}, - {child_pid, proplists:get_value(pid, Started)}, - {mfa, format_mfa(proplists:get_value(mfargs, Started))}, - {restart_type, proplists:get_value(restart_type, Started)}, - {child_type, proplists:get_value(child_type, Started)}, - {shutdown, proplists:get_value(shutdown, Started)} - ]} - ]}; parse_report(Level, Pid, Type, Report) -> Message = case proplists:get_value(message, Report, []) of [] -> <<"Report from process">>; @@ -284,6 +720,10 @@ format_reason({bad_return_value, Val}) -> ["bad return value ", format_term(Val)]; format_reason({{bad_return_value, Val}, Trace}) -> ["bad return value ", format_term(Val), " in ", format_mfa(Trace)]; +format_reason({bad_return_from_state_function, Val}) -> + ["bad return value from state function ", format_term(Val)]; +format_reason({{bad_return_from_state_function, Val}, Trace}) -> + ["bad return value from state function ", format_term(Val), " in ", format_mfa(Trace)]; format_reason({{badrecord, Record}, Trace}) -> ["bad record ", format_term(Record), " in ", format_mfa(Trace)]; format_reason({{case_clause, Value}, Trace}) -> @@ -369,4 +809,59 @@ format_term(Term) -> %% @private format(Format, Data) -> - iolist_to_binary(io_lib:format(Format, Data)). + iolist_to_binary(raven_io:format(Format, Data)). + +%% Start of Kivra specific +%% @private +-type proplist() :: [{any(), any()}]. +-spec extract_user(proplist() | binary()) -> {proplist(), proplist()}. +extract_user(Extras) -> + {[MaybeContext], ExtrasWithoutUser} = proplists:split(Extras, [user]), + UserContext = extract_user_context(MaybeContext), + {UserContext, ExtrasWithoutUser}. + +extract_user_context([]) -> []; +extract_user_context([{user, Context}]) when is_list(Context) -> [{user, Context}]; +extract_user_context([{user, Id}]) -> [{user, [{id, Id}]}]; +extract_user_context(Other) -> Other. + +%%%_* Tests ============================================================ +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +extract_user_test_() -> + [ + fun() -> + Actual = extract_user(Extras), + ?assertEqual(Expected, Actual) + end + || + {Extras, Expected} <- [ + {[], {[], []}}, + { + [{data, <<"stuff">>}], + {[], [{data, <<"stuff">>}]} + }, + { + [{user, <<"key">>}], + {[{user, [{id, <<"key">>}]}], []} + }, + { + [{user, [{email, <<"jane@example.com">>}]}], + {[{user, [{email, <<"jane@example.com">>}]}], []} + }, + { + [ + {user, [{id, <<"key">>}]}, + {data, <<"stuff">>} + ], + { + [{user, [{id, <<"key">>}]}], + [{data, <<"stuff">>}] + } + } + ] + ]. + +-endif. +%% End of Kivra specific diff --git a/src/raven_io.erl b/src/raven_io.erl new file mode 100644 index 0000000..7814445 --- /dev/null +++ b/src/raven_io.erl @@ -0,0 +1,40 @@ +-module(raven_io). + +-export([format/2]). + +format(Format, Args) -> + Chars = + try lists:flatten( + io_lib:build_text( + lists:map( fun(Elem) -> update(Elem, 50, 50) end + , io_lib:scan_format(Format, Args)))) + catch + _:_ -> + lists:flatten( + io_lib:format("FORMAT ERROR: ~p ~p", [Format, Args])) + end, + + MaxLenth = 8192, + if + length(Chars) =< MaxLenth -> Chars; + true -> lists:sublist(Chars, MaxLenth - 3) ++ "..." + end. + +update(M = #{control_char := $p, args := [Arg]}, PDepth, _) -> + M#{control_char := $P, args := [Arg, PDepth]}; +update(M = #{control_char := $w, args := [Arg]}, _, WDepth) -> + M#{control_char := $W, args := [Arg, WDepth]}; +update(X, _, _) -> X. + + +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). + +format_test() -> + %% Format error + ?assertEqual( "FORMAT ERROR: \"Data: ~p\" wat" + , format("Data: ~p", wat)), + + ok. +-endif. \ No newline at end of file diff --git a/src/raven_lager_backend.erl b/src/raven_lager_backend.erl index c7b57c5..8b709cc 100644 --- a/src/raven_lager_backend.erl +++ b/src/raven_lager_backend.erl @@ -5,34 +5,36 @@ -export([ - init/1, - code_change/3, - terminate/2, - handle_call/2, - handle_event/2, - handle_info/2 + init/1, + code_change/3, + terminate/2, + handle_call/2, + handle_event/2, + handle_info/2 ]). -record(state, {level}). -init(Level) -> - {ok, #state{level=lager_util:config_to_mask(Level)}}. +init(Level) when is_atom(Level) -> + init([{level, Level}]); +init([{level, Level}]) -> + {ok, #state{level=lager_util:level_to_num(Level)}}. %% @private handle_call(get_loglevel, #state{level=Level} = State) -> {ok, Level, State}; handle_call({set_loglevel, Level}, State) -> - try lager_util:config_to_mask(Level) of + try lager_util:level_to_num(Level) of Levels -> - {ok, ok, State#state{level=Levels}} - catch - _:_ -> - {ok, {error, bad_log_level}, State} - end; + {ok, ok, State#state{level=Levels}} + catch + _:_ -> + {ok, {error, bad_log_level}, State} + end; handle_call(_, State) -> - {ok, ok, State}. + {ok, ok, State}. %% @private handle_event({log, Data}, @@ -49,46 +51,24 @@ handle_event(_Event, State) -> handle_info(_, State) -> - {ok, State}. + {ok, State}. code_change(_, State, _) -> - {ok, State}. + {ok, State}. terminate(_, _) -> - ok. + ok. -capture(mask) -> - ok; capture({Message, Params}) -> raven:capture(Message, Params). -%% TODO - check what other metadata can be sent to sentry -parse_message({lager_msg, [], MetaData, Level, _, _Time, Message}) -> - case parse_meta(MetaData) of - mask -> - mask; - Extra -> - {Message, [{level, Level}, - {extra, Extra}]} - end. - - -%% @doc Extracts pid from lager message metadata. Lager messages that came -%% from error_logger are flagged as such in the metadata, in which case we -%% immediately return 'mask', indicating that the message should be skipped. -%% This assumes that raven's error_logger handler is installed, to avoid -%% double-capturing error_logger events. -%% TODO: respect default_error_logger config, instead of assuming it is set -%% to true. -parse_meta(MetaData) -> - parse_meta(MetaData, []). - -parse_meta([], Acc) -> - Acc; -parse_meta([{pid, Pid} = PidProp | Rest], Acc) when is_pid(Pid) -> - parse_meta(Rest, [PidProp | Acc]); -parse_meta([{error_logger, _} | _Rest], _Acc) -> - mask; -parse_meta([{_, _} | Rest], Acc) -> - parse_meta(Rest, Acc). +parse_message(Log) -> + {lager_msg:message(Log), [ {level, lager_msg:severity(Log)} + | extra(Log) + ]}. +extra(Log) -> + case lager_msg:metadata(Log) of + [] -> []; + Extra -> [{extra, Extra}] + end. diff --git a/src/raven_logger_backend.erl b/src/raven_logger_backend.erl new file mode 100644 index 0000000..5ea05cc --- /dev/null +++ b/src/raven_logger_backend.erl @@ -0,0 +1,298 @@ +-module(raven_logger_backend). +-export([ log/2 +]). + +-include_lib("kernel/include/logger.hrl"). +-include("raven.hrl"). + +%% see here: https://develop.sentry.dev/sdk/event-payloads/ +-define(ATTRIBUTE_FILTER, [ event_id, timestamp, platform, level, logger, + transaction, server_name, release, dist, tags, + environment, modules, extra, fingerprint, errors, + user, http_request, stacktrace, exception]). + +%% API + +log(LogEvent, _Config) -> + try log(LogEvent) + catch _:Reason:StackTrace -> + LE = list_to_binary(lists:flatten(io_lib:format("~0p", [LogEvent]))), + ST = list_to_binary(lists:flatten(io_lib:format("~0p", [StackTrace]))), + ?LOG_WARNING(#{ message => <<"Raven logger backend crashed">>, + crash_message => LE, + reason => Reason, + stacktrace => ST}) + end. + +%% Private + +log(LogEvent) -> + case is_loop(LogEvent) of + true -> ok; %% Dropping prevents log loop + false -> + Message = get_msg(LogEvent), + Args = get_args(Message, LogEvent), + raven_send_sentry_safe:capture(Message, Args) + end. + +is_loop(LogEvent) -> + is_log_crash_log(LogEvent) or is_httpc_log(LogEvent). + +is_log_crash_log(#{msg := Msg} = _LogEvent) -> + case Msg of + {report, #{ message := <<"Raven logger backend crashed">>, + crash_message := _, + reason := _, + stacktrace := _}} -> + true; + _ -> + false + end. + +is_httpc_log(#{meta := Meta} = _LogEvent) -> + case maps:is_key(report_cb, Meta) of + false -> false; + true -> #{report_cb := Report} = Meta, + Report =:= fun ssl_logger:format/1 + end. + +get_msg(#{msg := MsgList, meta := Meta} = _LogEvent) -> + case MsgList of + {string, Msg} -> Msg; + {report, Report} -> get_msg_from_report(Report, Meta); + {Format, _Args} when is_list(Format) -> Format; + _ -> unexpected_log_format(Meta) + end. + +%% Specific choice of msg +get_msg_from_report(#{format := Format, args := Args} = _Report, _Meta) -> + make_readable(Format, Args); +get_msg_from_report(#{description := Description} = _Report, _Meta) -> + Description; +get_msg_from_report(#{message := Message} = _Report, _Meta) -> + Message; +get_msg_from_report(#{reason := Reason} = _Report, _Meta) -> + Reason; +get_msg_from_report(#{error := Error} = _Report, _Meta) -> + Error; +%% If no specific choice, then use provided report_cb +get_msg_from_report(Report, #{error_logger := #{report_cb := Report_cb}} = _Meta) when is_function(Report_cb) -> + {Format, Args} = Report_cb(Report), + make_readable(Format, Args); +get_msg_from_report(Report, #{report_cb := Report_cb} = _Meta) when is_function(Report_cb) -> + {Format, Args} = Report_cb(Report), + make_readable(Format, Args); +%% If nothing provided, then give up +get_msg_from_report(_Report, Meta) -> + unexpected_log_format(Meta). + +unexpected_log_format(Meta) -> + Module = maps:get(module, Meta), + "Unexpected log format in module: " ++ atom_to_list(Module). + + +make_readable(Format, Args) -> + try + iolist_to_binary(io_lib:format(Format, Args)) + catch + Exception:Reason -> iolist_to_binary(io_lib:format("Error in log format string: ~p:~p", [Exception, Reason])) + end. + +get_args(Message, LogEvent) -> + Level = sentry_level(maps:get(level, LogEvent)), + Meta = maps:get(meta, LogEvent), + MetaBasic = maps:with(?ATTRIBUTE_FILTER, Meta), + MetaExtra = maps:without(?ATTRIBUTE_FILTER, Meta), + Msg = maps:get(msg, LogEvent), + Reason = get_reason_maybe(LogEvent, Message), + Basic = MetaBasic#{level => Level}, + Extra = get_extra(Reason, MetaExtra, Msg), + + BasicList = maps:to_list(Basic), + ExtraList = maps:to_list(Extra), + + case maps:get(correlation_id, Meta, undefined) of + undefined -> + BasicList ++ [{extra, ExtraList}]; + CorrelationID -> + BasicList ++ [{extra, ExtraList}, + {tags, [{correlation_id, CorrelationID}]}] + end. + +sentry_level(notice) -> info; +sentry_level(Level) -> Level. + +get_reason_maybe(#{msg := {report, #{reason := Reason}}} = _LogEvent, _Default) -> + Reason; +get_reason_maybe(_LogEvent, Default) -> + Default. + +get_extra(Reason, ExtraMeta, {report, Report}) -> + Extra = maps:merge(ExtraMeta, Report), + Extra#{reason => Reason}; +get_extra(Reason, ExtraMeta, {string, _String}) -> + ExtraMeta#{reason => Reason}; +get_extra(Reason, ExtraMeta, {Format, Args}) when is_list(Format) -> + Msg = make_readable(Format, Args), + ExtraMeta#{ reason => Reason + , msg => Msg}; +get_extra(Reason, ExtraMeta, Msg) -> + ExtraMeta#{ reason => Reason + , msg => Msg}. + +%%%_* Tests ============================================================ +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +logger_backend_test_() -> + { setup, + fun test_setup/0, + fun test_teardown/1, + [ fun test_log_unknown/0, + fun test_log_string/0, + fun test_log_format/0, + fun test_log_report/0, + fun test_log_report_with_compound_description/0, + fun test_log_unknown_report/0 + ] + }. + +test_setup() -> + persistent_term:put(?RAVEN_SSL_PERSIST_KEY, {ssl, []}), + meck:new(raven_send_sentry_safe), + meck:new(httpc), + 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), + application:start(raven), %% To se key vsn + 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"). + +test_teardown(_) -> + 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), + application:stop(raven), + persistent_term:erase(?RAVEN_SSL_PERSIST_KEY), + ok. + +test_log_unknown() -> + Msg = "whatisthis", + Message = "Unexpected log format in module: ievan_polka", + Args = [{correlation_id,"123456789"}, + {level,info}, + {module, ievan_polka}, + {tags, [{correlation_id, "123456789"}]}, + {extra,[{line, 214}, + {msg, "whatisthis"}, + {reason,"Unexpected log format in module: ievan_polka"}]}], + run(Msg, Message, Args). + +test_log_string() -> + Msg = {string, "foo"}, + Message = "foo", + Args = [{correlation_id,"123456789"}, + {level,info}, + {module, ievan_polka}, + {tags, [{correlation_id, "123456789"}]}, + {extra,[{line, 214}, + {reason,"foo"}]}], + run(Msg, Message, Args). + +test_log_format() -> + Msg = {"Foo ~p", [14]}, + Message = "Foo ~p", + Args = [{correlation_id,"123456789"}, + {level,info}, + {module, ievan_polka}, + {tags, [{correlation_id, "123456789"}]}, + {extra,[{line, 214}, + {msg,<<"Foo 14">>}, + {reason,"Foo ~p"}]}], + run(Msg, Message, Args). + +test_log_report() -> + Msg = {report, #{description => "gunnar", + a => "foo", + b => "bar"}}, + Message = "gunnar", + Args = [{correlation_id,"123456789"}, + {level,info}, + {module, ievan_polka}, + {tags, [{correlation_id, "123456789"}]}, + {extra,[{a,"foo"}, + {b,"bar"}, + {description,"gunnar"}, + {line, 214}, + {reason,"gunnar"}]}], + run(Msg, Message, Args). + +test_log_report_with_compound_description() -> + Msg = {report, #{description => {namn, "gunnar"}, + a => "foo", + b => "bar"}}, + Message = {namn, "gunnar"}, + Args = [{correlation_id,"123456789"}, + {level,info}, + {module, ievan_polka}, + {tags, [{correlation_id, "123456789"}]}, + {extra,[{a,"foo"}, + {b,"bar"}, + {description,{namn, "gunnar"}}, + {line, 214}, + {reason,{namn, "gunnar"}}]}], + run(Msg, Message, Args). + +test_log_unknown_report() -> + Msg = {report, #{a => "foo", + b => "bar"}}, + Message = "Unexpected log format in module: ievan_polka", + Args = [{correlation_id,"123456789"}, + {level,info}, + {module, ievan_polka}, + {tags, [{correlation_id, "123456789"}]}, + {extra,[{a,"foo"}, + {b,"bar"}, + {line, 214}, + {reason,"Unexpected log format in module: ievan_polka"}]}], + run(Msg, Message, Args). + +run(Msg, ExpectedMessage, ExpectedArgs) -> + meck:reset([raven_send_sentry_safe, httpc]), + Event = event(Msg), + log(Event, []), + [{_Pid, MFA, _}] = meck:history(raven_send_sentry_safe), + {raven_send_sentry_safe, capture, [Message, Args]} = MFA, + ?assertEqual(ExpectedMessage, Message), + ?assertEqual(sort_args(ExpectedArgs), sort_args(Args)). + +event(Msg) -> + Level = info, + Meta = meta(), + #{level => Level, meta => Meta, msg => Msg}. + +meta() -> + #{correlation_id => "123456789", + module => ievan_polka, + line => 214}. + +sort_args(Args) -> + SortedExtras = lists:sort(proplists:get_value(extra, Args)), + lists:sort([{extra, SortedExtras} | proplists:delete(extra, Args)]). + +mock_capture(Message, Args) -> + raven:capture(Message, Args). + +mock_request(_Op, {_Path, _Headers, _Type, Body}, _, _, ?RAVEN_HTTPC_PROFILE) -> + Decoded = jsx:decode(zlib:uncompress(base64:decode(Body))), + io:format(user, "~n~p~n", [Decoded]), + {ok, {{foo,200,bar},[],<<"body">>}}. + +-endif. diff --git a/src/raven_send_sentry_safe.erl b/src/raven_send_sentry_safe.erl new file mode 100644 index 0000000..715dd4d --- /dev/null +++ b/src/raven_send_sentry_safe.erl @@ -0,0 +1,77 @@ +-module(raven_send_sentry_safe). + +-behaviour(gen_server). + +-export([start/0, start_link/0, stop/0, capture/2]). + +-export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2]). + +%% API + +start() -> + gen_server:start({local, ?MODULE}, ?MODULE, undefined, []). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, undefined, []). + +stop() -> + gen_server:stop(?MODULE). + +capture(Message, Args) -> + gen_server:cast(?MODULE, {capture, Message, Args}). + + +%% gen_server callbacks + +init(_Arg) -> + logger:update_process_metadata(#{domain => [raven]}), + {ok, #{backoff_until => current_time()}}. + +terminate(_Arg, _State) -> + ok. + +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast(_Request = {capture, Message, Args}, State) -> + Qlen = qlen(), + if + Qlen > 10 -> + io:format(" skip, to long queue (~p)~n", [Qlen]), + {noreply, State}; + true -> + #{backoff_until := Bou} = State, + Now = current_time(), + if + Bou > Now -> + logger:warning(<<"Sentry dropped log event">>), + {noreply, State}; + true -> + {ok, BackoffUntil} = raven_capture(Message, Args), + {noreply, State#{backoff_until => BackoffUntil}} + end + end; +handle_cast(_Request, State) -> + {ok, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +%% Local + +qlen() -> + {message_queue_len, Qlen} = + erlang:process_info(self(), message_queue_len), + Qlen. + +raven_capture(Message, Args) -> + {ok, Body} = raven:capture_prepare(Message, Args), + case raven:capture_with_backoff_send(Body, true) of + ok -> + {ok, current_time()}; + {ok, Seconds} -> + {ok, current_time() + Seconds*1_000_000} + end. + +current_time() -> + erlang:system_time(microsecond). diff --git a/src/raven_sup.erl b/src/raven_sup.erl index ff4d878..993d62b 100644 --- a/src/raven_sup.erl +++ b/src/raven_sup.erl @@ -16,10 +16,9 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). - %% @hidden init([]) -> - {ok, { - {one_for_one, 5, 10}, [ - ] - }}. + Config = {one_for_one, 5, 10}, + SendSentrySafe = ?WORKER(raven_send_sentry_safe, start_link, [], permanent), + Workers = [SendSentrySafe], + {ok, {Config, Workers}}.