log_monitor/apps/log_monitor/src/mailer.erl

480 lines
16 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, queue_details/0, empty_queue/0, empty_file_queue/1]).
%% 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}).
%%--------------------------------------------------------------------
%% @doc
%% Get the queue details.
%%
%% @spec queue() -> {ok, Details}
%% @end
%%--------------------------------------------------------------------
queue_details() ->
gen_server:call(mailer, {queue_details}).
%%--------------------------------------------------------------------
%% @doc
%% Delete all the mails from the queue.
%%
%% @spec empty_queue() -> ok
%% @end
%%--------------------------------------------------------------------
empty_queue() ->
gen_server:call(mailer, {empty_queue}).
%%--------------------------------------------------------------------
%% @doc
%% Delete all the mails of the given file from the queue.
%%
%% @spec empty_file_queue(File) -> ok
%% @end
%%--------------------------------------------------------------------
empty_file_queue(File) ->
gen_server:call(mailer, {empty_file_queue, File}).
%%%===================================================================
%%% 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({queue_details}, _From, State) ->
Details = dict:to_list(get_queue_details()),
{reply, Details, State};
handle_call({empty_queue}, _From, State) ->
remove_all_emails(),
{reply, ok, State};
handle_call({empty_file_queue, File}, _From, State) ->
remove_file_emails(File),
{reply, ok, 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
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Get the number of errors for each file.
%%
%% @spec queue_details() -> Dict
%% @end
%%--------------------------------------------------------------------
get_queue_details() ->
mnesia:activity(
transaction,
fun() ->
mnesia:foldl(
fun(#log_monitor_error{id = _Id, file = File, text = _Text}, Acc) ->
dict:update_counter(File, 1, Acc)
end, dict:new(), log_monitor_error)
end).
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Get all the emails that need to be sent.
%%
%% @spec emails_to_send() -> List
%% @end
%%--------------------------------------------------------------------
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
%% Remove all emails of the given file from the database.
%%
%% @spec remove_file_emails(File) -> void()
%% @end
%%--------------------------------------------------------------------
remove_file_emails(File) ->
mnesia:activity(
transaction,
fun() ->
mnesia:foldl(
fun(#log_monitor_error{id = Id, file = ErrorFile, text = _Text}, _Acc) ->
case ErrorFile of
File -> mnesia:delete({log_monitor_error, Id});
_ -> ok
end
end, [], log_monitor_error)
end),
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Remove all emails from the database.
%%
%% @spec remove_all_emails() -> void()
%% @end
%%--------------------------------------------------------------------
remove_all_emails() ->
mnesia:clear_table(log_monitor_error),
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.