├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── include ├── raven.hrl └── raven_error_logger.hrl ├── rebar.config ├── rebar.lock ├── src ├── raven.erl ├── raven_app.erl ├── raven_erlang.app.src ├── raven_error_logger.erl ├── raven_error_logger_filter.erl ├── raven_lager_backend.erl └── raven_sup.erl └── test ├── conf ├── deprecated_dsn.config └── sample.config └── raven_app_test.erl /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | ebin 3 | .eunit 4 | .rebar 5 | *.o 6 | *.beam 7 | *.plt 8 | erl_crash.dump 9 | .*.sw? 10 | .rebar3 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | sudo: false 3 | install: 4 | - wget https://s3.amazonaws.com/rebar3/rebar3 && chmod +x rebar3 5 | script: 6 | - make test 7 | otp_release: 8 | - 20.1 9 | - 20.0 10 | - 21.0 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2015 Ali Sabil 4 | Copyright (c) 2018 Yuri Artemev 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | ERL_FLAGS="-config ./test/conf/sample.config" rebar3 eunit 5 | ERL_FLAGS="-config ./test/conf/deprecated_dsn.config" rebar3 eunit 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raven-erlang 2 | 3 | raven-erlang is an Erlang client for [Sentry](http://aboutsentry.com/) that integrates with the standard `error_logger` module. It also serves as a [Lager](https://github.com/erlang-lager/lager) backend. 4 | 5 | [![Build Status](https://travis-ci.org/artemeff/raven-erlang.svg?branch=master)](https://travis-ci.org/artemeff/raven-erlang) 6 | [![Hex.pm](https://img.shields.io/hexpm/v/raven_erlang.svg)](https://hex.pm/packages/raven_erlang) 7 | 8 | ## Basic Usage 9 | 10 | ## Add as dependency 11 | 12 | Add raven as a dependency to your project, and include the `raven-erlang` application in your release. 13 | 14 | In `rebar.config`: 15 | 16 | ```erlang 17 | {deps, [ 18 | {raven_erlang, "0.4.3"} 19 | ]}. 20 | ``` 21 | 22 | To start `raven_erlang` with your application, add in your `myapp.app.src`: 23 | 24 | ```erlang 25 | % ... 26 | {applications, [ 27 | % ... 28 | raven_erlang 29 | ]}, 30 | % ... 31 | ``` 32 | 33 | ## Configure 34 | 35 | `raven_erlang` is configured using the application environment. This is generally done in app.config or sys.config: 36 | 37 | ```erlang 38 | {raven_erlang, [ 39 | % One can point `raven_erlang` to a project like this: 40 | {uri, "https://app.getsentry.com"}, 41 | {project, "1"}, 42 | {public_key, "PUBLIC_KEY"}, 43 | {private_key, "PRIVATE_KEY"}, % This is now optional 44 | 45 | % ...or just use the DSN: 46 | {dsn, "https://PUBLIC_KEY@app.getsentry.com/1"}, 47 | % {dsn, "https://PUBLIC_KEY:PRIVATE_KEY@app.getsentry.com/1"}, % If using the private key 48 | 49 | % Set to inet6 to use IPv6. 50 | % See `ipfamily` in `httpc:set_options/1` for more information. 51 | % Default value is `inet`. 52 |    {ipfamily, inet}, 53 | 54 | % Set to true in order to install the standard error logger. 55 | % Now all events logged using `error_logger` will be sent to Sentry. 56 | {error_logger, true}, 57 | 58 | % Customize error logger: 59 | % Default value is `[]`. 60 | {error_logger_config, [ 61 | % `warning` or `error`. 62 | % If set to `error`, error logger will ignore warning messages and reports. 63 | % Default value is `warning`. 64 | {level, warning}, 65 | 66 | % Not all messages that error_logger generates are useful. 67 | % For example, supervisors will always generate an error_report when 68 | % restarting a child, even if it exits with `reason = normal`. 69 | % You can provide a module that implements `raven_error_logger_filter` behavior 70 | % to avoid spamming sentry with issues that are not errors. 71 | % See http://erlang.org/doc/apps/sasl/error_logging.html for more information. 72 | % Default value is `undefined`. 73 | {filter, callback_module} 74 | ]} 75 | ]}. 76 | ``` 77 | 78 | ## Lager Backend 79 | 80 | At the moment, the raven lager backend shares its configuration with the raven application, and does 81 | not allow per-backend configuration. 82 | 83 | ### Simple Configuration 84 | 85 | This adds the raven backend to lager. By default, it configures the raven lager backend to send most metadata that the lager parse transform creates (see Advanced Configuration). 86 | 87 | ```erlang 88 | {lager, [ 89 | {handlers, [ 90 | {raven_lager_backend, info}]}]} 91 | ``` 92 | 93 | ### Advanced Configuration 94 | 95 | This configuration uses a list `[Level :: atom(), MetadataKeys :: [atom()]]`. 96 | 97 | `MetadataKeys` is a list of atoms that correspond to the metadata to be sent by Raven, should it be included in the lager log message. 98 | 99 | The configuration shown here is equivalent to the Simple Configuration. 100 | 101 | ```erlang 102 | {lager, [ 103 | {handlers, [ 104 | {raven_lager_backend, 105 | [info, [pid, file, line, module, function, stacktrace]]}]}]} 106 | ``` 107 | 108 | To exclude all metadata except `pid`: 109 | 110 | ```erlang 111 | {lager, [ 112 | {handlers, [ 113 | {raven_lager_backend, [info, [pid]]}]}]} 114 | ``` 115 | 116 | 117 | ## Advanced Usage 118 | 119 | You can log directly events to sentry using the `raven:capture/2` function, for example: 120 | 121 | ```erlang 122 | try erlang:error(badarg) 123 | catch Class:Reason:Stacktrace -> 124 | raven:capture("Test Event", [ 125 | {exception, {Class, Reason}}, 126 | {stacktrace, Stacktrace}, 127 | {extra, [ 128 | {pid, self()}, 129 | {process_dictionary, erlang:get()} 130 | ]} 131 | ]) 132 | end. 133 | ``` 134 | -------------------------------------------------------------------------------- /include/raven.hrl: -------------------------------------------------------------------------------- 1 | -define(APP, raven_erlang). 2 | -------------------------------------------------------------------------------- /include/raven_error_logger.hrl: -------------------------------------------------------------------------------- 1 | -type report_type() :: 2 | crash_report | 3 | supervisor_report | 4 | progress | 5 | std_error | 6 | any(). % error_logger:error_report/2 can be called with any Type. 7 | -type logging_level() :: warning | error. 8 | -type reason() :: any(). 9 | -type supervisor() :: atom(). 10 | -type supervisor_report_context() :: 11 | start_error | 12 | child_terminated | 13 | shutdown_error. 14 | 15 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 noet syntax=erlang 3 | {erl_opts, [ 4 | warnings_as_errors, 5 | warn_export_all, 6 | {platform_define, "^R14", no_callbacks} 7 | ]}. 8 | {deps, [{jsone, "1.4.6"}]}. 9 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"jsone">>,{pkg,<<"jsone">>,<<"1.4.6">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"jsone">>, <<"644D6D57BEFB22C8E19B324DEE19D73B1C004565009861A8F64C68B7B9E64DBF">>}]} 6 | ]. 7 | -------------------------------------------------------------------------------- /src/raven.erl: -------------------------------------------------------------------------------- 1 | -module(raven). 2 | -include("raven.hrl"). 3 | -export([ 4 | start/0, 5 | stop/0, 6 | capture/2, 7 | user_agent/0 8 | ]). 9 | -ifdef(TEST). 10 | -export([ 11 | get_config/0 12 | ]). 13 | -endif. 14 | 15 | -define(SENTRY_VERSION, "2.0"). 16 | -define(JSONE_OPTS, [native_utf8, {object_key_type, scalar}]). 17 | 18 | -record(cfg, { 19 | uri :: string(), 20 | public_key :: string(), 21 | private_key :: string(), 22 | project :: string(), 23 | ipfamily :: atom() 24 | }). 25 | 26 | -type cfg_rec() :: #cfg{}. 27 | 28 | -spec start() -> ok | {error, term()}. 29 | start() -> 30 | application:ensure_all_started(?APP). 31 | 32 | -spec stop() -> ok | {error, term()}. 33 | stop() -> 34 | application:stop(?APP). 35 | 36 | -spec capture(string() | binary(), [parameter()]) -> ok. 37 | -type parameter() :: 38 | {stacktrace, [stackframe()]} | 39 | {exception, {exit | error | throw, term()}} | 40 | {atom(), binary() | integer()}. 41 | -type stackframe() :: 42 | {module(), atom(), non_neg_integer() | [term()]} | 43 | {module(), atom(), non_neg_integer() | [term()], [{atom(), term()}]}. 44 | capture(Message, Params) when is_list(Message) -> 45 | capture(unicode:characters_to_binary(Message), Params); 46 | capture(Message, Params0) -> 47 | Cfg = get_config(), 48 | Params1 = [{tags, get_tags()} | Params0], 49 | Params2 = maybe_append_release(Params1), 50 | Document = {[ 51 | {event_id, event_id_i()}, 52 | {project, unicode:characters_to_binary(Cfg#cfg.project)}, 53 | {platform, erlang}, 54 | {server_name, node()}, 55 | {timestamp, timestamp_i()}, 56 | {message, term_to_json_i(Message)} | 57 | lists:map(fun 58 | ({stacktrace, Value}) -> 59 | {'sentry.interfaces.Stacktrace', {[ 60 | {frames,lists:reverse([frame_to_json_i(Frame) || Frame <- Value])} 61 | ]}}; 62 | ({exception, {Type, Value}}) -> 63 | {'sentry.interfaces.Exception', {[ 64 | {type, Type}, 65 | {value, term_to_json_i(Value)} 66 | ]}}; 67 | ({tags, Tags}) -> 68 | {tags, {[{Key, term_to_json_i(Value)} || {Key, Value} <- Tags]}}; 69 | ({extra, Tags}) -> 70 | {extra, {[{Key, term_to_json_i(Value)} || {Key, Value} <- Tags]}}; 71 | ({Key, Value}) -> 72 | {Key, term_to_json_i(Value)} 73 | end, Params2) 74 | ]}, 75 | Timestamp = integer_to_list(unix_timestamp_i()), 76 | Body = base64:encode(zlib:compress(jsone:encode(Document, ?JSONE_OPTS))), 77 | UA = user_agent(), 78 | Headers = [ 79 | {"X-Sentry-Auth", 80 | ["Sentry sentry_version=", ?SENTRY_VERSION, 81 | ",sentry_client=", UA, 82 | ",sentry_timestamp=", Timestamp, 83 | ",sentry_key=", Cfg#cfg.public_key]}, 84 | {"User-Agent", UA} 85 | ], 86 | ok = httpc:set_options([{ipfamily, Cfg#cfg.ipfamily}]), 87 | httpc:request(post, 88 | {Cfg#cfg.uri ++ "/api/store/", Headers, "application/octet-stream", Body}, 89 | [], 90 | [{body_format, binary}, {sync, false}, {receiver, fun(_) -> ok end}] 91 | ), 92 | ok. 93 | 94 | -spec user_agent() -> iolist(). 95 | user_agent() -> 96 | {ok, Vsn} = application:get_key(?APP, vsn), 97 | ["raven-erlang/", Vsn]. 98 | 99 | %% @private 100 | -spec get_config() -> cfg_rec(). 101 | get_config() -> 102 | get_config(?APP). 103 | 104 | -spec get_config(App :: atom()) -> cfg_rec(). 105 | get_config(App) -> 106 | IpFamily = application:get_env(App, ipfamily, inet), 107 | case application:get_env(App, dsn) of 108 | {ok, Dsn} -> 109 | {match, [_, Protocol, Keys, Uri, Project]} = 110 | re:run(Dsn, "^(https?://)(.+)@(.+)/(.+)$", [{capture, all, list}]), 111 | [PublicKey | MaybePrivateKey] = string:split(Keys, ":"), 112 | PrivateKey = 113 | case MaybePrivateKey of 114 | [] -> ""; 115 | [Key] -> Key 116 | end, 117 | #cfg{uri = Protocol ++ Uri, 118 | public_key = PublicKey, 119 | private_key = PrivateKey, 120 | project = Project, 121 | ipfamily = IpFamily}; 122 | undefined -> 123 | {ok, Uri} = application:get_env(App, uri), 124 | {ok, PublicKey} = application:get_env(App, public_key), 125 | {ok, PrivateKey} = application:get_env(App, private_key), 126 | {ok, Project} = application:get_env(App, project), 127 | #cfg{uri = Uri, 128 | public_key = PublicKey, 129 | private_key = PrivateKey, 130 | project = Project, 131 | ipfamily = IpFamily} 132 | end. 133 | 134 | get_tags() -> 135 | application:get_env(?APP, tags, []). 136 | 137 | maybe_append_release(Params) -> 138 | case application:get_env(?APP, release) of 139 | {ok, Release} -> [{release, Release} | Params]; 140 | undefined -> Params 141 | end. 142 | 143 | event_id_i() -> 144 | <> = crypto:strong_rand_bytes(16), 145 | <> = <>, 146 | iolist_to_binary(io_lib:format("~32.16.0b", [UUID])). 147 | 148 | timestamp_i() -> 149 | {{Y,Mo,D}, {H,Mn,S}} = calendar:now_to_datetime(os:timestamp()), 150 | FmtStr = "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B", 151 | iolist_to_binary(io_lib:format(FmtStr, [Y, Mo, D, H, Mn, S])). 152 | 153 | unix_timestamp_i() -> 154 | {Mega, Sec, Micro} = os:timestamp(), 155 | Mega * 1000000 * 1000000 + Sec * 1000000 + Micro. 156 | 157 | frame_to_json_i({Module, Function, Arguments}) -> 158 | frame_to_json_i({Module, Function, Arguments, []}); 159 | frame_to_json_i({Module, Function, Arguments, Location}) -> 160 | Arity = case is_list(Arguments) of 161 | true -> length(Arguments); 162 | false -> Arguments 163 | end, 164 | Line = case lists:keyfind(line, 1, Location) of 165 | false -> -1; 166 | {line, L} -> L 167 | end, 168 | { 169 | case is_list(Arguments) of 170 | true -> [{vars, [iolist_to_binary(io_lib:format("~w", [Argument])) || Argument <- Arguments]}]; 171 | false -> [] 172 | end ++ [ 173 | {module, Module}, 174 | {function, <<(atom_to_binary(Function, utf8))/binary, "/", (list_to_binary(integer_to_list(Arity)))/binary>>}, 175 | {lineno, Line}, 176 | {filename, case lists:keyfind(file, 1, Location) of 177 | false -> <<(atom_to_binary(Module, utf8))/binary, ".erl">>; 178 | {file, File} -> list_to_binary(File) 179 | end} 180 | ] 181 | }. 182 | 183 | term_to_json_i(Term) when is_binary(Term); is_atom(Term) -> 184 | Term; 185 | term_to_json_i(Term) -> 186 | iolist_to_binary(io_lib:format("~120p", [Term])). 187 | -------------------------------------------------------------------------------- /src/raven_app.erl: -------------------------------------------------------------------------------- 1 | -module(raven_app). 2 | -include("raven.hrl"). 3 | -behaviour(application). 4 | -export([ 5 | start/2, 6 | stop/1 7 | ]). 8 | 9 | %% @hidden 10 | start(_StartType, _StartArgs) -> 11 | case application:get_env(?APP, error_logger) of 12 | {ok, true} -> 13 | error_logger:add_report_handler(raven_error_logger); 14 | _ -> 15 | ok 16 | end, 17 | raven_sup:start_link(). 18 | 19 | %% @hidden 20 | stop(_State) -> 21 | case application:get_env(?APP, error_logger) of 22 | {ok, true} -> 23 | error_logger:delete_report_handler(raven_error_logger), 24 | ok; 25 | _ -> 26 | ok 27 | end. 28 | -------------------------------------------------------------------------------- /src/raven_erlang.app.src: -------------------------------------------------------------------------------- 1 | {application, raven_erlang, 2 | [ {description, "Erlang client for Sentry"} 3 | , {vsn, "0.4.3"} 4 | , {registered, []} 5 | , {applications, [kernel, stdlib, crypto, public_key, ssl, inets, jsone]} 6 | , {mod, {raven_app, []}} 7 | , {env, 8 | [ {error_logger, false} 9 | , {ipfamily, inet} 10 | ]} 11 | 12 | , {pkg_name, raven_erlang} 13 | , {licenses, ["MIT"]} 14 | , {maintainers, ["Ali Sabil", "Yuri Artemev"]} 15 | , {links, [{"Github", "https://github.com/artemeff/raven-erlang"}]} 16 | ]}. 17 | -------------------------------------------------------------------------------- /src/raven_error_logger.erl: -------------------------------------------------------------------------------- 1 | -module(raven_error_logger). 2 | -include("raven.hrl"). 3 | -include("raven_error_logger.hrl"). 4 | -behaviour(gen_event). 5 | -export([ 6 | init/1, 7 | code_change/3, 8 | terminate/2, 9 | handle_call/2, 10 | handle_event/2, 11 | handle_info/2 12 | ]). 13 | 14 | -record(config, { 15 | logging_level = warning :: logging_level(), 16 | filter = undefined :: module() 17 | }). 18 | 19 | init(_) -> 20 | {ok, []}. 21 | 22 | handle_call(_, State) -> 23 | {ok, ok, State}. 24 | 25 | handle_event({error, _, {Pid, Format, Data}}, State) -> 26 | {Message, Details} = parse_message(error, Pid, Format, Data), 27 | raven:capture(Message, Details), 28 | {ok, State}; 29 | handle_event({error_report, _, {Pid, Type, Report}}, State) -> 30 | case should_send_report(Type, Report) of 31 | true -> 32 | {Message, Details} = parse_report(error, Pid, Type, Report), 33 | raven:capture(Message, Details), 34 | {ok, State}; 35 | false -> 36 | {ok, State} 37 | end; 38 | 39 | handle_event({warning_msg, _, {Pid, Format, Data}}, State) -> 40 | case get_config() of 41 | #config{logging_level = warning} -> 42 | {Message, Details} = parse_message(warning, Pid, Format, Data), 43 | raven:capture(Message, Details), 44 | {ok, State}; 45 | _ -> 46 | {ok, State} 47 | end; 48 | handle_event({warning_report, _, {Pid, Type, Report}}, State) -> 49 | case {get_config(), should_send_report(Type, Report)} of 50 | {#config{logging_level = warning}, true}-> 51 | {Message, Details} = parse_report(warning, Pid, Type, Report), 52 | raven:capture(Message, Details), 53 | {ok, State}; 54 | _ -> 55 | {ok, State} 56 | end; 57 | 58 | handle_event(_, State) -> 59 | {ok, State}. 60 | 61 | handle_info(_, State) -> 62 | {ok, State}. 63 | 64 | code_change(_, State, _) -> 65 | {ok, State}. 66 | 67 | terminate(_, _) -> 68 | ok. 69 | 70 | 71 | %% @private 72 | -spec get_config() -> #config{}. 73 | get_config() -> 74 | Default = #config{}, 75 | case application:get_env(?APP, error_logger_config) of 76 | {ok, Options} when is_list(Options) -> 77 | #config{ 78 | logging_level = 79 | proplists:get_value(level, 80 | Options, 81 | Default#config.logging_level), 82 | filter = 83 | proplists:get_value(filter, 84 | Options, 85 | Default#config.filter) 86 | }; 87 | undefined -> 88 | Default 89 | end. 90 | 91 | 92 | %% @private 93 | parse_message(error = Level, Pid, "** Generic server " ++ _, [Name, LastMessage, State, Reason]) -> 94 | %% gen_server terminate 95 | {Exception, Stacktrace} = parse_reason(Reason), 96 | {format_exit(gen_server, Name, Reason), [ 97 | {level, Level}, 98 | {exception, Exception}, 99 | {stacktrace, Stacktrace}, 100 | {extra, [ 101 | {name, Name}, 102 | {pid, Pid}, 103 | {last_message, LastMessage}, 104 | {state, State}, 105 | {reason, Reason} 106 | ]} 107 | ]}; 108 | parse_message(error = Level, Pid, "** State machine " ++ _, [Name, LastMessage, StateName, State, Reason]) -> 109 | %% gen_fsm terminate 110 | {Exception, Stacktrace} = parse_reason(Reason), 111 | {format_exit(gen_fsm, Name, Reason), [ 112 | {level, Level}, 113 | {exception, Exception}, 114 | {stacktrace, Stacktrace}, 115 | {extra, [ 116 | {name, Name}, 117 | {pid, Pid}, 118 | {last_message, LastMessage}, 119 | {state, State}, 120 | {state_name, StateName}, 121 | {reason, Reason} 122 | ]} 123 | ]}; 124 | parse_message(error = Level, Pid, "** gen_event handler " ++ _, [ID, Name, LastMessage, State, Reason]) -> 125 | %% gen_event terminate 126 | {Exception, Stacktrace} = parse_reason(Reason), 127 | {format_exit(gen_event, Name, Reason), [ 128 | {level, Level}, 129 | {exception, Exception}, 130 | {stacktrace, Stacktrace}, 131 | {extra, [ 132 | {id, ID}, 133 | {name, Name}, 134 | {pid, Pid}, 135 | {last_message, LastMessage}, 136 | {state, State}, 137 | {reason, Reason} 138 | ]} 139 | ]}; 140 | parse_message(error = Level, Pid, "** Generic process " ++ _, [Name, LastMessage, State, Reason]) -> 141 | %% gen_process terminate 142 | {Exception, Stacktrace} = parse_reason(Reason), 143 | {format_exit(gen_process, Name, Reason), [ 144 | {level, Level}, 145 | {exception, Exception}, 146 | {stacktrace, Stacktrace}, 147 | {extra, [ 148 | {name, Name}, 149 | {pid, Pid}, 150 | {last_message, LastMessage}, 151 | {state, State}, 152 | {reason, Reason} 153 | ]} 154 | ]}; 155 | parse_message(error = Level, Pid, "Error in process " ++ _, [Name, Node, Reason]) -> 156 | %% process terminate 157 | {Exception, Stacktrace} = parse_reason(Reason), 158 | {format_exit(process, Name, Reason), [ 159 | {level, Level}, 160 | {exception, Exception}, 161 | {stacktrace, Stacktrace}, 162 | {extra, [ 163 | {name, Name}, 164 | {pid, Pid}, 165 | {node, Node}, 166 | {reason, Reason} 167 | ]} 168 | ]}; 169 | parse_message(error = Level, Pid, "Ranch listener " ++ _, [Name, Protocol, RefPid, Reason]) -> 170 | %% ranch connection terminated 171 | {Exception, Stacktrace} = parse_reason(Reason), 172 | {format_exit(Protocol, Name, Reason), [ 173 | {level, Level}, 174 | {exception, Exception}, 175 | {stacktrace, Stacktrace}, 176 | {extra, [ 177 | {name, Name}, 178 | {pid, Pid}, 179 | {ref_pid, RefPid}, 180 | {protocol, Protocol}, 181 | {reason, Reason} 182 | ]} 183 | ]}; 184 | parse_message(error = Level, Pid, "** Task " ++ _, [TaskPid, RefPid, Fun, FunArgs, Reason]) -> 185 | {Exception, Stacktrace} = parse_reason(Reason), 186 | {format_exit("Task", TaskPid, Reason), [ 187 | {level, Level}, 188 | {exception, Exception}, 189 | {stacktrace, Stacktrace}, 190 | {extra, [ 191 | {pid, Pid}, 192 | {parent_pid, RefPid}, 193 | {function, Fun}, 194 | {function_args, FunArgs} 195 | ]} 196 | ]}; 197 | parse_message(Level, Pid, Format, Data) -> 198 | {format(Format, Data), [ 199 | {level, Level}, 200 | {extra, [ 201 | {pid, Pid}, 202 | {data, Data} 203 | ]} 204 | ]}. 205 | 206 | 207 | %% @private 208 | -spec should_send_report(report_type(), error_logger:report()) -> boolean(). 209 | should_send_report(supervisor_report, Report) -> 210 | #config{filter = Mod} = get_config(), 211 | case erlang:function_exported(Mod, should_send_supervisor_report, 3) of 212 | true -> 213 | Mod:should_send_supervisor_report( 214 | proplists:get_value(supervisor, Report), 215 | proplists:get_value(reason, Report), 216 | proplists:get_value(errorContext, Report) 217 | ); 218 | false -> 219 | true 220 | end; 221 | should_send_report(_, _) -> true. 222 | 223 | 224 | %% @private 225 | parse_report(Level, Pid, Type, Report) -> 226 | case {Level, Type} of 227 | {_, crash_report} when is_list(Report) -> 228 | parse_crash_report( 229 | Level, 230 | Pid, 231 | Report 232 | ); 233 | {_, supervisor_report} when is_list(Report) -> 234 | parse_supervisor_report( 235 | Level, 236 | Pid, 237 | proplists:get_value(errorContext, Report), 238 | proplists:get_value(offender, Report), 239 | proplists:get_value(reason, Report), 240 | proplists:get_value(supervisor, Report) 241 | ); 242 | {info, progress} when is_list(Report) -> 243 | parse_progress_report( 244 | Pid, 245 | proplists:get_value(started, Report), 246 | proplists:get_value(supervisor, Report) 247 | ); 248 | {error, std_error} when is_list(Report) -> 249 | parse_std_error_report(Pid, Report); 250 | _ -> 251 | parse_unknown_report(Level, Pid, Type, Report) 252 | end. 253 | 254 | 255 | %% @private 256 | parse_crash_report(Level, Pid, [Report, Neighbors]) -> 257 | Name = case proplists:get_value(registered_name, Report, []) of 258 | [] -> proplists:get_value(pid, Report); 259 | N -> N 260 | end, 261 | case Name of 262 | undefined -> 263 | {<<"Process crashed">>, [ 264 | {level, Level}, 265 | {extra, [ 266 | {pid, Pid}, 267 | {neighbors, Neighbors} | 268 | Report 269 | ]} 270 | ]}; 271 | _ -> 272 | {Class, R, Trace} = proplists:get_value(error_info, Report, {error, unknown, []}), 273 | Reason = {{Class, R}, Trace}, 274 | {Exception, Stacktrace} = parse_reason(Reason), 275 | {format_exit("Process", Name, Reason), [ 276 | {level, Level}, 277 | {exception, Exception}, 278 | {stacktrace, Stacktrace}, 279 | {extra, [ 280 | {name, Name}, 281 | {pid, Pid}, 282 | {reason, Reason} | 283 | Report 284 | ]} 285 | ]} 286 | end. 287 | 288 | 289 | %% @private 290 | parse_supervisor_report(Level, Pid, Context, Offender, Reason, Supervisor) -> 291 | {Exception, Stacktrace} = parse_reason(Reason), 292 | {format("Supervisor ~s had child exit with reason ~s", [format_name(Supervisor), format_reason(Reason)]), [ 293 | {level, Level}, 294 | {logger, supervisors}, 295 | {exception, Exception}, 296 | {stacktrace, Stacktrace}, 297 | {extra, [ 298 | {supervisor, Supervisor}, 299 | {context, Context}, 300 | {pid, Pid}, 301 | {child_pid, proplists:get_value(pid, Offender)}, 302 | {mfa, format_mfa(proplists:get_value(mfargs, Offender))}, 303 | {restart_type, proplists:get_value(restart_type, Offender)}, 304 | {child_type, proplists:get_value(child_type, Offender)}, 305 | {shutdown, proplists:get_value(shutdown, Offender)} 306 | ]} 307 | ]}. 308 | 309 | 310 | %% @private 311 | parse_progress_report(Pid, Started, Supervisor) -> 312 | Message = case proplists:get_value(name, Started, []) of 313 | [] -> format("Supervisor ~s started child", [format_name(Supervisor)]); 314 | Name -> format("Supervisor ~s started ~s", [format_name(Supervisor), format_name(Name)]) 315 | end, 316 | {Message, [ 317 | {level, info}, 318 | {logger, supervisors}, 319 | {extra, [ 320 | {supervisor, Supervisor}, 321 | {pid, Pid}, 322 | {child_pid, proplists:get_value(pid, Started)}, 323 | {mfa, format_mfa(proplists:get_value(mfargs, Started))}, 324 | {restart_type, proplists:get_value(restart_type, Started)}, 325 | {child_type, proplists:get_value(child_type, Started)}, 326 | {shutdown, proplists:get_value(shutdown, Started)} 327 | ]} 328 | ]}. 329 | 330 | 331 | %% @private 332 | parse_std_error_report(Pid, Report) -> 333 | Message = case proplists:get_value(message, Report) of 334 | undefined -> format_string(Report); 335 | M -> format_string(M) 336 | end, 337 | {Toplevel, Extra} = lists:partition(fun 338 | ({exception, _}) -> true; 339 | ({stacktrace, _}) -> true; 340 | (_) -> false 341 | end, Report), 342 | {Message, [ 343 | {level, error}, 344 | {extra, [ 345 | {type, std_error}, 346 | {pid, Pid} | 347 | lists:keydelete(message, 1, Extra) 348 | ]} | 349 | Toplevel 350 | ]}. 351 | 352 | 353 | %% @private 354 | parse_unknown_report(Level, Pid, Type, Report) -> 355 | Message = format_string(Report), 356 | {Message, [ 357 | {level, Level}, 358 | {extra, [ 359 | {type, Type}, 360 | {pid, Pid} 361 | ]} 362 | ]}. 363 | 364 | 365 | %% @private 366 | parse_reason({'function not exported', Stacktrace}) -> 367 | {{exit, undef}, parse_stacktrace(Stacktrace)}; 368 | parse_reason({bad_return, {_MFA, {'EXIT', Reason}}}) -> 369 | parse_reason(Reason); 370 | parse_reason({bad_return, {MFA, Value}}) -> 371 | {{exit, {bad_return, Value}}, parse_stacktrace(MFA)}; 372 | parse_reason({bad_return_value, Value}) -> 373 | {{exit, {bad_return, Value}}, []}; 374 | parse_reason({{bad_return_value, Value}, MFA}) -> 375 | {{exit, {bad_return, Value}}, parse_stacktrace(MFA)}; 376 | parse_reason({badarg, Stacktrace}) -> 377 | {{error, badarg}, parse_stacktrace(Stacktrace)}; 378 | parse_reason({{badmatch, Value}, Stacktrace}) -> 379 | {{exit, {badmatch, Value}}, parse_stacktrace(Stacktrace)}; 380 | parse_reason({'EXIT', Reason}) -> 381 | parse_reason(Reason); 382 | parse_reason({Reason, Child}) when is_tuple(Child) andalso element(1, Child) =:= child -> 383 | parse_reason(Reason); 384 | parse_reason({{Class, Reason}, Stacktrace}) when Class =:= exit; Class =:= error; Class =:= throw -> 385 | {{Class, Reason}, parse_stacktrace(Stacktrace)}; 386 | parse_reason({Reason, Stacktrace}) -> 387 | case is_elixir() of 388 | false -> 389 | {{exit, Reason}, parse_stacktrace(Stacktrace)}; 390 | _ -> 391 | parse_reason_ex(Reason) 392 | end; 393 | parse_reason(Reason) -> 394 | {{exit, Reason}, []}. 395 | 396 | %% @private 397 | parse_stacktrace({_, _, _} = MFA) -> [MFA]; 398 | parse_stacktrace({_, _, _, _} = MFA) -> [MFA]; 399 | parse_stacktrace([{_, _, _} | _] = Trace) -> Trace; 400 | parse_stacktrace([{_, _, _, _} | _] = Trace) -> Trace; 401 | parse_stacktrace(_) -> []. 402 | 403 | %% @private 404 | parse_reason_ex({Reason, Stacktrace}) -> 405 | case 'Elixir.Exception':'exception?'(Reason) of 406 | false -> 407 | {{exit, Reason}, parse_stacktrace(Stacktrace)}; 408 | true -> 409 | {{exit, format_message_ex(Reason)}, parse_stacktrace(Stacktrace)} 410 | end. 411 | 412 | %% @private 413 | format_message_ex(Reason) -> 'Elixir.Exception':message(Reason). 414 | 415 | %% @private 416 | %format_stacktrace_ex(Stacktrace) -> 'Elixir.Exception':format_stacktrace(Stacktrace). 417 | 418 | %% @private 419 | format_exit(Tag, Name, Reason) when is_pid(Name) -> 420 | format("~s terminated with reason: ~s", [Tag, format_reason(Reason)]); 421 | format_exit(Tag, Name, Reason) -> 422 | format("~s ~s terminated with reason: ~s", [Tag, format_name(Name), format_reason(Reason)]). 423 | 424 | %% @private 425 | format_name({local, Name}) -> Name; 426 | format_name({global, Name}) -> format_string(Name); 427 | format_name({via, _, Name}) -> format_string(Name); 428 | format_name(Name) -> format_string(Name). 429 | 430 | %% @private 431 | format_reason({'function not exported', Trace}) -> 432 | ["call to undefined function ", format_mfa(Trace)]; 433 | format_reason({undef, Trace}) -> 434 | ["call to undefined function ", format_mfa(Trace)]; 435 | format_reason({bad_return, {_MFA, {'EXIT', Reason}}}) -> 436 | format_reason(Reason); 437 | format_reason({bad_return, {Trace, Val}}) -> 438 | ["bad return value ", format_term(Val), " from ", format_mfa(Trace)]; 439 | format_reason({bad_return_value, Val}) -> 440 | ["bad return value ", format_term(Val)]; 441 | format_reason({{bad_return_value, Val}, Trace}) -> 442 | ["bad return value ", format_term(Val), " in ", format_mfa(Trace)]; 443 | format_reason({{badrecord, Record}, Trace}) -> 444 | ["bad record ", format_term(Record), " in ", format_mfa(Trace)]; 445 | format_reason({{case_clause, Value}, Trace}) -> 446 | ["no case clause matching ", format_term(Value), " in ", format_mfa(Trace)]; 447 | format_reason({function_clause, Trace}) -> 448 | ["no function clause matching ", format_mfa(Trace)]; 449 | format_reason({if_clause, Trace}) -> 450 | ["no true branch found while evaluating if expression in ", format_mfa(Trace)]; 451 | format_reason({{try_clause, Value}, Trace}) -> 452 | ["no try clause matching ", format_term(Value), " in ", format_mfa(Trace)]; 453 | format_reason({badarith, Trace}) -> 454 | ["bad arithmetic expression in ", format_mfa(Trace)]; 455 | format_reason({{badmatch, Value}, Trace}) -> 456 | ["no match of right hand value ", format_term(Value), " in ", format_mfa(Trace)]; 457 | format_reason({emfile, _Trace}) -> 458 | "maximum number of file descriptors exhausted, check ulimit -n"; 459 | format_reason({system_limit, [{M, F, _}|_] = Trace}) -> 460 | Limit = case {M, F} of 461 | {erlang, open_port} -> 462 | "maximum number of ports exceeded"; 463 | {erlang, spawn} -> 464 | "maximum number of processes exceeded"; 465 | {erlang, spawn_opt} -> 466 | "maximum number of processes exceeded"; 467 | {erlang, list_to_atom} -> 468 | "tried to create an atom larger than 255, or maximum atom count exceeded"; 469 | {ets, new} -> 470 | "maximum number of ETS tables exceeded"; 471 | _ -> 472 | format_mfa(Trace) 473 | end, 474 | ["system limit: ", Limit]; 475 | format_reason({badarg, Trace}) -> 476 | ["bad argument in ", format_mfa(Trace)]; 477 | format_reason({{badarity, {Fun, Args}}, Trace}) -> 478 | {arity, Arity} = lists:keyfind(arity, 1, erlang:fun_info(Fun)), 479 | [io_lib:format("fun called with wrong arity of ~w instead of ~w in ", [length(Args), Arity]), format_mfa(Trace)]; 480 | format_reason({noproc, Trace}) -> 481 | ["no such process or port in call to ", format_mfa(Trace)]; 482 | format_reason({{badfun, Term}, Trace}) -> 483 | ["bad function ", format_term(Term), " in ", format_mfa(Trace)]; 484 | format_reason({#{'__exception__' := true} = Reason, Stacktrace}) -> 485 | [format_message_ex(Reason), 'Elixir.Exception':format_stacktrace(Stacktrace)]; 486 | format_reason({Reason, {M, F, A} = Trace}) when is_atom(M), is_atom(F), is_list(A) -> 487 | [format_reason(Reason), " in ", format_mfa(Trace)]; 488 | format_reason({Reason, [{M, F, A}|_] = Trace}) when is_atom(M), is_atom(F), is_integer(A) -> 489 | [format_reason(Reason), " in ", format_mfa(Trace)]; 490 | format_reason({Reason, [{M, F, A, Props}|_] = Trace}) when is_atom(M), is_atom(F), is_integer(A), is_list(Props) -> 491 | [format_reason(Reason), " in ", format_mfa(Trace)]; 492 | format_reason(Reason) -> 493 | format_term(Reason). 494 | 495 | %% @private 496 | format_mfa([{_, _, _} = MFA | _]) -> 497 | format_mfa(MFA); 498 | format_mfa([{_, _, _, _} = MFA | _]) -> 499 | format_mfa(MFA); 500 | format_mfa({M, F, A, _}) -> 501 | format_mfa({M, F, A}); 502 | format_mfa({M, F, A}) -> 503 | format_mfa_platform({M, F, A}); 504 | format_mfa(Term) -> 505 | format_term(Term). 506 | 507 | %% @private 508 | format_mfa_platform({M, F, A} = MFA) -> 509 | case is_elixir() of 510 | false -> format_mfa_erlang(MFA); 511 | _ -> 'Elixir.Exception':format_mfa(M, F, A) 512 | end. 513 | 514 | %% @private 515 | format_mfa_erlang({M, F, A}) when is_list(A) -> 516 | {Format, Args} = format_args(A, [], []), 517 | format("~w:~w(" ++ Format ++ ")", [M, F | Args]); 518 | format_mfa_erlang({M, F, A}) when is_integer(A) -> 519 | format("~w:~w/~w", [M, F, A]). 520 | 521 | %% @private 522 | format_args([], FormatAcc, ArgsAcc) -> 523 | {string:join(lists:reverse(FormatAcc), ", "), lists:reverse(ArgsAcc)}; 524 | format_args([Arg | Rest], FormatAcc, ArgsAcc) -> 525 | format_args(Rest, ["~s" | FormatAcc], [format_term(Arg) | ArgsAcc]). 526 | 527 | %% @private 528 | format_string(Term) when is_atom(Term); is_binary(Term) -> 529 | format("~s", [Term]); 530 | format_string(Term) -> 531 | try format("~s", [Term]) of 532 | Result -> Result 533 | catch 534 | error:badarg -> format_term(Term) 535 | end. 536 | 537 | %% @private 538 | format_term(Term) -> 539 | format("~120p", [Term]). 540 | 541 | %% @private 542 | format(Format, Data) -> 543 | iolist_to_binary(io_lib:format(Format, Data)). 544 | 545 | %% @private 546 | is_elixir() -> 547 | case code:is_loaded('Elixir.Exception') of 548 | false -> false; 549 | _ -> true 550 | end. 551 | -------------------------------------------------------------------------------- /src/raven_error_logger_filter.erl: -------------------------------------------------------------------------------- 1 | -module(raven_error_logger_filter). 2 | -include("raven_error_logger.hrl"). 3 | 4 | -optional_callbacks([should_send_supervisor_report/3]). 5 | 6 | %% @doc OTP supervisors will generate an `error_report` every time 7 | %% a process is restarted, even if the shutdown is normal. 8 | %% To avoid spamming Sentry with issues that are not errors, 9 | %% one can implement this callback to filter supervisor reports, 10 | %% received from error logger. 11 | -callback should_send_supervisor_report(supervisor(), 12 | reason(), 13 | supervisor_report_context()) -> boolean(). 14 | -------------------------------------------------------------------------------- /src/raven_lager_backend.erl: -------------------------------------------------------------------------------- 1 | %% @doc raven backend for lager 2 | 3 | -module(raven_lager_backend). 4 | -behaviour(gen_event). 5 | 6 | 7 | -export([ 8 | init/1, 9 | code_change/3, 10 | terminate/2, 11 | handle_call/2, 12 | handle_event/2, 13 | handle_info/2 14 | ]). 15 | 16 | 17 | -define(DEFAULT_METADATA_KEYS, [pid, file, line, module, function, stacktrace]). 18 | 19 | -record(state, {level, metadata_keys=[]}). 20 | 21 | init([Level]) when is_atom(Level) -> 22 | init(Level); 23 | init([Level, Metadata]) when is_atom(Level), is_list(Metadata) -> 24 | case check_metadata(Metadata) of 25 | ok -> 26 | {ok, #state{ 27 | level=lager_util:config_to_mask(Level), 28 | metadata_keys=Metadata 29 | }}; 30 | {error, Reason} -> 31 | {error, {fatal, {bad_metadata, Reason}}} 32 | end; 33 | init(Level) when is_atom(Level) -> 34 | init([Level, ?DEFAULT_METADATA_KEYS]); 35 | init(_) -> 36 | {error, {fatal, invalid_configuration}}. 37 | 38 | %% @private 39 | handle_call(get_loglevel, #state{level=Level} = State) -> 40 | {ok, Level, State}; 41 | handle_call({set_loglevel, Level}, State) -> 42 | try lager_util:config_to_mask(Level) of 43 | Levels -> 44 | {ok, ok, State#state{level=Levels}} 45 | catch 46 | _:_ -> 47 | {ok, {error, bad_log_level}, State} 48 | end; 49 | handle_call(_, State) -> 50 | {ok, ok, State}. 51 | 52 | %% @private 53 | handle_event({log, Data}, 54 | #state{level=L, metadata_keys=MetadataKeys} = State) -> 55 | case lager_util:is_loggable(Data, L, ?MODULE) of 56 | true -> 57 | {Message, Params} = parse_message(Data, MetadataKeys), 58 | raven:capture(Message, Params), 59 | {ok, State}; 60 | false -> 61 | {ok, State} 62 | end; 63 | handle_event(_Event, State) -> 64 | {ok, State}. 65 | 66 | 67 | handle_info(_, State) -> 68 | {ok, State}. 69 | 70 | code_change(_, State, _) -> 71 | {ok, State}. 72 | 73 | terminate(_, _) -> 74 | ok. 75 | 76 | %% @private 77 | check_metadata(Metadata) -> 78 | case lists:all(fun(X) -> is_atom(X) end, Metadata) of 79 | true -> 80 | ok; 81 | false -> 82 | {error, metadata_must_be_atom} 83 | end. 84 | 85 | parse_message(LagerMsg, MetadataKeys) when is_tuple(LagerMsg) andalso 86 | element(1, LagerMsg) =:= lager_msg -> 87 | Extra = parse_meta(lager_msg:metadata(LagerMsg), MetadataKeys), 88 | {lager_msg:message(LagerMsg), 89 | [{level, lager_msg:severity(LagerMsg)}, 90 | {extra, Extra}]}. 91 | 92 | 93 | %% @doc Select metadata as defined in config. 94 | %% 95 | %% == Example === 96 | %% 97 | %% ``` 98 | %% {raven_lager_backend, {warning, [pid, stacktrace, module, line, hostname]} 99 | %% ''' 100 | parse_meta(MetaData, MetadataKeys) -> 101 | [Prop || {K, _}=Prop <- MetaData, lists:member(K, MetadataKeys)]. 102 | 103 | -------------------------------------------------------------------------------- /src/raven_sup.erl: -------------------------------------------------------------------------------- 1 | -module(raven_sup). 2 | -behaviour(supervisor). 3 | -export([ 4 | start_link/0, 5 | init/1 6 | ]). 7 | 8 | start_link() -> 9 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 10 | 11 | %% @hidden 12 | init([]) -> 13 | {ok, {{one_for_one, 5, 10}, []}}. 14 | -------------------------------------------------------------------------------- /test/conf/deprecated_dsn.config: -------------------------------------------------------------------------------- 1 | [ 2 | {raven_erlang, [ 3 | {dsn, "https://PUBLIC_KEY:PRIVATE_KEY@app.getsentry.com/1"}, 4 | {error_logger, true}, % Set to true in order to install the standard error logger 5 | {ipfamily, inet6} 6 | ]} 7 | ]. 8 | -------------------------------------------------------------------------------- /test/conf/sample.config: -------------------------------------------------------------------------------- 1 | [ 2 | {raven_erlang, [ 3 | {dsn, "https://PUBLIC_KEY@app.getsentry.com/1"}, 4 | {error_logger, true}, % Set to true in order to install the standard error logger 5 | {ipfamily, inet6} 6 | ]} 7 | ]. 8 | -------------------------------------------------------------------------------- /test/raven_app_test.erl: -------------------------------------------------------------------------------- 1 | %% @doc Tests for raven_app. 2 | 3 | -module(raven_app_test). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | 6 | %%%%%%%%%%%%%%%%%%%%%%%%%% 7 | %%% TESTS DESCRIPTIONS %%% 8 | %%%%%%%%%%%%%%%%%%%%%%%%%% 9 | 10 | load_configuration_test_() -> 11 | [ 12 | {"Loads configuration from application env", 13 | fun() -> 14 | {StartAppStatus, _} = application:ensure_all_started(raven_erlang), 15 | ?assertEqual(ok, StartAppStatus), 16 | Config = raven:get_config(), 17 | {cfg,"https://app.getsentry.com","PUBLIC_KEY",PrivateKey,"1",inet6} = Config, 18 | ?assert(PrivateKey == "PRIVATE_KEY" orelse PrivateKey == ""), 19 | ok = application:stop(raven_erlang) 20 | end}, 21 | {"Loads a default value (inet) for ipfamily if not specified", 22 | fun() -> 23 | ok = application:unset_env(raven_erlang, ipfamily), 24 | {StartAppStatus, _} = application:ensure_all_started(raven_erlang), 25 | ?assertEqual(ok, StartAppStatus), 26 | Config = raven:get_config(), 27 | {cfg,"https://app.getsentry.com","PUBLIC_KEY",PrivateKey,"1",inet} = Config, 28 | ?assert(PrivateKey == "PRIVATE_KEY" orelse PrivateKey == ""), 29 | ok = application:stop(raven_erlang) 30 | end} 31 | ]. 32 | 33 | capture_event_test_() -> 34 | [ 35 | {"We can start the app and capture a simple event", 36 | fun() -> 37 | {StartAppStatus, _} = application:ensure_all_started(raven_erlang), 38 | ?assertEqual(ok, StartAppStatus), 39 | ok = raven:capture("Test event", []), 40 | ok = application:stop(raven_erlang) 41 | end} 42 | ]. 43 | --------------------------------------------------------------------------------