%%%------------------------------------------------------------------- %% @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 %% @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() -> <> = 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]).