%% ------------------------------------------------------------------- %% %% Copyright (c) 2015 Basho Technologies, Inc. All Rights Reserved. %% %% This Source Code Form is subject to the terms of the Mozilla Public %% License, v. 2.0. If a copy of the MPL was not distributed with this %% file, You can obtain one at http://mozilla.org/MPL/2.0/. %% %% ------------------------------------------------------------------- %% @doc Exometer report collector and logger. %% %% This module implements a behavior for collecting reporting data and %% handling it (logging to disk or ets, printing to tty, etc.) %% %% The logger has built-in support for receiving input via UDP, TCP or %% internal Erlang messaging, as well as a plugin API for custom input %% handling. Correspondingly, it has support for output to TTY or ets, as %% well as a plugin API for custom output. %% %% An example of how the logger can be used can be found in %% `test/exometer_test_udp_reporter.erl', which implements a UDP-based %% reporter as well as an input plugin and an output plugin. This reporter %% is used by `test/exometer_report_SUITE.erl'. %% %% Loggers can be combined, e.g. by creating one logger that receives Erlang %% messages, and other loggers that receive from different sources, prefix %% their reports and pass them on to the first logger. %% %%

Input plugins

%% %% An input plugin is initiated by `Module:logger_init_input(State)', where %% `State' is whatever was given as a `state' option (default: `undefined'). %% The function must create a process and return `{ok, Pid}'. `Pid' is %% responsible for setting up whatever input channel is desired, and passes %% on incoming data to the logger via Erlang messages `{plugin, Pid, Data}'. %% %%

Output Chaining

%% %% Each incoming data item is passed through the list of output operators. %% Each output operator is able to modify the data (the `tty' and `ets' %% operators leave the data unchanged). Output plugins receive the data %% in `Module:logger_handle_data(Data, State)', which must return %% `{NewData, NewState}'. The state is private to the plugin, while `NewData' %% will be passed along to the next output operator. %% %%

Flow control

%% %% The logger will handle flow control automatically for `udp' and `tcp' %% inputs. If `{active,once}' or `{active, false}', the logger will trigger %% `{active, once}' each time it has handled an incoming message. %% If `{active, N}', it will "refill" the port each time it receives an %% indication that it has become passive. %% %% Input plugins create a process in `Module:logger_init_input/1'. This process %% can mimick the behavior of Erlang ports by sending a `{plugin_passive, Pid}' %% message to the logger. The logger will reply with a message, %% `{plugin_active, N}', where `N' is the value given by the `active' option. %% @end -module(exometer_report_logger). -behaviour(gen_server). -export([new/1]). -export([start_link/1]). -export([info/0, info/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(st, {id, input, output}). -include_lib("parse_trans/include/exprecs.hrl"). -export_records([tcp, udp, tty, ets, int]). %% input records -record(tcp, {socket, port, options = [], active = once}). -record(udp, {socket, port, options = [], active = true}). %% output records -record(tty, {prefix = []}). -record(ets, {tab}). %% both input and output -record(int, {process}). -record(plugin, {module, mod_state, process, active = once}). -type proplist() :: [{atom(), any()}]. -type logger_info() :: {id, any()} | {input, proplist()} | {output, proplist()}. -type plugin_state() :: any(). -callback logger_init_input(any()) -> {ok, pid()}. -callback logger_init_output(any()) -> {ok, plugin_state()}. -callback logger_handle_data(binary(), plugin_state()) -> {binary(), plugin_state()}. -spec new([{id, any()} | {input, list()} | {output, list()}]) -> {ok,pid()}. %% @doc Create a new logger instance. %% %% This function creates a logger process with the given input and output %% parameters. %% %% * `{id, ID}' is mainly for documentation and simplifying identification %% of instances returned by {@link info/0}. %% * `{input, PropList}' specifies what the logger listens to. Only the first %% `input' entry is regarded, but the option is mandatory. %% * `{output, PropList}' specifies what the logger should to with received %% data. Multiple `output' entries are allowed, and they will be processed %% in the order given. %% %% Valid input options: %% %% * `{mode, udp | tcp | internal | plugin}' defines the protocol %% * `{active, false | true | once | N}' provides flow control. Default: `true'. %% * (mode-specific options) %% %% Valid output options: %% %% * `{mode, tty | ets | plugin | internal}' defines output types %% * (output-specific options) %% %% Mode-specific options, `udp': %% %% * `{port, integer()}' - UDP port number %% * `{options, list()}' - Options to pass to {@link gen_udp:open/2} %% %% Mode-specific options, `tcp': %% %% * `{port, integer()}' - TCP port number %% * `{options, list()}' - Options to pass to {@link gen_tcp:listen/2} %% %% Mode-specific options, `tty': %% %% * `{prefix, iolist()}' - Prefix string inserted before the data, which is %% printed as-is (note that any delimiter would need to be part of the prefix) %% %% Mode-specific options, `ets': %% * `{table, ets:table()}' - Ets table identifier. If not specified, an %% ordered-set table will be created by the logger process. The incoming %% data will be inserted as `{os:timestamp(), Data}'. %% %% Mode-specific options, `internal': %% * `{process, PidOrRegname}' specifies another logger instance, which is to %% receive data from this logger (if used in output), or which is allowed %% to send to this logger (if used in input). If no process is given for %% input, any process can send data (on the form %% `{exometer_report_logger, Pid, Data}') to this logger. %% %% Mode-specific options, `plugin': %% %% * `{module, Module}' - name of callback module %% (behaviour: `exometer_report_logger') %% * `{state, State}' - Passed as initial argument to %% `Module:logger_init_input/1' or `Module:logger_init_output/1', depending %% on whether the plugin is specified as input or output. %% @end new(Options) -> supervisor:start_child(exometer_report_logger_sup, [Options]). -spec start_link(proplist()) -> {ok, pid()}. %% @doc Start function for logger instance. %% %% This function is the start function eventually called as a result from %% {@link new/1}, but whereas `new/1' creates a supervised instance, this %% function simply creates the process. It would normally not be used directly. %% @end start_link(Options) -> ID = exometer_util:get_opt(id, Options, undefined), Input = get_input(Options), Output = get_output(Options), gen_server:start_link(?MODULE, {ID, Input, Output}, []). -spec info() -> [{pid(), [logger_info()]}]. %% @doc List active logger instances. %% %% This function lists the instances started via {@link new/1}, along with their %% respective settings as nested property lists. %% @end info() -> [{P, info(P)} || {_, P, _, _} <- supervisor:which_children( exometer_report_logger_sup)]. -spec info(pid()) -> [logger_info()]. %% @doc Lists the settings of a given logger instance. info(P) -> gen_server:call(P, info). %% Client-side check get_input(Opts) -> L = exometer_util:get_opt(input, Opts), get_input(exometer_util:get_opt(mode, L), L). get_input(tcp, L) -> Port = exometer_util:get_opt(port, L), Options = exometer_util:get_opt(options, L, []), Active = exometer_util:get_opt( active, L, get_opt_active(Options)), #tcp{port = Port, options = Options, active = Active}; get_input(udp, L) -> Port = exometer_util:get_opt(port, L), Options = exometer_util:get_opt(options, L, []), Active = exometer_util:get_opt( active, L, get_opt_active(Options)), #udp{port = Port, options = Options, active = Active}; get_input(internal, L) -> P = exometer_util:get_opt(process, L, undefined), #int{process = P}; get_input(plugin, L) -> Mod = exometer_util:get_opt(module, L), St = exometer_util:get_opt(state, L, undefined), Active = exometer_util:get_opt(active, L, true), #plugin{module = Mod, mod_state = St, active = Active}. get_opt_active(Opts) -> case lists:keyfind(active, 1, Opts) of {_, true} -> true; {_, N} when is_integer(N) -> N; _ -> once end. %% Client-side check get_output(Opts) -> [get_output(exometer_util:get_opt(mode, O), O) || {output, O} <- Opts]. get_output(tty, O) -> Prefix = exometer_util:get_opt(prefix, O, []), #tty{prefix = Prefix}; get_output(ets, O) -> Tab = exometer_util:get_opt(tab, O, undefined), #ets{tab = Tab}; get_output(internal, O) -> P = exometer_util:get_opt(process, O), #int{process = P}; get_output(plugin, O) -> Mod = exometer_util:get_opt(module, O), St = exometer_util:get_opt(state, O, undefined), #plugin{module = Mod, mod_state = St}. %% Gen_server callbacks %% @private init({ID, Input, Output}) -> NewL = init_input(Input), NewO = init_output(Output), {ok, #st{id = ID, input = NewL, output = NewO}}. %% @private handle_call(info, _, #st{id = ID, input = I, output = O} = S) -> {reply, info_(ID, I, O), S}; handle_call(_Req, _From, St) -> {reply, {error, unsupported}, St}. %% @private handle_cast({socket, Socket}, #st{input = L} = S) -> case L of #tcp{active = N} = T -> case inet:getopts(Socket, [active]) of {ok, [{active, false}]} -> inet:setopts(Socket, [{active, N}]); _ -> ok end, {noreply, S#st{input = T#tcp{socket = Socket}}}; _ -> {noreply, S} end; handle_cast(_Msg, St) -> {noreply, St}. %% @private handle_info({tcp, Socket, Data}, #st{input = #tcp{socket = Socket, active = Active}, output = Out} = S) -> handle_data(Data, Out), check_active(Socket, Active), {noreply, S}; handle_info({udp, Socket, _Host, _Port, Data}, #st{input = #udp{socket = Socket}, output = Out} = S) -> Out1 = handle_data(Data, Out), {noreply, S#st{output = Out1}}; handle_info({plugin, Pid, Data}, #st{input = #plugin{process = Pid}, output = Out} = S) -> Out1 = handle_data(Data, Out), {noreply, S#st{output = Out1}}; handle_info({tcp_passive, Socket}, #st{input = #tcp{socket = Socket, active = Active}} = S) -> inet:setopts(Socket, Active), {noreply, S}; handle_info({udp_passive, Socket}, #st{input = #udp{socket = Socket, active = Active}} = S) -> inet:setopts(Socket, Active), {noreply, S}; handle_info({plugin_passive, Pid}, #st{input = #plugin{process = Pid, active = Active}} = S) -> Pid ! {plugin_active, Active}, {noreply, S}; handle_info({?MODULE, P, Data}, #st{input = #int{process = Pl}, output = Out} = S) when Pl =:= P; Pl =:= undefined -> Out1 = handle_data(Data, Out), {noreply, S#st{output = Out1}}; handle_info(_, S) -> {noreply, S}. %% @private terminate(_, _) -> ok. %% @private code_change(_FromVsn, S, _Extra) -> {ok, S}. %% End gen_server callbacks info_(ID, I, O) -> [{id, ID}, {input, ensure_list(pp(I))}, {output, ensure_list(pp(O))}]. ensure_list(I) when is_tuple(I) -> [I]; ensure_list(I) when is_list(I) -> I. %% Copied from git:uwiger/jobs/src/jobs_info.erl pp(L) when is_list(L) -> [pp(X) || X <- L]; pp(X) -> case '#is_record-'(X) of true -> RecName = element(1,X), {RecName, lists:zip( '#info-'(RecName,fields), pp(tl(tuple_to_list(X))))}; false -> if is_tuple(X) -> list_to_tuple(pp(tuple_to_list(X))); true -> X end end. init_input(#tcp{port = Port, options = Opts} = T) -> _ = spawn_tcp_acceptor(Port, Opts), T; init_input(#udp{port = Port, options = Opts} = U) -> {ok, Socket} = gen_udp:open(Port, Opts), U#udp{socket = Socket}; init_input(#plugin{module = Mod, mod_state = St} = P) -> case Mod:logger_init_input(St) of {ok, Pid} when is_pid(Pid) -> P#plugin{process = Pid} end; init_input(#int{} = I) -> I. spawn_tcp_acceptor(Port, Opts) -> Parent = self(), spawn_link(fun() -> {ok, LSock} = gen_tcp:listen(Port, Opts), {ok, Socket} = gen_tcp:accept(LSock), ok = gen_tcp:controlling_process(Socket, Parent), gen_server:cast(Parent, {socket, Socket}) end). init_output(Out) -> [init_output_(O) || O <- Out]. init_output_(#tty{} = TTY) -> TTY; init_output_(#int{} = Int) -> Int; init_output_(#ets{tab = T} = E) -> Tab = case T of undefined -> ets:new(?MODULE, [ordered_set]); _ -> T end, E#ets{tab = Tab}; init_output_(#plugin{module = Mod, mod_state = St} = P) -> {ok, St1} = Mod:logger_init_output(St), P#plugin{mod_state = St1}. check_active(Socket, once) -> inet:setopts(Socket, [{active, once}]); check_active(_Socket, _) -> ok. handle_data(Data, Out) -> handle_data(Data, Out, []). handle_data(Data, [H|T], Acc) -> {Data1, H1} = handle_data_(Data, H), handle_data(Data1, T, [H1|Acc]); handle_data(_, [], Acc) -> lists:reverse(Acc). handle_data_(Data, #tty{prefix = Pfx} = Out) -> io:fwrite(iolist_to_binary([Pfx, Data, $\n])), {Data, Out}; handle_data_(Data, #ets{tab = T} = Out) -> ets:insert(T, {os:timestamp(), Data}), {Data, Out}; handle_data_(Data, #int{process = P} = Out) -> try P ! {?MODULE, self(), Data} catch _:_ -> error end, {Data, Out}; handle_data_(Data, #plugin{module = Mod, mod_state = ModSt} = Out) -> {Data1, ModSt1} = Mod:logger_handle_data(Data, ModSt), {Data1, Out#plugin{mod_state = ModSt1}}.