-
Notifications
You must be signed in to change notification settings - Fork 1
/
journald_sock.erl
141 lines (117 loc) · 4.47 KB
/
journald_sock.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
%% coding: utf8
%% @doc Tiny interface to Journald socket
%%
%% See `man sd_journal_sendv', `man systemd.journal-fields'
%%
%% See [https://github.com/systemd/systemd/blob/v246/src/journal/journal-send.c#L206-L337
%% sd_journal_sendv implementation].
-module(journald_sock).
-export([open/1, close/1, is_handle/1, log/2, format/1]).
-export_type([handle/0, log_msg/0, key/0, value/0]).
-include_lib("kernel/include/file.hrl").
-record(state, {
fd :: gen_udp:socket(),
% see inet:local_address()
path :: file:filename_all()
}).
-type key() :: iodata().
-type value() :: iodata() | integer() | atom().
-type log_msg() ::
#{key() => value()} |
[{key(), value()}].
-opaque handle() :: #state{}.
-define(DEFAULT_SOCKET_PATH, "/run/systemd/journal/socket").
%% @doc Opens journald log socket
-spec open(#{socket_path => file:filename_all()}) -> handle().
open(Opts) ->
Path = maps:get(socket_path, Opts, ?DEFAULT_SOCKET_PATH),
% any better way to check UNIX socket?
{ok, #file_info{type = other}} = file:read_file_info(Path),
{ok, S} = gen_udp:open(0, [
local,
{mode, binary}
]),
#state{
fd = S,
path = Path
}.
%% @doc Closes journald log socket
-spec close(handle()) -> ok.
close(#state{fd = S}) ->
gen_udp:close(S).
%% @doc Returns `true' if the argument is `handle()' and `false' otherwise.
%%
%% Might be usefull to avoid opaqueness violations.
-spec is_handle(any()) -> boolean().
is_handle(#state{}) -> true;
is_handle(_) -> false.
%% @doc Logs a Key - Value message to journald socket
%%
%% Keys have to be `iodata()' and should not contain newlines or `=' signs.
%% Values can be `iodata()', `atom()' or `integer()' and may contain any binaries. utf8 is
%% preferrable
-spec log(log_msg(), handle()) -> ok | {error, inet:posix()}.
log(KV, #state{fd = Fd, path = Path}) ->
Packet = format(KV),
gen_udp_send(Fd, {local, Path}, Packet).
%% @doc Formats a flat Key-Value message to a (non-documented) format acceptable by journald socket
-spec format(log_msg()) -> iodata().
format(Map) when is_map(Map) ->
format(maps:to_list(Map));
format([_ | _] = KV) when is_list(KV) ->
[format_pair(K, V) || {K, V} <- KV].
%% =====================
%% Internal
-if(?OTP_RELEASE >= 22).
gen_udp_send(Fd, Dest, Packet) ->
gen_udp:send(Fd, Dest, Packet).
-else.
gen_udp_send(Fd, Dest, Packet) ->
gen_udp:send(Fd, Dest, 0, Packet).
-endif.
%% Keys supposed to be uppercase, but we are not enforcing that
%% Values are supposed to be valid iolists (eg, lists of binaries and integers in a 0..255 range)
format_pair(K, V) when is_binary(K) orelse is_list(K), is_binary(V) orelse is_list(V) ->
(iolist_size(K) > 0) orelse error(empty_key),
(not has_nl_or_eq(K)) orelse error({newline_or_eq_in_key, K}),
case has_nl(V) of
false ->
[K, "=", V, $\n];
true ->
[K, <<"\n", (iolist_size(V)):64/unsigned-little>>, V, $\n]
end.
has_nl(Subj) ->
has(Subj, <<"\n">>).
has_nl_or_eq(Subj) ->
has(Subj, [<<"\n">>, <<"=">>]).
has(Bin, What) when is_binary(Bin) ->
binary:match(Bin, What) =/= nomatch;
has(IoList, What) ->
re:run(IoList, [$[, What, $]], [{capture, none}, unicode]) =/= nomatch.
-ifdef(EUNIT).
-include_lib("eunit/include/eunit.hrl").
format_test() ->
Samples = [
{<<"A=B\n">>, [{"A", "B"}]},
{<<"A=B\n">>, #{"A" => "B"}},
{<<"A=B\n">>, #{<<"A">> => <<"B">>}},
{<<"A=B\nC=D\n">>, [
{<<"A">>, <<"B">>},
{"C", "D"}
]},
{<<"A=привет\n"/utf8>>, #{"A" => <<"привет"/utf8>>}},
{<<"A\n", 3:64/unsigned-little, "B\nC\n">>, #{<<"A">> => <<"B\nC">>}},
{<<"A\n", 3:64/unsigned-little, "B\nC\n">>, #{<<"A">> => ["B", $\n, <<"C">>]}},
{<<"A\n", 19:64/unsigned-little, "привет\nмир\n"/utf8>>, #{"A" => <<"привет\nмир"/utf8>>}}
],
[?assertEqual(Expect, iolist_to_binary(format(KV)), KV) || {Expect, KV} <- Samples],
?assertError(function_clause, format(#{})),
?assertError(function_clause, format([])),
?assertError(function_clause, format(#{a => b})),
?assertError(function_clause, format(#{"A" => #{}})),
?assertError({newline_or_eq_in_key, "A\nB"}, format(#{"A\nB" => "C"})),
?assertError({newline_or_eq_in_key, "A=B"}, format(#{"A=B" => "C"})),
?assertError({newline_or_eq_in_key, <<"A=B">>}, format(#{<<"A=B">> => "C"})),
?assertError(empty_key, format(#{<<>> => "A"})),
?assertError(empty_key, format(#{[] => "A"})).
-endif.