Current section
Files
Jump to
Current section
Files
src/logger_webhook_h.erl
%%%-------------------------------------------------------------------
%% @doc Webhook logger
%%
%% This logging handler sends reports to a webhook with a provided URL
%%
%% It uses multipart/form-data, so make sure the receiver supports this.
%% It's been tested to work fine with Discord.
%%
%% It requires certifi: https://hex.pm/packages/certifi
%%
%% Shell example:
%% ```
%% logger:add_handler(error_webhook, logger_webhook_h, #{
%% config => #{ url => "https://discord.com/api/webhooks/876023916370468945/Q26jWxqZRTNWiP9qejPG6-P-z_UG3IVM_7nVt5nWSoir8bEsIV9iWXgIkNcLiBohTFSW" },
%% level => warning,
%% formatter => {logger_formatter, #{ single_line => false }}
%% }).
%% '''
%% sys.config example:
%% ```
%% [
%% {kernel, [
%% {logger, [
%% {handler, error_webhook, logger_webhook_h,
%% #{ config => #{ url => "https://discord.com/api/webhooks/12345/fanchyschmancyid" },
%% level => warning,
%% formatter => {logger_formatter, #{ single_line => false }}}}
%% ]}
%% ]}
%% ].
%% '''
%%
%% @author nixx quality <nixx@is-fantabulo.us>
%% @copyright Public Domain
%% @end
%%%-------------------------------------------------------------------
-module(logger_webhook_h).
-export([
adding_handler/1,
changing_config/3,
log/2
]).
verify_config(Config) ->
try
DefinedUrl = maps:get(url, Config),
true = is_list(DefinedUrl),
Method = maps:get(method, Config, post),
true = is_atom(Method),
true = Method /= get
of
_ -> true
catch
_:_ -> false
end.
adding_handler(HandlerConfig) ->
case verify_config(maps:get(config, HandlerConfig, #{})) of
true ->
{ok, HandlerConfig};
false ->
{error, bad_config}
end.
changing_config(set, _OldConfig, NewConfig) ->
case verify_config(maps:get(config, NewConfig, #{})) of
true -> {ok, NewConfig};
false -> {error, bad_config}
end;
changing_config(update, OldConfig, NewConfigParts) ->
NewConfig = maps:merge(OldConfig, NewConfigParts),
case verify_config(maps:get(config, NewConfig, #{})) of
true -> {ok, NewConfig};
false -> {error, bad_config}
end.
log(LogEvent, HandlerConfig) ->
InetsRunning = whereis(httpc_manager) /= undefined,
SSLRunning = whereis(tls_connection_sup) /= undefined,
case InetsRunning andalso SSLRunning of
true ->
io:format("sending request~n"),
send_request(LogEvent, HandlerConfig);
false ->
ok
end.
% references:
% https://gist.github.com/ArthurClemens/dbd70f9b7a4342810d923670a9db0f39
% https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
send_request(LogEvent, #{ config := Config, formatter := {FM, FC} } = _HandlerConfig) ->
Method = maps:get(method, Config, post),
URL = maps:get(url, Config),
Message = FM:format(LogEvent, FC),
Boundary = generate_uuid(),
LineSeparator = <<"\r\n">>,
RequestBody = iolist_to_binary([
"--", Boundary, LineSeparator,
"Content-Disposition: form-data; name=\"content\"", LineSeparator,
LineSeparator,
Message, LineSeparator,
"--", Boundary, "--"
]),
ContentType = binary_to_list(iolist_to_binary(["multipart/form-data; boundary=", Boundary])),
ContentLength = integer_to_list(byte_size(RequestBody)),
Headers = [
{"Content-Length", ContentLength}
],
HTTPOptions = [{ssl, [
{verify, verify_peer},
{cacerts, certifi:cacerts()},
{depth, 99},
{customize_hostname_check, [
{match_fun, public_key:pkix_verify_hostname_match_fun(https)}
]}
]}],
Options = [{body_format, binary}],
{ok, {{_, Code, _}, _, _}} = httpc:request(Method, {URL, Headers, ContentType, RequestBody}, HTTPOptions, Options),
true = Code >= 200 andalso Code < 300.
% Thanks to https://github.com/afiskon/erlang-uuid-v4
generate_uuid() ->
<<A:32, B:16, C:16, D:16, E:48>> = crypto:strong_rand_bytes(16),
io_lib:format("~8.16.0b-~4.16.0b-4~3.16.0b-~4.16.0b-~12.16.0b",
[A, B, C band 16#0fff, D band 16#3fff bor 16#8000, E]).