Current section

Files

Jump to
opentelemetry src otel_meter_default.erl
Raw

src/otel_meter_default.erl

%%%------------------------------------------------------------------------
%% Copyright 2019, 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_meter_default).
-behaviour(otel_meter).
-behaviour(gen_server).
-export([start_link/1,
new_instrument/4,
new_instruments/2,
lookup_instrument/1,
record/4,
record_batch/3,
%% functions used for bound instruments
record/3,
bind/3,
release/2,
%% observer functions
observer_tab/0,
register_observer/3,
set_observer_callback/3,
observe/3]).
-export([init/1,
handle_call/3,
handle_cast/2]).
-include("otel_meter.hrl").
-include_lib("kernel/include/logger.hrl").
-define(OBSERVER_TAB, otel_metric_accumulator_observers).
-define(TAB, ?MODULE).
-record(state, {}).
start_link(Opts) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []).
-spec new_instrument(opentelemetry:meter(), otel_meter:name(), otel_meter:instrument_kind(), otel_meter:instrument_opts()) -> boolean().
new_instrument(_Meter, Name, InstrumentKind, Opts) ->
gen_server:call(?MODULE, {new, Name, InstrumentKind, Opts}).
%% @doc returns `true' if any instrument in the list is successfully created
-spec new_instruments(opentelemetry:meter(), [otel_meter:instrument_opts()]) -> boolean().
new_instruments(_Meter, List) ->
gen_server:call(?MODULE, {new, List}).
-spec record(opentelemetry:meter(), bound_instrument(), number()) -> ok.
record(_Meter, unknown_instrument, Number) when is_number(Number) ->
ok;
record(_Meter, BoundInstrument, Number) when is_number(Number) ->
_ = otel_metric_accumulator:record(BoundInstrument, Number),
ok;
record(_, _, _) ->
ok.
-spec record(opentelemetry:meter(), otel_meter:name(), otel_meter:labels(), number()) -> ok.
record(_Meter, Name, LabelSet, Number) when is_number(Number) ->
_ = otel_metric_accumulator:record(Name, LabelSet, Number),
ok;
record(_, _, _, _) ->
ok.
-spec record_batch(opentelemetry:meter(), [{otel_meter:name(), number()}], otel_meter:labels()) -> ok.
record_batch(_Meter, Measures, LabelSet) ->
[otel_metric_accumulator:record(Name, LabelSet, Number) || {Name, Number} <- Measures, is_number(Number)],
ok.
-spec release(opentelemetry:meter(), bound_instrument()) -> ok.
release(_Meter, _BoundInstrument) ->
ok.
-spec bind(opentelemetry:meter(), instrument() | otel_meter:name(), otel_meter:labels())
-> bound_instrument().
bind(_Meter, Instrument=#instrument{}, LabelSet) ->
bind_instrument(Instrument, LabelSet);
bind(_Meter, Name, LabelSet) ->
case lookup_instrument(Name) of
unknown_instrument ->
unknown_instrument;
Instrument ->
bind_instrument(Instrument, LabelSet)
end.
-spec lookup_instrument(otel_meter:name()) -> instrument() | unknown_instrument.
lookup_instrument(Name) ->
case ets:lookup(?TAB, Name) of
[Instrument] ->
Instrument;
[] ->
unknown_instrument
end.
observer_tab() ->
?OBSERVER_TAB.
-spec register_observer(opentelemetry:meter(), otel_meter:name(), otel_observer:callback()) -> ok.
register_observer(_Meter, Name, Callback) ->
case lookup_instrument(Name) of
unknown_instrument ->
unknown_instrument;
Instrument ->
gen_server:call(?MODULE, {register_observer, Name, Instrument, Callback})
end.
-spec set_observer_callback(opentelemetry:meter(), otel_meter:name(), otel_observer:callback())
-> ok | unknown_instrument.
set_observer_callback(_Meter, Name, Callback) ->
case lookup_instrument(Name) of
unknown_instrument ->
unknown_instrument;
Instrument ->
gen_server:call(?MODULE, {register_observer, Name, Instrument, Callback})
end.
-spec observe(instrument(), number(), otel_meter:labels()) -> ok.
observe(ObserverInstrument, Number, LabelSet) when is_number(Number) ->
otel_metric_accumulator:observe(ObserverInstrument, Number, LabelSet),
ok;
observe(_, _, _) ->
ok.
init(_Opts) ->
%% TODO: we do not want to lose instrument and observer tables ever
%% eventually need to have an heir to take them if this process crashes.
%% Another option is to just use persistent_term since these things
%% don't change after creation.
%% ets table is required for other parts to not crash so we create
%% it in init and not in a handle_continue or whatever else
case ets:info(?TAB, name) of
undefined ->
ets:new(?TAB, [named_table,
protected,
{read_concurrency, true},
{keypos, #instrument.name}
]);
_ ->
ok
end,
%% observers are stored in a separate table from other instruments
case ets:info(?OBSERVER_TAB, name) of
undefined ->
_ = ets:new(?OBSERVER_TAB, [named_table,
protected,
{keypos, #observer.name}]);
_ ->
ok
end,
{ok, #state{}}.
handle_call({new, Name, InstrumentKind, Opts}, _From, State) ->
Result = insert_new_instrument(Name, InstrumentKind, Opts),
{reply, Result, State};
handle_call({new, List}, _From, State) ->
Result = insert_new_instruments(List),
{reply, Result, State};
handle_call({register_observer, Name, Instrument, Callback}, _From, State) ->
_ = ets:insert(?OBSERVER_TAB, #observer{name=Name,
instrument={otel_meter_default, Instrument},
callback=Callback}),
{reply, ok, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
%% internal
%% TODO: use a counter ref for `sum' and `mmsc' aggregated
%% instruments with `input_type' `integer'?
bind_instrument(Instrument, LabelSet) ->
otel_metric_accumulator:lookup_active(Instrument, LabelSet).
insert_new_instrument(Name, InstrumentKind, Opts) ->
case instrument(Name, InstrumentKind, Opts) of
{error, _} ->
false;
Instrument ->
insert_new(Instrument)
end.
%% Insert each individually so we can log more useful error messages.
%% Instruments should all be created once at the start of an application
%% so the performance isn't a concern here.
insert_new_instruments(List) when is_list(List) ->
%% use foldl to track if any insert has failed and return true only if
%% none fails
lists:foldl(fun({Name, InstrumentKind, Opts}, Acc) ->
insert_new_instrument(Name, InstrumentKind, Opts) andalso Acc;
(X, _Acc) ->
?LOG_INFO("Unable to create instrument from argument ~p. "
"Format must be {Name, InstrumentKind, Opts}.", [X]),
false
end, true, List);
insert_new_instruments(_) ->
false.
insert_new(Instrument=#instrument{name=Name}) ->
try
ets:insert_new(?TAB, Instrument)
catch
C:T:S ->
?LOG_INFO("Unable to create instrument.", #{instrument_name => Name,
class => C,
exception => T,
stacktrace => S}),
false
end.
instrument(Name, InstrumentKind, InstrumentConfig) ->
%% InstrumentKind must be a module that implements `otel_instrument'
try InstrumentKind:module_info() of
_ ->
#instrument{name=Name,
description=maps:get(description, InstrumentConfig, <<>>),
kind=InstrumentKind,
number_kind=maps:get(number_kind, InstrumentConfig, integer),
unit=maps:get(unit, InstrumentConfig, one),
monotonic=maps:get(monotonic, InstrumentConfig),
synchronous=maps:get(synchronous, InstrumentConfig)}
catch
error:undef ->
?LOG_INFO("Unable to create instrument kind because the kind must be a module.",
#{instrument_name => Name,
instrument_kind => InstrumentKind}),
{error, kind_not_a_module};
error:{badkey, Key} ->
?LOG_INFO("Unable to create instrument because instrument map missing required key.",
#{instrument_name => Name,
missing_key => Key}),
{error, {missing_key, Key}}
end.