Current section

Files

Jump to
opentelemetry_experimental src otel_log_handler.erl
Raw

src/otel_log_handler.erl

%%%------------------------------------------------------------------------
%% Copyright 2022, OpenTelemetry Authors
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% @doc
%% @end
%%%-------------------------------------------------------------------------
-module(otel_log_handler).
-behaviour(gen_statem).
-include_lib("kernel/include/logger.hrl").
-include_lib("opentelemetry_api/include/opentelemetry.hrl").
-export([start_link/2]).
-export([log/2,
adding_handler/1,
removing_handler/1,
changing_config/3,
filter_config/1,
report_cb/1]).
-export([init/1,
callback_mode/0,
idle/3,
exporting/3,
handle_event/3]).
-type config() :: #{id => logger:handler_id(),
regname := atom(),
config => term(),
level => logger:level() | all | none,
module => module(),
filter_default => log | stop,
filters => [{logger:filter_id(), logger:filter()}],
formatter => {module(), logger:formatter_config()}}.
-define(DEFAULT_CALL_TIMEOUT, 5000).
-define(DEFAULT_MAX_QUEUE_SIZE, 2048).
-define(DEFAULT_SCHEDULED_DELAY_MS, timer:seconds(5)).
-define(DEFAULT_EXPORTER_TIMEOUT_MS, timer:minutes(5)).
-define(name_to_reg_name(Module, Id),
list_to_atom(lists:concat([Module, "_", Id]))).
-record(data, {exporter :: {module(), term()} | undefined,
exporter_config :: {module(), term()} | undefined,
resource :: otel_resource:t(),
runner_pid :: pid() | undefined,
max_queue_size :: integer() | infinity,
exporting_timeout_ms :: integer(),
scheduled_delay_ms :: integer(),
config :: #{},
batch :: #{opentelemetry:instrumentation_scope() => [logger:log_event()]}}).
start_link(RegName, Config) ->
gen_statem:start_link({local, RegName}, ?MODULE, [RegName, Config], []).
-spec adding_handler(Config) -> {ok, Config} | {error, Reason} when
Config :: config(),
Reason :: term().
adding_handler(#{id := Id,
module := Module}=Config) ->
RegName = ?name_to_reg_name(Module, Id),
ChildSpec =
#{id => Id,
start => {?MODULE, start_link, [RegName, Config]},
restart => temporary,
shutdown => 2000,
type => worker,
modules => [?MODULE]},
case supervisor:start_child(opentelemetry_experimental_sup, ChildSpec) of
{ok, _Pid} ->
%% ok = logger_handler_watcher:register_handler(Name,Pid),
%% OlpOpts = logger_olp:get_opts(Olp),
{ok, Config#{regname => RegName}};
{error, {Reason, Ch}} when is_tuple(Ch), element(1, Ch) == child ->
{error, Reason};
{error, _Reason}=Error ->
Error
end.
%%%-----------------------------------------------------------------
%%% Updating handler config
-spec changing_config(SetOrUpdate, OldConfig, NewConfig) ->
{ok,Config} | {error,Reason} when
SetOrUpdate :: set | update,
OldConfig :: config(),
NewConfig :: config(),
Config :: config(),
Reason :: term().
changing_config(SetOrUpdate, OldConfig, NewConfig=#{regname := Id}) ->
gen_statem:call(Id, {changing_config, SetOrUpdate, OldConfig, NewConfig}).
%%%-----------------------------------------------------------------
%%% Handler being removed
-spec removing_handler(Config) -> ok when
Config :: config().
removing_handler(Config=#{regname := Id}) ->
gen_statem:call(Id, {removing_handler, Config}).
%%%-----------------------------------------------------------------
%%% Log a string or report
-spec log(LogEvent, Config) -> ok when
LogEvent :: logger:log_event(),
Config :: config().
log(LogEvent, _Config=#{regname := Id}) ->
Scope = case LogEvent of
#{meta := #{otel_scope := Scope0=#instrumentation_scope{}}} ->
Scope0;
#{meta := #{mfa := {Module, _, _}}} ->
opentelemetry:get_application_scope(Module);
_ ->
opentelemetry:instrumentation_scope(<<>>, <<>>, <<>>)
end,
gen_statem:cast(Id, {log, Scope, LogEvent}).
%%%-----------------------------------------------------------------
%%% Remove internal fields from configuration
-spec filter_config(Config) -> Config when
Config :: config().
filter_config(Config=#{regname := Id}) ->
gen_statem:call(Id, {filter_config, Config}).
init([_RegName, Config]) ->
process_flag(trap_exit, true),
Resource = otel_resource_detector:get_resource(),
SizeLimit = maps:get(max_queue_size, Config, ?DEFAULT_MAX_QUEUE_SIZE),
ExportingTimeout = maps:get(exporting_timeout_ms, Config, ?DEFAULT_EXPORTER_TIMEOUT_MS),
ScheduledDelay = maps:get(scheduled_delay_ms, Config, ?DEFAULT_SCHEDULED_DELAY_MS),
ExporterConfig = maps:get(exporter, Config, {opentelemetry_exporter, #{protocol => grpc}}),
{ok, idle, #data{exporter=undefined,
exporter_config=ExporterConfig,
resource=Resource,
config=Config,
max_queue_size=case SizeLimit of
infinity -> infinity;
_ -> SizeLimit div erlang:system_info(wordsize)
end,
exporting_timeout_ms=ExportingTimeout,
scheduled_delay_ms=ScheduledDelay,
batch=#{}}}.
callback_mode() ->
[state_functions, state_enter].
idle(enter, _OldState, Data=#data{exporter=undefined,
exporter_config=ExporterConfig,
scheduled_delay_ms=SendInterval}) ->
Exporter = init_exporter(ExporterConfig),
{keep_state, Data#data{exporter=Exporter},
[{{timeout, export_logs}, SendInterval, export_logs}]};
idle(enter, _OldState, #data{scheduled_delay_ms=SendInterval}) ->
{keep_state_and_data, [{{timeout, export_logs}, SendInterval, export_logs}]};
idle(_, export_logs, Data=#data{exporter=undefined,
exporter_config=ExporterConfig}) ->
Exporter = init_exporter(ExporterConfig),
{next_state, exporting, Data#data{exporter=Exporter}, [{next_event, internal, export}]};
idle(_, export_logs, Data) ->
{next_state, exporting, Data, [{next_event, internal, export}]};
idle(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
exporting({timeout, export_logs}, export_logs, _) ->
{keep_state_and_data, [postpone]};
exporting(enter, _OldState, _Data) ->
keep_state_and_data;
exporting(internal, export, Data=#data{exporter=Exporter,
resource=Resource,
config=Config,
batch=Batch}) when map_size(Batch) =/= 0 ->
_ = export(Exporter, Resource, Batch, Config),
{next_state, idle, Data#data{batch=#{}}};
exporting(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
handle_event({call, From}, {changing_config, _SetOrUpdate, _OldConfig, NewConfig}, Data) ->
{keep_state, Data#data{config=NewConfig}, [{reply, From, NewConfig}]};
handle_event({call, From}, {removing_handler, Config}, _Data) ->
%% TODO: flush
{keep_state_and_data, [{reply, From, Config}]};
handle_event({call, From}, {filter_handler, Config}, Data) ->
{keep_state, Data, [{reply, From, Config}]};
handle_event({call, From}, {filter_config, Config}, Data) ->
{keep_state, Data, [{reply, From, Config}]};
handle_event({call, _From}, _Msg, _Data) ->
keep_state_and_data;
handle_event(cast, {log, Scope, LogEvent}, Data=#data{batch=Logs}) ->
{keep_state, Data#data{batch=maps:update_with(Scope, fun(V) ->
[LogEvent | V]
end, [LogEvent], Logs)}};
handle_event(_, _, _) ->
keep_state_and_data.
%%
init_exporter(ExporterConfig) ->
case otel_exporter:init(ExporterConfig) of
Exporter when Exporter =/= undefined andalso Exporter =/= none ->
Exporter;
_ ->
undefined
end.
export(undefined, _, _, _) ->
true;
export({ExporterModule, ExporterConfig}, Resource, Batch, Config) ->
%% don't let a exporter exception crash us
%% and return true if exporter failed
try
otel_exporter:export_logs(ExporterModule, {Batch, Config}, Resource, ExporterConfig)
=:= failed_not_retryable
catch
Kind:Reason:StackTrace ->
?LOG_WARNING(#{source => exporter,
during => export,
kind => Kind,
reason => Reason,
exporter => ExporterModule,
stacktrace => StackTrace}, #{report_cb => fun ?MODULE:report_cb/1}),
true
end.
%% logger format functions
report_cb(#{source := exporter,
during := export,
kind := Kind,
reason := Reason,
exporter := ExporterModule,
stacktrace := StackTrace}) ->
{"log exporter threw exception: exporter=~p ~ts",
[ExporterModule, otel_utils:format_exception(Kind, Reason, StackTrace)]}.