Current section

Files

Jump to
syslog_socket src syslog_socket.erl
Raw

src/syslog_socket.erl

%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
%%%
%%%------------------------------------------------------------------------
%%% @doc
%%% ==Erlang syslog Client Interface==
%%% A minimal syslog client interface that supports both RFC3164 and RFC5424.
%%% @end
%%%
%%% MIT License
%%%
%%% Copyright (c) 2016-2017 Michael Truog <mjtruog at gmail dot com>
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a
%%% copy of this software and associated documentation files (the "Software"),
%%% to deal in the Software without restriction, including without limitation
%%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%%% and/or sell copies of the Software, and to permit persons to whom the
%%% Software is furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in
%%% all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%%% DEALINGS IN THE SOFTWARE.
%%%
%%% @author Michael Truog <mjtruog [at] gmail (dot) com>
%%% @copyright 2016-2017 Michael Truog
%%% @version 1.7.2 {@date} {@time}
%%%------------------------------------------------------------------------
-module(syslog_socket).
-author('mjtruog [at] gmail (dot) com').
-behaviour(gen_server).
%% external interface
-export([start_link/1,
stop_link/1,
stop_link/2,
send/3,
send/4,
send/5,
facility/1,
facility_valid/1,
severity/1,
severity_valid/1]).
%% gen_server callbacks
-export([init/1,
handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(TRANSPORT_DEFAULT, local).
-define(PROTOCOL_DEFAULT, rfc3164).
-define(UTF8_DEFAULT, true).
-define(FACILITY_DEFAULT, local0).
-define(TIMEOUT_DEFAULT, 5000). % milliseconds
-define(TIMEOUT_MAX_ERLANG, 4294967295). % milliseconds
-define(SP, $\s).
-type transport() :: local | udp | tcp | tls.
-type protocol() :: rfc3164 | rfc5424.
-type timeout_milliseconds() :: 1..?TIMEOUT_MAX_ERLANG.
-type app_name() :: nonempty_string().
-type facility() :: kernel | user | mail | daemon | auth0 | syslog |
print | news | uucp | clock0 | auth1 | ftp | ntp |
auth2 | auth3 | clock1 | local0 | local1 | local2 |
local3 | local4 | local5 | local6 | local7 |
non_neg_integer() |
% common aliases
auth | authpriv | cron | kern | lpr | security.
-type severity() :: emergency | alert | critical | error | warning |
notice | informational | debug |
0..7 |
% common aliases
emerg | panic | crit | err | warn | info.
-type message_id() :: string() | binary().
-type options() :: list({transport, transport()} |
{transport_options, list()} |
{protocol, protocol()} |
{utf8, boolean()} |
{facility, facility()} |
{app_name, app_name()} |
{path, nonempty_string()} |
{host, inet:ip_address() | inet:hostname()} |
{port, undefined | inet:port_number()} |
{timeout, timeout_milliseconds()}).
-export_type([transport/0,
protocol/0,
timeout_milliseconds/0,
app_name/0,
facility/0,
severity/0,
message_id/0,
options/0]).
-record(state,
{
transport :: transport(),
transport_options :: list(),
protocol :: protocol(),
utf8 :: boolean(),
utf8_bom :: binary(),
facility :: non_neg_integer(),
app_name :: app_name(),
path :: nonempty_string(),
host :: inet:ip_address() | inet:hostname(),
port :: inet:port_number(),
timeout :: pos_integer(),
socket = undefined :: undefined | inet:socket() | ssl:sslsocket(),
hostname = undefined :: undefined | string(),
os_pid = undefined :: undefined | string()
}).
%%%------------------------------------------------------------------------
%%% External interface functions
%%%------------------------------------------------------------------------
%%-------------------------------------------------------------------------
%% @doc
%% ===Start a syslog socket.===
%% Connection is immediate within the new Erlang process.
%% @end
%%-------------------------------------------------------------------------
-spec start_link(Options :: options()) ->
{ok, pid()} | {error, any()}.
start_link(Options) when is_list(Options) ->
Defaults = [{transport, ?TRANSPORT_DEFAULT},
{transport_options, undefined},
{protocol, ?PROTOCOL_DEFAULT},
{utf8, ?UTF8_DEFAULT},
{facility, ?FACILITY_DEFAULT},
{app_name, undefined},
{path, "/dev/log"},
{host, {127,0,0,1}},
{port, undefined},
{timeout, ?TIMEOUT_DEFAULT}],
[Transport, TransportOptionsValue, Protocol,
UTF8, FacilityValue, AppName,
Path, Host, PortValue, Timeout] = take_values(Defaults, Options),
true = is_list(AppName) andalso is_integer(hd(AppName)), % required
if
Protocol =:= rfc3164 ->
ok;
Protocol =:= rfc5424 ->
true = (length(AppName) =< 48)
end,
true = is_boolean(UTF8),
Facility = facility(FacilityValue),
true = is_integer(Timeout) andalso
(Timeout > 0) andalso (Timeout =< ?TIMEOUT_MAX_ERLANG),
TransportOptions = if
Transport =:= local ->
if
TransportOptionsValue =:= undefined ->
[local];
is_list(TransportOptionsValue) ->
[local | TransportOptionsValue]
end;
Transport =:= udp ->
if
TransportOptionsValue =:= undefined ->
[];
is_list(TransportOptionsValue) ->
TransportOptionsValue
end;
Transport =:= tcp ->
if
TransportOptionsValue =:= undefined ->
[{send_timeout, Timeout},
{send_timeout_close, true},
{keepalive, true},
{reuseaddr, true}];
is_list(TransportOptionsValue) ->
TransportOptionsValue
end;
Transport =:= tls ->
true = is_list(TransportOptionsValue) andalso
(length(TransportOptionsValue) > 0),
TransportOptionsValue
end,
true = is_list(Path) andalso is_integer(hd(Path)),
Port = if
PortValue =:= undefined ->
if
Transport =:= local ->
0;
Transport =:= udp ->
514;
Transport =:= tcp ->
601;
Transport =:= tls ->
6514
end;
is_integer(PortValue), PortValue > 0 ->
PortValue
end,
gen_server:start_link(?MODULE,
[Transport, TransportOptions, Protocol,
UTF8, Facility, AppName,
Path, Host, Port, Timeout], []).
%%-------------------------------------------------------------------------
%% @doc
%% ===Stop an existing syslog socket asynchronously.===
%% @end
%%-------------------------------------------------------------------------
-spec stop_link(Pid :: pid()) ->
ok.
stop_link(Pid) ->
gen_server:cast(Pid, stop).
%%-------------------------------------------------------------------------
%% @doc
%% ===Stop an existing syslog socket synchronously.===
%% @end
%%-------------------------------------------------------------------------
-spec stop_link(Pid :: pid(),
Timeout :: timeout_milliseconds()) ->
ok.
stop_link(Pid, Timeout) ->
gen_server:call(Pid, stop, Timeout).
%%-------------------------------------------------------------------------
%% @doc
%% ===Send data to syslog without a timestamp or message_id.===
%% @end
%%-------------------------------------------------------------------------
-spec send(Pid :: pid(),
Severity :: severity(),
Data :: iodata()) ->
ok.
send(Pid, Severity, Data) ->
send(Pid, Severity, undefined, "", Data).
%%-------------------------------------------------------------------------
%% @doc
%% ===Send data to syslog without a message_id.===
%% @end
%%-------------------------------------------------------------------------
-spec send(Pid :: pid(),
Severity :: severity(),
Timestamp :: undefined | erlang:timestamp(),
Data :: iodata()) ->
ok.
send(Pid, Severity, Timestamp, Data) ->
send(Pid, Severity, Timestamp, "", Data).
%%-------------------------------------------------------------------------
%% @doc
%% ===Send data to syslog.===
%% @end
%%-------------------------------------------------------------------------
-spec send(Pid :: pid(),
Severity :: severity(),
Timestamp :: undefined | erlang:timestamp(),
MessageId :: message_id(),
Data :: iodata()) ->
ok.
send(Pid, Severity, Timestamp, MessageId, Data) ->
gen_server:cast(Pid, {send, Severity, Timestamp, MessageId, Data}).
%%%------------------------------------------------------------------------
%%% facility and severity values are based on
%%% https://tools.ietf.org/html/rfc3164#section-4.1.1
%%% https://tools.ietf.org/html/rfc5424#section-6.2.1
%%%------------------------------------------------------------------------
%%-------------------------------------------------------------------------
%% @doc
%% ===Get the syslog facility numerical value.===
%% @end
%%-------------------------------------------------------------------------
-spec facility(Facility :: facility()) ->
non_neg_integer().
facility(kernel) -> 0; % kernel messages
facility(user) -> 1; % user-level messages
facility(mail) -> 2; % mail system
facility(daemon) -> 3; % system daemons
facility(auth0) -> 4; % security/authorization messages 0
facility(syslog) -> 5; % messages generated internally by syslogd
facility(print) -> 6; % line printer subsystem
facility(news) -> 7; % network news subsystem
facility(uucp) -> 8; % UUCP subsystem
facility(clock0) -> 9; % clock daemon 0
facility(auth1) -> 10; % security/authorization messages 1
facility(ftp) -> 11; % FTP daemon
facility(ntp) -> 12; % NTP subsystem
facility(auth2) -> 13; % log audit (security/authorization 2)
facility(auth3) -> 14; % log alert (security/authorization 3)
facility(clock1) -> 15; % clock daemon 1
facility(local0) -> 16; % local use 0
facility(local1) -> 17; % local use 1
facility(local2) -> 18; % local use 2
facility(local3) -> 19; % local use 3
facility(local4) -> 20; % local use 4
facility(local5) -> 21; % local use 5
facility(local6) -> 22; % local use 6
facility(local7) -> 23; % local use 7
facility(auth) -> facility(auth0);
facility(authpriv) -> facility(auth1);
facility(cron) -> facility(clock0);
facility(kern) -> facility(kernel);
facility(lpr) -> facility(print);
facility(security) -> facility(auth0);
facility(Facility) when is_integer(Facility), Facility >= 0 ->
Facility.
%%-------------------------------------------------------------------------
%% @doc
%% ===Test the validity of a syslog facility value.===
%% @end
%%-------------------------------------------------------------------------
-spec facility_valid(Facility :: facility()) ->
boolean().
facility_valid(Facility) ->
try facility(Facility) of
_ -> true
catch
_:_ -> false
end.
%%-------------------------------------------------------------------------
%% @doc
%% ===Get the syslog severity numerical value.===
%% @end
%%-------------------------------------------------------------------------
-spec severity(Severity :: severity()) ->
0..7.
severity(emergency) -> 0; % system is unusable
severity(alert) -> 1; % action must be taken immediately
severity(critical) -> 2; % critical conditions
severity(error) -> 3; % error conditions
severity(warning) -> 4; % warning conditions
severity(notice) -> 5; % normal but significant condition
severity(informational) -> 6; % informational messages
severity(debug) -> 7; % debug-level messages
severity(emerg) -> severity(emergency);
severity(panic) -> severity(emergency);
severity(crit) -> severity(critical);
severity(err) -> severity(error);
severity(warn) -> severity(warning);
severity(info) -> severity(informational);
severity(Severity) when is_integer(Severity), Severity >= 0, Severity =< 7 ->
Severity.
%%-------------------------------------------------------------------------
%% @doc
%% ===Test the validity of a syslog severity value.===
%% @end
%%-------------------------------------------------------------------------
-spec severity_valid(Severity :: severity()) ->
boolean().
severity_valid(Severity) ->
try severity(Severity) of
_ -> true
catch
_:_ -> false
end.
%%%------------------------------------------------------------------------
%%% Callback functions from gen_server
%%%------------------------------------------------------------------------
init([Transport, TransportOptions, Protocol,
UTF8, Facility, AppName,
Path, Host, Port, Timeout]) ->
State0 = #state{transport = Transport,
transport_options = TransportOptions,
protocol = Protocol,
utf8 = UTF8,
utf8_bom = unicode:encoding_to_bom(utf8),
facility = Facility,
app_name = AppName,
path = Path,
host = Host,
port = Port,
timeout = Timeout},
case transport_open(State0) of
{ok, StateN} ->
{ok, protocol_init(StateN)};
{error, _} = Error ->
{stop, Error, State0}
end.
handle_call(stop, _, State) ->
{stop, normal, ok, State};
handle_call(Request, _, State) ->
{stop, {error, {call, Request}}, State}.
handle_cast({send, Severity, Timestamp0, MessageId, Data}, State) ->
TimestampN = case Timestamp0 of
undefined ->
os:timestamp();
{_, _, _} ->
Timestamp0
end,
HEADER = protocol_header(Severity, TimestampN, MessageId, State),
DATA = protocol_data(State),
MSG = protocol_msg(Data, State),
ok = transport_send([HEADER, DATA, MSG], State),
{noreply, State};
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(Request, State) ->
{stop, {error, {cast, Request}}, State}.
handle_info({Error, Socket}, #state{socket = Socket} = State)
when Error =:= udp_closed;
Error =:= tcp_closed;
Error =:= ssl_closed ->
{stop, {error, Error}, State#state{socket = undefined}};
handle_info({Error, Socket}, #state{socket = Socket} = State)
when Error =:= udp_error;
Error =:= tcp_error;
Error =:= ssl_error ->
{stop, {error, Error}, State};
handle_info(Request, State) ->
{stop, {error, {info, Request}}, State}.
terminate(_, State) ->
ok = transport_close(State),
ok.
code_change(_, State, _) ->
{ok, State}.
%%%------------------------------------------------------------------------
%%% Private functions
%%%------------------------------------------------------------------------
protocol_init(#state{protocol = rfc3164} = State) ->
HostnameOnly = lists:takewhile(fun(C) -> C /= $. end, hostname()),
State#state{hostname = HostnameOnly,
os_pid = os:getpid()};
protocol_init(#state{protocol = rfc5424} = State) ->
State#state{hostname = hostname(),
os_pid = os:getpid()}.
protocol_header(Severity, Timestamp, MessageId,
#state{transport = Transport,
protocol = rfc3164,
facility = Facility,
app_name = APP_NAME,
hostname = Hostname,
os_pid = PROCID}) ->
PRIVAL = (Facility bsl 3) + severity(Severity),
TIMESTAMP = timestamp_rfc3164(Timestamp),
HOSTNAME_SP = if
Transport =:= local ->
[];
true ->
[Hostname, ?SP]
end,
SP_MSGID = case MessageId of
[] ->
[];
<<>> ->
[];
_ ->
[?SP, MessageId]
end,
[$<, int_to_dec_list(PRIVAL), $>,
TIMESTAMP, ?SP,
HOSTNAME_SP,
% extra information as MSG prefix
APP_NAME, $[, PROCID, $], $:, SP_MSGID];
protocol_header(Severity, Timestamp, MessageId,
#state{protocol = rfc5424,
facility = Facility,
app_name = APP_NAME,
hostname = HOSTNAME,
os_pid = PROCID}) ->
PRIVAL = (Facility bsl 3) + severity(Severity),
VERSION = $1,
TIMESTAMP = timestamp_rfc5424(Timestamp),
MSGID = case MessageId of
[] ->
$-;
<<>> ->
$-;
_ ->
truncate(MessageId, 32)
end,
[$<, int_to_dec_list(PRIVAL), $>,
VERSION, ?SP,
TIMESTAMP, ?SP,
truncate(HOSTNAME, 255), ?SP,
truncate(APP_NAME, 48), ?SP,
PROCID, ?SP,
MSGID].
protocol_data(#state{protocol = rfc3164}) ->
[];
protocol_data(#state{protocol = rfc5424}) ->
STRUCTURED_DATA = $-,
[?SP, STRUCTURED_DATA].
protocol_msg(Data,
#state{utf8 = UTF8,
utf8_bom = UTF8BOM}) ->
if
Data == <<>>; Data == [] ->
[];
UTF8 =:= true ->
[?SP, UTF8BOM, Data];
UTF8 =:= false ->
[?SP, Data] % ASCII
end.
timestamp_rfc3164({_, _, _} = Timestamp) ->
{{_, DateMM, DateDD},
{TimeHH, TimeMM, TimeSS}} = calendar:now_to_local_time(Timestamp),
[DateMM0, DateMM1, DateMM2] = if
DateMM == 1 -> "Jan";
DateMM == 2 -> "Feb";
DateMM == 3 -> "Mar";
DateMM == 4 -> "Apr";
DateMM == 5 -> "May";
DateMM == 6 -> "Jun";
DateMM == 7 -> "Jul";
DateMM == 8 -> "Aug";
DateMM == 9 -> "Sep";
DateMM == 10 -> "Oct";
DateMM == 11 -> "Nov";
DateMM == 12 -> "Dec"
end,
[DateDD0, DateDD1] = int_to_dec_list(DateDD, 2, $\s),
[TimeHH0, TimeHH1] = int_to_dec_list(TimeHH, 2, $0),
[TimeMM0, TimeMM1] = int_to_dec_list(TimeMM, 2, $0),
[TimeSS0, TimeSS1] = int_to_dec_list(TimeSS, 2, $0),
[DateMM0, DateMM1, DateMM2, $\s,
DateDD0, DateDD1, $\s,
TimeHH0, TimeHH1, $:, TimeMM0, TimeMM1, $:, TimeSS0, TimeSS1].
timestamp_rfc5424({_, _, MicroSeconds} = Timestamp) ->
{{DateYYYY, DateMM, DateDD},
{TimeHH, TimeMM, TimeSS}} = calendar:now_to_universal_time(Timestamp),
[DateYYYY0, DateYYYY1,
DateYYYY2, DateYYYY3] = int_to_dec_list(DateYYYY, 4, $0),
[DateMM0, DateMM1] = int_to_dec_list(DateMM, 2, $0),
[DateDD0, DateDD1] = int_to_dec_list(DateDD, 2, $0),
[TimeHH0, TimeHH1] = int_to_dec_list(TimeHH, 2, $0),
[TimeMM0, TimeMM1] = int_to_dec_list(TimeMM, 2, $0),
[TimeSS0, TimeSS1] = int_to_dec_list(TimeSS, 2, $0),
[MicroSeconds0, MicroSeconds1,
MicroSeconds2, MicroSeconds3,
MicroSeconds4, MicroSeconds5] = int_to_dec_list(MicroSeconds, 6, $0),
[DateYYYY0, DateYYYY1, DateYYYY2, DateYYYY3, $-,
DateMM0, DateMM1, $-, DateDD0, DateDD1, $T,
TimeHH0, TimeHH1, $:, TimeMM0, TimeMM1, $:, TimeSS0, TimeSS1, $.,
MicroSeconds0, MicroSeconds1,
MicroSeconds2, MicroSeconds3,
MicroSeconds4, MicroSeconds5, $Z].
transport_open(#state{transport = Transport,
transport_options = TransportOptions} = State)
when Transport =:= local; Transport =:= udp ->
try gen_udp:open(0, TransportOptions) of
{ok, Socket} ->
{ok, State#state{socket = Socket}};
{error, _} = Error ->
Error
catch
ErrorType:ErrorReason ->
{error, {ErrorType, ErrorReason}}
end;
transport_open(#state{transport = tcp,
transport_options = TransportOptions,
host = Host,
port = Port,
timeout = Timeout} = State) ->
try gen_tcp:connect(Host, Port, TransportOptions, Timeout) of
{ok, Socket} ->
{ok, State#state{socket = Socket}};
{error, _} = Error ->
Error
catch
ErrorType:ErrorReason ->
{error, {ErrorType, ErrorReason}}
end;
transport_open(#state{transport = tls,
transport_options = TransportOptions,
host = Host,
port = Port,
timeout = Timeout} = State) ->
try ssl:connect(Host, Port, TransportOptions, Timeout) of
{ok, Socket} ->
{ok, State#state{socket = Socket}};
{error, _} = Error ->
Error
catch
ErrorType:ErrorReason ->
{error, {ErrorType, ErrorReason}}
end.
transport_send(Data, #state{transport = local,
path = Path,
port = Port,
socket = Socket}) ->
ok = gen_udp:send(Socket, {local, Path}, Port, Data);
transport_send(Data, #state{transport = udp,
host = Host,
port = Port,
socket = Socket}) ->
ok = gen_udp:send(Socket, Host, Port, Data);
transport_send(Data, #state{transport = tcp,
socket = Socket}) ->
ok = gen_tcp:send(Socket, Data);
transport_send(Data, #state{transport = tls,
socket = Socket}) ->
ok = ssl:send(Socket, Data).
transport_close(#state{socket = undefined}) ->
ok;
transport_close(#state{transport = Transport,
socket = Socket})
when Transport =:= local; Transport =:= udp ->
_ = (catch gen_udp:close(Socket)),
ok;
transport_close(#state{transport = tcp,
socket = Socket}) ->
_ = (catch gen_tcp:close(Socket)),
ok;
transport_close(#state{transport = tls,
socket = Socket}) ->
_ = (catch ssl:close(Socket)),
ok.
hostname() ->
case lists:dropwhile(fun(C) -> C /= $@ end,
erlang:atom_to_list(node())) of
[$@ | "nohost"] ->
{ok, Hostname} = inet:gethostname(),
Hostname;
[$@ | FQDN] ->
FQDN
end.
truncate(Value, N) when is_list(Value) ->
lists:sublist(Value, N);
truncate(Value, N) when is_binary(Value) ->
case Value of
<<ValuePrefix:N/binary-unit:8, _/binary>> ->
ValuePrefix;
_ ->
Value
end.
take_values(DefaultList, List)
when is_list(DefaultList), is_list(List) ->
take_values([], DefaultList, List).
take_values(Result, [], List) ->
lists:reverse(Result) ++ List;
take_values(Result, [{Key, Default} | DefaultList], List) ->
case lists:keytake(Key, 1, List) of
false ->
take_values([Default | Result], DefaultList, List);
{value, {Key, Value}, RemainingList} ->
take_values([Value | Result], DefaultList, RemainingList)
end.
int_to_dec_list(I) when is_integer(I), I >= 0 ->
int_to_dec_list([], I).
int_to_dec_list(L, I)
when I < 10 ->
[int_to_dec(I) | L];
int_to_dec_list(L, I) ->
int_to_dec_list([int_to_dec(I rem 10) | L], I div 10).
int_to_dec_list(I, N, Char) when is_integer(I), I >= 0 ->
int_to_dec_list([], I, 1, N, Char).
int_to_dec_list(L, I, Count, N, Char)
when I < 10 ->
int_to_list_pad([int_to_dec(I) | L], N - Count, Char);
int_to_dec_list(L, I, Count, N, Char) ->
int_to_dec_list([int_to_dec(I rem 10) | L], I div 10, Count + 1, N, Char).
int_to_list_pad(L, 0, _) ->
L;
int_to_list_pad(L, Count, Char) ->
int_to_list_pad([Char | L], Count - 1, Char).
int_to_dec(I) when 0 =< I, I =< 9 ->
I + $0.