283 lines
10 KiB
Erlang
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.
|