1%%%=============================================================================
2%%% Copyright 2016-2017, Tobias Schlager <schlagert@github.com>
3%%%
4%%% Permission to use, copy, modify, and/or distribute this software for any
5%%% purpose with or without fee is hereby granted, provided that the above
6%%% copyright notice and this permission notice appear in all copies.
7%%%
8%%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9%%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10%%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11%%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12%%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13%%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14%%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15%%%
16%%% @doc
17%%% A backend for `lager' redirecting its messages into the `syslog'
18%%% application. Configure like this:
19%%% <pre>
20%%%   {handlers, [{syslog_lager_backend, []}]},
21%%% </pre>
22%%%
23%%% @see https://github.com/basho/lager
24%%% @end
25%%%=============================================================================
26-module(syslog_lager_backend).
27
28-behaviour(gen_event).
29
30%% API
31-export([set_log_level/1]).
32
33%% gen_event callbacks
34-export([init/1,
35         handle_event/2,
36         handle_call/2,
37         handle_info/2,
38         terminate/2,
39         code_change/3]).
40
41-include("syslog.hrl").
42
43-define(CFG, [message]).
44
45%% avoid warnings when lager is not a project dependency
46-dialyzer(no_missing_calls).
47
48%%%=============================================================================
49%%% API
50%%%=============================================================================
51
52%%------------------------------------------------------------------------------
53%% @doc
54%% Set a specific log level for this lager backend, never fails.
55%% @end
56%%------------------------------------------------------------------------------
57-spec set_log_level(syslog:severity() | info) -> ok.
58set_log_level(informational) ->
59    set_log_level(info);
60set_log_level(Level) ->
61    catch lager:set_loglevel(?MODULE, Level),
62    ok.
63
64%%%=============================================================================
65%%% gen_event callbacks
66%%%=============================================================================
67
68-record(state, {
69          log_level       :: integer() | {mask, integer()},
70          formatter       :: atom(),
71          format_cfg      :: list(),
72          sd_id           :: string() | undefined,
73          metadata_keys   :: [atom()],
74          appname_key     :: atom() | undefined}).
75
76%%------------------------------------------------------------------------------
77%% @private
78%%------------------------------------------------------------------------------
79init([]) ->
80    init([syslog_lib:get_property(log_level, ?SYSLOG_LOGLEVEL)]);
81init([Level]) ->
82    init([Level, {}, {lager_default_formatter, ?CFG}]);
83init([Level, {}, {Formatter, FormatterConfig}]) when is_atom(Formatter) ->
84    init([Level, {undefined, []}, {Formatter, FormatterConfig}]);
85init([Level, SData, {Formatter, FormatterConfig}])
86  when is_atom(Formatter) ->
87    AppnameKey = syslog_lib:get_name_metdata_key(),
88    init([Level, SData, {Formatter, FormatterConfig}, AppnameKey]);
89init([Level, {}, {Formatter, FormatterConfig}, AppnameKey])
90  when is_atom(Formatter) ->
91    init([Level, {undefined, []}, {Formatter, FormatterConfig}, AppnameKey]);
92init([Level, {SDataId, MDKeys}, {Formatter, FormatterConfig}, AppnameKey])
93  when is_atom(Formatter) ->
94    {ok, #state{
95            log_level = level_to_mask(Level),
96            sd_id = SDataId,
97            metadata_keys = MDKeys,
98            formatter = Formatter,
99            format_cfg = FormatterConfig,
100            appname_key = get_appname_key(AppnameKey)}}.
101
102%%------------------------------------------------------------------------------
103%% @private
104%%------------------------------------------------------------------------------
105handle_event({log, Level, _, [_, Location, Message]}, State)
106  when Level =< State#state.log_level ->
107    Severity = get_severity(Level),
108    Pid = get_pid(Location),
109    Timestamp = os:timestamp(),
110    syslog_logger:log(Severity, Pid, Timestamp, [], Message, no_format),
111    {ok, State};
112handle_event({log, Msg}, State = #state{log_level = Level}) ->
113    case lager_util:is_loggable(Msg, Level, ?MODULE) of
114        true ->
115            syslog_logger:log(get_severity(Msg),
116                              get_pid(Msg),
117                              lager_msg:timestamp(Msg),
118                              get_structured_data(Msg, State),
119                              format_msg(Msg, State),
120                              no_format,
121                              get_appname_override(Msg, State));
122        false ->
123            ok
124    end,
125    {ok, State};
126handle_event(_Event, State) ->
127    {ok, State}.
128
129%%------------------------------------------------------------------------------
130%% @private
131%%------------------------------------------------------------------------------
132handle_call(get_loglevel, State = #state{log_level = Level}) ->
133    {ok, Level, State};
134handle_call({set_loglevel, Level}, State) ->
135    try
136        {ok, ok, State#state{log_level = level_to_mask(Level)}}
137    catch
138        _:_ -> {ok, {error, {bad_log_level, Level}}, State}
139    end;
140handle_call(_Request, State) ->
141    {ok, undef, State}.
142
143%%------------------------------------------------------------------------------
144%% @private
145%%------------------------------------------------------------------------------
146handle_info(_Info, State) -> {ok, State}.
147
148%%------------------------------------------------------------------------------
149%% @private
150%%------------------------------------------------------------------------------
151terminate(_Arg, #state{}) -> ok.
152
153%%------------------------------------------------------------------------------
154%% @private
155%%------------------------------------------------------------------------------
156code_change(_OldVsn, State, _Extra) -> {ok, State}.
157
158%%%=============================================================================
159%%% internal functions
160%%%=============================================================================
161
162%%------------------------------------------------------------------------------
163%% @private
164%%------------------------------------------------------------------------------
165get_severity(info) ->
166    informational;
167get_severity(Level) when is_atom(Level) ->
168    Level;
169get_severity(Level) when is_integer(Level) ->
170    get_severity(lager_util:num_to_level(Level));
171get_severity(Msg) ->
172    get_severity(lager_msg:severity(Msg)).
173
174%%------------------------------------------------------------------------------
175%% @private
176%%------------------------------------------------------------------------------
177get_pid(Location) when is_list(Location) ->
178    Location;
179get_pid(Msg) ->
180    try lager_msg:metadata(Msg) of
181        Metadata ->
182            case lists:keyfind(pid, 1, Metadata) of
183                {pid, Pid} when is_pid(Pid)    -> Pid;
184                {pid, List} when is_list(List) -> List;
185                _                              -> self()
186            end
187    catch
188        _:_ -> self()
189    end.
190
191%%------------------------------------------------------------------------------
192%% @private
193%%------------------------------------------------------------------------------
194level_to_mask(informational) ->
195    level_to_mask(info);
196level_to_mask(Level) ->
197    try
198        lager_util:config_to_mask(Level)
199    catch
200        error:undef -> lager_util:level_to_num(Level)
201    end.
202
203%%------------------------------------------------------------------------------
204%% @private
205%%------------------------------------------------------------------------------
206get_structured_data(_Msg, #state{sd_id = undefined}) ->
207    [];
208get_structured_data(_Msg, #state{metadata_keys = []}) ->
209    [];
210get_structured_data(Msg, #state{sd_id = SDId, metadata_keys = MDKeys}) ->
211    try lager_msg:metadata(Msg) of
212        Metadata -> syslog_lib:get_structured_data(Metadata, SDId, MDKeys)
213    catch
214        _:_ -> []
215    end.
216
217%%------------------------------------------------------------------------------
218%% @private
219%% Map `true' to `application' for backwards compatibility.
220%%------------------------------------------------------------------------------
221get_appname_key(true)                      -> application;
222get_appname_key(false)                     -> undefined;
223get_appname_key(Value) when is_atom(Value) -> Value.
224
225%%------------------------------------------------------------------------------
226%% @private
227%%------------------------------------------------------------------------------
228get_appname_override(_, #state{appname_key = undefined}) ->
229    [];
230get_appname_override(Msg, #state{appname_key = Key}) ->
231    try lager_msg:metadata(Msg) of
232        Metadata ->
233            case proplists:get_value(Key, Metadata) of
234                undefined -> [];
235                Result    -> [{appname, Result}]
236            end
237    catch
238        _:_ -> []
239    end.
240
241%%------------------------------------------------------------------------------
242%% @private
243%%------------------------------------------------------------------------------
244format_msg(Msg, #state{formatter = Formatter, format_cfg = FormatterConfig}) ->
245    Formatter:format(Msg, FormatterConfig).
246