log_monitor/apps/log_monitor/src/gatherer.erl

283 lines
10 KiB
Erlang

%%%-------------------------------------------------------------------
%%% @author Fabio Salvini <fs@fabiosalvini.com>
%%% @copyright (C) 2017, Fabio Salvini
%%% @doc
%%%
%%% @end
%%% Created : 2 Jul 2017 by Fabio Salvini <fs@fabiosalvini.com>
%%%-------------------------------------------------------------------
-module(gatherer).
-behaviour(gen_fsm).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
%% API
-export([start_link/2]).
%% gen_fsm callbacks
-export([init/1, handle_event/3, handle_sync_event/4,
handle_info/3, terminate/3, code_change/4]).
-export([state_off/2, state_on/2]).
-define(SERVER, ?MODULE).
-record(log, {file, error_regex}).
-record(state_off, {log}).
-record(state_on, {log, error, until, max_until}).
%%%===================================================================
%%% API
%%%===================================================================
%%--------------------------------------------------------------------
%% @doc
%% Creates a gen_fsm process which calls Module:init/1 to
%% initialize. To ensure a synchronized start-up procedure, this
%% function does not return until Module:init/1 has returned.
%%
%% @spec start_link(File, ErrorRegex) -> {ok, Pid} |
%% ignore |
%% {error, Error}
%% @end
%%--------------------------------------------------------------------
start_link(File, ErrorRegex) ->
gen_fsm:start_link(?MODULE, [File, ErrorRegex], []).
%%%===================================================================
%%% gen_fsm callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm is started using gen_fsm:start/[3,4] or
%% gen_fsm:start_link/[3,4], this function is called by the new
%% process to initialize.
%%
%% @spec init(Args) -> {ok, StateName, State} |
%% {ok, StateName, State, Timeout} |
%% ignore |
%% {stop, StopReason}
%% @end
%%--------------------------------------------------------------------
init([File, ErrorRegex]) ->
{ok, state_off, #state_off{log = #log{file = File, error_regex = ErrorRegex}}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% There should be one instance of this function for each possible
%% state name. Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_event/2, the instance of this function with the same
%% name as the current state name StateName is called to handle
%% the event. It is also called if a timeout occurs.
%%
%% @spec state_off(Event, State) ->
%% {next_state, NextStateName, NextState} |
%% {next_state, NextStateName, NextState, Timeout} |
%% {stop, Reason, NewState}
%% @end
%%--------------------------------------------------------------------
state_off({log_line, Text}, State = #state_off{log = Log}) ->
case is_error(Text, Log#log.error_regex) of
true ->
Timeout = gathering_time(),
MaxTimeout = max_gathering_time(),
Now = to_milliseconds(os:timestamp()),
Until = Now + Timeout,
MaxUntil = Now + MaxTimeout,
{next_state,
state_on,
#state_on{log = Log, error = Text, until = Until, max_until = MaxUntil},
Timeout};
false -> {next_state, state_off, State}
end.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% There should be one instance of this function for each possible
%% state name. Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_event/2, the instance of this function with the same
%% name as the current state name StateName is called to handle
%% the event. It is also called if a timeout occurs.
%%
%% @spec state_on(Event, State) ->
%% {next_state, NextStateName, NextState} |
%% {next_state, NextStateName, NextState, Timeout} |
%% {stop, Reason, NewState}
%% @end
%%--------------------------------------------------------------------
state_on({log_line, Text}, #state_on{log = Log, error = Error, until = Until, max_until = MaxUntil}) ->
case is_error(Text, Log#log.error_regex) of
true ->
Timeout = gathering_time(),
Now = to_milliseconds(os:timestamp()),
NewUntil = min(Now + Timeout, MaxUntil),
{next_state,
state_on,
#state_on{log = Log, error = Error ++ Text, until = NewUntil, max_until = MaxUntil},
if MaxUntil > Now -> Timeout; true -> 0 end};
false ->
Now = to_milliseconds(os:timestamp()),
Timeout = Until - Now,
{next_state,
state_on,
#state_on{log = Log, error = Error ++ Text, until = Until, max_until = MaxUntil},
Timeout}
end;
state_on(timeout, #state_on{log = Log, error = Error, until = _}) ->
mailer ! {error, Log#log.file, Error},
{next_state, state_off, #state_off{log = Log}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_all_state_event/2, this function is called to handle
%% the event.
%%
%% @spec handle_event(Event, StateName, State) ->
%% {next_state, NextStateName, NextState} |
%% {next_state, NextStateName, NextState, Timeout} |
%% {stop, Reason, NewState}
%% @end
%%--------------------------------------------------------------------
handle_event(_Event, StateName, State) ->
{next_state, StateName, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm receives an event sent using
%% gen_fsm:sync_send_all_state_event/[2,3], this function is called
%% to handle the event.
%%
%% @spec handle_sync_event(Event, From, StateName, State) ->
%% {next_state, NextStateName, NextState} |
%% {next_state, NextStateName, NextState, Timeout} |
%% {reply, Reply, NextStateName, NextState} |
%% {reply, Reply, NextStateName, NextState, Timeout} |
%% {stop, Reason, NewState} |
%% {stop, Reason, Reply, NewState}
%% @end
%%--------------------------------------------------------------------
handle_sync_event(_Event, _From, StateName, State) ->
Reply = ok,
{reply, Reply, StateName, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_fsm when it receives any
%% message other than a synchronous or asynchronous event
%% (or a system message).
%%
%% @spec handle_info(Info,StateName,State)->
%% {next_state, NextStateName, NextState} |
%% {next_state, NextStateName, NextState, Timeout} |
%% {stop, Reason, NewState}
%% @end
%%--------------------------------------------------------------------
handle_info(_Info, StateName, State) ->
{next_state, StateName, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_fsm when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_fsm terminates with
%% Reason. The return value is ignored.
%%
%% @spec terminate(Reason, StateName, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, _StateName, _State) ->
shutdown.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, StateName, State, Extra) ->
%% {ok, StateName, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, StateName, State, _Extra) ->
{ok, StateName, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Check if the error regex match the text.
%%
%% @spec is_error(Text, ErrorRegex) -> boolean()
%% @end
%%--------------------------------------------------------------------
is_error(Text, ErrorRegex) ->
case re:run(Text, ErrorRegex) of
{match, _} ->
true;
nomatch ->
false
end.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Get the time (in milliseconds) to gather successive log lines
%% to append to the error line.
%%
%% @spec gathering_time() -> integer()
%% @end
%%--------------------------------------------------------------------
gathering_time() ->
{ok, GeneralConfig} = application:get_env(log_monitor, general),
proplists:get_value(gathering_time, GeneralConfig).
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Get the maximum allowed gathering time (in milliseconds)
%%
%% @spec gathering_time() -> integer()
%% @end
%%--------------------------------------------------------------------
max_gathering_time() ->
{ok, GeneralConfig} = application:get_env(log_monitor, general),
proplists:get_value(max_gathering_time, GeneralConfig).
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Transform the tuple in milliseconds.
%%
%% @spec to_milliseconds({Me, S, Mu}) -> integer()
%% @end
%%--------------------------------------------------------------------
to_milliseconds({Me, S, Mu}) ->
(Me * 1000 * 1000 * 1000) + (S * 1000) + (Mu div 1000).
%%%===================================================================
%%% Tests
%%%===================================================================
-ifdef(TEST).
start_test() ->
{ok, _Pid} = start_link("/tmp/test.log", "ERROR").
is_error_test() ->
?assertNot(is_error("2017-01-01 MyApp - WARN: abc", "ERROR")),
?assert(is_error("2017-01-01 MyApp - ERROR: abc", "ERROR")).
-endif.