src/lager_logtail_backend.erl

%%%-----------------------------------------------------------------------------
%%% @doc
%%%
%%% @author boc_dev
%%% @copyright MIT
%%% @version 0.0.1
%%% @end
%%%-----------------------------------------------------------------------------

-module(lager_logtail_backend).
-author(boc_dev).
-behaviour(gen_event).

%%%=============================================================================
%%% Exports and Definitions
%%%=============================================================================

-export([
    init/1,
    handle_call/2,
    handle_event/2,
    handle_info/2,
    terminate/2,
    code_change/3
]).

-type lager_msg_metadata() :: [tuple()].
-type binary_proplist() :: [{binary(), binary()}].

%%% this is only exported for the spawn call
-export([deferred_log/3]).

-record(state, {
                 level          :: integer(),
                 retry_interval :: integer(),
                 retry_times    :: integer(),
                 token          :: string(),
                 url            :: string()
               }).
-type state() :: state.

-include_lib("lager/include/lager.hrl").

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

%%%=============================================================================
%%% Behaviour Impl
%%%=============================================================================

-spec init(list()) -> {ok, state()}.
init([Level, RetryTimes, RetryInterval, Token]) ->
    State = #state{
                    level          = lager_util:level_to_num(Level),
                    retry_interval = RetryInterval,
                    retry_times    = RetryTimes,
                    token          = Token,
                    url            = "https://in.logtail.com"
                  },
    {ok, State}.

-spec handle_call(get_loglevel | set_loglevel, state()) -> {ok, state()}.
handle_call(get_loglevel, #state{ level = Level } = State) ->
    {ok, Level, State};
handle_call({set_loglevel, Level}, State) ->
    {ok, ok, State#state{ level = lager_util:level_to_num(Level) }};
handle_call(_Request, State) ->
    {ok, ok, State}.
   
-spec handle_event({log, any()}, state()) -> {ok, state()}.
handle_event({log, Message}, #state{level=Level} = State) ->

    case lager_util:is_loggable(Message, Level, ?MODULE) of
        true ->
            Payload = jsx:encode(cons_metadata_to_binary_proplist(lager_msg:metadata(Message), [
                                     {<<"level">>, any_to_binary(lager_msg:severity(Message))},
                                     {<<"message">>, any_to_binary(lager_msg:message(Message))}
                                 ])),
            Request = {State#state.url, [{"Authorization", "Bearer " ++ State#state.token}], "application/json", Payload},
            RetryTimes = State#state.retry_times,
            RetryInterval = State#state.retry_interval,

            %% Spawn a background process to handle sending the payload.
            %% It will recurse until the payload has ben successfully sent.
            spawn(?MODULE, deferred_log, [Request, RetryTimes, RetryInterval]),
            {ok, State};
        false ->
            {ok, State}
    end;
handle_event(_Event, State) ->
    {ok, State}.

handle_info(_Info, State) ->
    {ok, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

-spec deferred_log(any(), integer(), integer()) -> ok.
deferred_log(_Request, 0, _) ->
    io:format("LogTail Request Failed and can't try again"),
    ok;
deferred_log(Request, Retries, Interval) ->
    case httpc:request(post, Request, [], [{body_format, binary}]) of
        {ok, {{_, 202, _}, _Header, _Body}} -> 
            ok;
        Failure ->
            io:format("LogTail Request Failed with ~p retries left: ~p", [Retries, Failure]),
            timer:sleep(Interval * 1000),
            deferred_log(Request, Retries - 1, Interval)
    end.

-spec cons_metadata_to_binary_proplist(Metadata::lager_msg_metadata(), Proplist::binary_proplist()) -> Proplist::binary_proplist().
cons_metadata_to_binary_proplist(Metadata, Proplist) ->
    lists:foldl(fun({Key, Value}, Acc) -> [{any_to_binary(Key), any_to_binary(Value)} | Acc] end, Proplist, Metadata).

-spec any_to_binary(any()) -> binary().
any_to_binary(V) when is_atom(V)    -> any_to_binary(atom_to_list(V));
any_to_binary(V) when is_pid(V)     -> any_to_binary(pid_to_list(V));
any_to_binary(V) when is_list(V)    -> list_to_binary(V);
any_to_binary(V) when is_integer(V) -> integer_to_binary(V);
any_to_binary(V) when is_binary(V)  -> V;
any_to_binary(V)                    -> term_to_binary(V).

%%%===================================================================
%%% Tests
%%%===================================================================

-ifdef(TEST).

any_to_binary_test() ->
    ?assertEqual(<<"hello">>, any_to_binary(hello)),
    ?assertEqual(<<1,2,3>>, any_to_binary([1,2,3])),
    ?assertEqual(<<"1">>, any_to_binary(1)),
    ?assertEqual(<<"1">>, any_to_binary(<<"1">>)),
    ?assertEqual(<<131,104,2,100,0,5,104,101,108,108,111,97,1>>, any_to_binary({hello, 1})).

-endif.