380 lines
13 KiB
Erlang
380 lines
13 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(mailer).
|
|
|
|
-behaviour(gen_server).
|
|
|
|
-include_lib("mnesia_tables.hrl").
|
|
-ifdef(TEST).
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-endif.
|
|
|
|
%% API
|
|
-export([start_link/0]).
|
|
-export([queue/0]).
|
|
|
|
%% gen_server callbacks
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
terminate/2, code_change/3]).
|
|
|
|
-record(state, {max_emails_per_minute, remaining_emails}).
|
|
|
|
%%%===================================================================
|
|
%%% API
|
|
%%%===================================================================
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @doc
|
|
%% Starts the server
|
|
%%
|
|
%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
start_link() ->
|
|
gen_server:start_link(?MODULE, [], []).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @doc
|
|
%% Get the number of emails to send.
|
|
%%
|
|
%% @spec queue() -> {ok, Count}
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
queue() ->
|
|
gen_server:call(mailer, {queue_count}).
|
|
|
|
%%%===================================================================
|
|
%%% gen_server callbacks
|
|
%%%===================================================================
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Initializes the server
|
|
%%
|
|
%% @spec init(Args) -> {ok, State} |
|
|
%% {ok, State, Timeout} |
|
|
%% ignore |
|
|
%% {stop, Reason}
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
init([]) ->
|
|
register(mailer, self()),
|
|
{ok, EmailConfig} = application:get_env(log_monitor, email_config),
|
|
MaxEmailsPerMinute = proplists:get_value(max_emails_per_minute, EmailConfig),
|
|
timer:send_after(1000, {send_emails}),
|
|
timer:send_interval(60000, {reset_count}),
|
|
{ok, #state{
|
|
max_emails_per_minute = MaxEmailsPerMinute,
|
|
remaining_emails = MaxEmailsPerMinute
|
|
}
|
|
}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Handling call messages
|
|
%%
|
|
%% @spec handle_call(Request, From, State) ->
|
|
%% {reply, Reply, State} |
|
|
%% {reply, Reply, State, Timeout} |
|
|
%% {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, Reply, State} |
|
|
%% {stop, Reason, State}
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
handle_call({queue_count}, _From, State) ->
|
|
Count = length(emails_to_send()),
|
|
{reply, Count, State};
|
|
|
|
handle_call(_Request, _From, State) ->
|
|
Reply = ok,
|
|
{reply, Reply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Handling cast messages
|
|
%%
|
|
%% @spec handle_cast(Msg, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
handle_cast(_Msg, State) ->
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Handling all non call/cast messages
|
|
%%
|
|
%% @spec handle_info(Info, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
handle_info({error, File, Text}, State) ->
|
|
mnesia:activity(
|
|
transaction,
|
|
fun() ->
|
|
Id = erlang:monotonic_time(),
|
|
mnesia:write(#log_monitor_error{id = Id, file = File, text = Text})
|
|
end),
|
|
{noreply, State};
|
|
handle_info({send_emails}, State) ->
|
|
Emails = lists:sublist(emails_to_send(), State#state.remaining_emails),
|
|
try send_emails(Emails) of
|
|
_ -> timer:send_after(1000, {send_emails})
|
|
catch
|
|
_Throw ->
|
|
error_logger:info_msg("Waiting 60s before sending new emails~n"),
|
|
timer:send_after(60000, {send_emails})
|
|
end,
|
|
{noreply, #state{
|
|
max_emails_per_minute = State#state.max_emails_per_minute,
|
|
remaining_emails = State#state.remaining_emails - length(Emails)
|
|
}
|
|
};
|
|
handle_info({reset_count}, State) ->
|
|
{noreply, #state{
|
|
max_emails_per_minute = State#state.max_emails_per_minute,
|
|
remaining_emails = State#state.max_emails_per_minute
|
|
}
|
|
}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% This function is called by a gen_server 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_server terminates
|
|
%% with Reason. The return value is ignored.
|
|
%%
|
|
%% @spec terminate(Reason, State) -> void()
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
terminate(_Reason, _State) ->
|
|
shutdown.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Convert process state when code is changed
|
|
%%
|
|
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%%===================================================================
|
|
%%% Internal functions
|
|
%%%===================================================================
|
|
emails_to_send() ->
|
|
mnesia:activity(
|
|
transaction,
|
|
fun() ->
|
|
mnesia:foldl(
|
|
fun(#log_monitor_error{id = Id, file = File, text = Text}, Acc) ->
|
|
[{Id, File, Text}] ++ Acc
|
|
end, [], log_monitor_error)
|
|
end).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Send a list of emails.
|
|
%%
|
|
%% @spec send_emails(Emails) -> void()
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
send_emails(Emails) ->
|
|
case Emails of
|
|
[] -> {ok};
|
|
[{Id, File, Text} | Others] ->
|
|
case send_email(File, Text) of
|
|
{error, Reason, Message} ->
|
|
error_logger:error_msg("Error sending email: ~s ~p~n", [Reason, Message]),
|
|
throw(cannot_send_email);
|
|
_ ->
|
|
remove_email(Id),
|
|
send_emails(Others),
|
|
{ok}
|
|
end
|
|
end.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Send an email.
|
|
%%
|
|
%% @spec send_email(File, Text) -> void()
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
send_email(File, Text) ->
|
|
{ok, EmailConfig} = application:get_env(log_monitor, email_config),
|
|
Sender = proplists:get_value(sender, EmailConfig),
|
|
DefaultReceiver = proplists:get_value(default_receiver, EmailConfig),
|
|
RawSubject = case group_subject(File) of
|
|
"" -> proplists:get_value(subject, EmailConfig);
|
|
GroupSubject -> GroupSubject
|
|
end,
|
|
Connection = proplists:get_value(connection, EmailConfig),
|
|
case receivers(File) of
|
|
{ok, GroupReceivers} ->
|
|
Receivers = if GroupReceivers == []
|
|
-> [DefaultReceiver];
|
|
true -> GroupReceivers
|
|
end,
|
|
Subject = email_subject(RawSubject, File, first_line(Text)),
|
|
error_logger:info_msg(
|
|
"Sending email [~n\tSender: ~s,~n\tReceivers: ~s,~n\tSubject: ~s,~n\tFile: ~s~n]~n",
|
|
[Sender, lists:join(",", Receivers), Subject, File]
|
|
),
|
|
gen_smtp_client:send_blocking(
|
|
{Sender, Receivers,
|
|
io_lib:format(
|
|
"Subject: ~s\r\nFrom: ~s\r\n\r\nLogfile: ~s~nError: ~n~s~n",
|
|
[Subject, Sender, File, Text]
|
|
)},
|
|
Connection
|
|
);
|
|
{error, _Reason} -> ok
|
|
end.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Remove the email with the given id from the database.
|
|
%%
|
|
%% @spec remove_email(Id) -> void()
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
remove_email(Id) ->
|
|
mnesia:activity(
|
|
transaction,
|
|
fun() ->
|
|
mnesia:delete({log_monitor_error, Id})
|
|
end),
|
|
ok.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Retrieve the list of receivers for the given file.
|
|
%%
|
|
%% @spec receivers(File) -> {ok, Receivers} | {error, Reason}
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
receivers(File) ->
|
|
mnesia:activity(
|
|
transaction,
|
|
fun() ->
|
|
case mnesia:read(log_monitor_file, File) of
|
|
[{log_monitor_file, File, _, GroupName}] ->
|
|
case mnesia:read(log_monitor_group, GroupName) of
|
|
[Group] ->
|
|
{ok, Group#log_monitor_group.email_receivers};
|
|
_ -> {error, "File group not found"}
|
|
end;
|
|
_ -> {error, "File not found"}
|
|
end
|
|
end).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Retrieve the group raw subject (with placeholdres) from the file.
|
|
%%
|
|
%% @spec group_subject(File) -> Subject
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
group_subject(File) ->
|
|
mnesia:activity(
|
|
transaction,
|
|
fun() ->
|
|
case mnesia:read(log_monitor_file, File) of
|
|
[Logfile] ->
|
|
case mnesia:read(log_monitor_group, Logfile#log_monitor_file.group) of
|
|
[Group] -> Group#log_monitor_group.email_subject;
|
|
_ -> ""
|
|
end;
|
|
_ -> ""
|
|
end
|
|
end).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Build the email subject replacing the placeholders.
|
|
%%
|
|
%% @spec email_subject(Text, File, FirstErrorLine) -> Subject
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
email_subject(Text, File, FirstErrorLine) ->
|
|
Text1 = re:replace(Text, "%f", File, [global, {return, list}]),
|
|
Text2 = re:replace(Text1, "%F", file_name_from_path(File), [global, {return, list}]),
|
|
re:replace(Text2, "%l", FirstErrorLine, [global, {return, list}]).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Extract the basename given the full file path.
|
|
%%
|
|
%% @spec file_name_from_path(File) -> Basename
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
file_name_from_path(File) ->
|
|
{match, [_, _, Basename]} = re:run(File, "^(.*\/)?(.*)\\..*$", [{capture, all, list}]),
|
|
Basename.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% @private
|
|
%% @doc
|
|
%% Returns the first line of the given text
|
|
%%
|
|
%% @spec first_line(Text) -> Line
|
|
%% @end
|
|
%%--------------------------------------------------------------------
|
|
first_line(Text) ->
|
|
lists:takewhile(fun(X) -> X =/= 10 end, Text). %% 10: newline
|
|
|
|
%%%===================================================================
|
|
%%% Tests
|
|
%%%===================================================================
|
|
-ifdef(TEST).
|
|
|
|
%% start_test() ->
|
|
%% {ok, _Pid} = start_link().
|
|
|
|
email_subject_test() ->
|
|
?assertEqual("Error", email_subject("Error", "/var/log/myApp.log", "Line")),
|
|
?assertEqual(
|
|
"[myApp] Error: /var/log/myApp.log",
|
|
email_subject("[%F] Error: %f", "/var/log/myApp.log", "Line")
|
|
),
|
|
?assertEqual(
|
|
"[myApp] Line",
|
|
email_subject("[%F] %l", "/var/log/myApp.log", "Line")
|
|
).
|
|
|
|
file_name_from_path_test() ->
|
|
?assertEqual("myApp", file_name_from_path("myApp.log")),
|
|
?assertEqual("myApp", file_name_from_path("./myApp.log")),
|
|
?assertEqual("myApp", file_name_from_path("/var/log/myApp.log")).
|
|
|
|
first_line_test() ->
|
|
?assertEqual("", first_line("")),
|
|
?assertEqual("Line", first_line("Line")),
|
|
?assertEqual("Line", first_line("Line\nLine")).
|
|
|
|
-endif.
|