%% @doc Utility module to handle logs.
%% @end
-module(grisp_connect_log).
-behaviour(logger_formatter).
-include_lib("kernel/include/logger.hrl").
% API functions
-export([get/1]).
-export([sync/1]).
% Behaviour logger_formatter callback functions
-export([check_config/1]).
-export([format/2]).
%--- Macros --------------------------------------------------------------------
% FixMe:
% Sending over ~30_000 bytes over WS breaks rtems I/O driver.
% We want avoid to return chunks that are bigger then that.
-define(MAX_CHUNK_BYTES, 30_000).
%--- Types ---------------------------------------------------------------------
-type get_options() :: #{
max_batch_size => non_neg_integer(),
max_byte_size => non_neg_integer()
}.
-type sync_options() :: #{
seq := non_neg_integer(),
dropped => non_neg_integer()
}.
%--- API Functions -------------------------------------------------------------
-spec get(Opts :: get_options()) ->
#{dropped := non_neg_integer(), events := list()}.
get(Opts) ->
{ok, DefaultSize} = application:get_env(grisp_connect, logs_batch_size),
BatchSize = maps:get(max_batch_size, Opts, DefaultSize),
ByteSize = min(maps:get(max_byte_size, Opts, ?MAX_CHUNK_BYTES), ?MAX_CHUNK_BYTES),
{Events, Dropped} = grisp_connect_logger_bin:chunk(BatchSize, ByteSize),
#{events => [[Seq, jsonify(E)] || {Seq, E} <- Events],
dropped => Dropped}.
-spec sync(Opts :: sync_options()) -> ok.
sync(#{seq := Seq, dropped := Dropped}) ->
grisp_connect_logger_bin:sync(Seq, Dropped).
%--- Beahviour logger_formatter Callback Functions -----------------------------
check_config(Config = #{description_only := Bool})
when is_boolean(Bool) ->
logger_formatter:check_config(maps:remove(description_only, Config));
check_config(#{description_only := V}) ->
{error, {invalid_formatter_config, ?MODULE, {description_only, V}}};
check_config(Config) ->
logger_formatter:check_config(Config).
format(LogEvent = #{msg := {report, #{description := Desc}}},
Config = #{description_only := true}) when is_binary(Desc) ->
logger_formatter:format(LogEvent#{msg := {string, Desc}},
maps:remove(description_only, Config));
format(LogEvent, Config) ->
logger_formatter:format(LogEvent, maps:remove(description_only, Config)).
%--- Internal Functions --------------------------------------------------------
jsonify(Event) ->
jsonify_meta(jsonify_msg(binary_to_term(base64:decode(Event)))).
jsonify_msg(#{msg := {string, String}} = Event) ->
maps:put(msg, unicode:characters_to_binary(String), Event);
jsonify_msg(#{msg := {report, Report}} = Event) ->
case is_json_compatible(Report) of
true ->
maps:put(msg, Report, Event);
false ->
String = unicode:characters_to_binary(
io_lib:format("~tp", [Report])),
Meta = maps:get(meta, Event, #{}),
Event2 = Event#{meta => Meta#{incompatible_term => true}},
maps:put(msg, String, Event2)
end;
jsonify_msg(#{msg := {FormatString, Term}} = Event) ->
%FIXME: scan format and ensure unicode encoding
String = unicode:characters_to_binary(io_lib:format(FormatString, Term)),
maps:put(msg, String, Event).
jsonify_meta(#{meta := Meta} = Event) ->
MFA = case maps:is_key(mfa, Meta) of
true ->
{M, F, A} = maps:get(mfa, Meta),
[M, F, A];
false ->
null
end,
File = case maps:is_key(file, Meta) of
true -> unicode:characters_to_binary(maps:get(file, Meta));
false -> null
end,
Default = #{mfa => MFA, file => File},
Optional = maps:without(maps:keys(Default), Meta),
FilterFun = fun(Key, Value) -> jsx:is_term(#{Key => Value}) end,
maps:put(meta, maps:merge(maps:filter(FilterFun, Optional), Default), Event).
is_json_compatible(Term) ->
try jsx:is_term(Term)
catch error:_ ->
false
end.