Current section
Files
Jump to
Current section
Files
src/opentelemetry.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 The types defined here, and referencing records in opentelemetry.hrl
%% are used to store trace information while being collected on the
%% Erlang node.
%%
%% Thus, while the types are based on protos found in the opentelemetry-proto
%% repo: src/opentelemetry/proto/trace/v1/trace.proto,
%% they are not exact translations because further processing is done after
%% the span has finished and can be vendor specific. For example, there is
%% no count of the number of dropped attributes in the span record. And
%% an attribute's value can be a function to only evaluate the value if it
%% is actually used (at the time of exporting). And the stacktrace is a
%% regular Erlang stack trace.
%% @end
%%%-------------------------------------------------------------------------
-module(opentelemetry).
-export([start_tracer_provider/2,
set_default_tracer/1,
set_default_tracer/2,
create_application_tracers/1,
get_tracer/0,
get_tracer/1,
get_tracer/3,
set_tracer/2,
get_application/1,
get_application_tracer/1,
get_application_scope/1,
set_text_map_propagator/1,
set_text_map_extractor/1,
get_text_map_extractor/0,
set_text_map_injector/1,
get_text_map_injector/0,
instrumentation_scope/3,
instrumentation_library/3,
timestamp/0,
timestamp_to_nano/1,
convert_timestamp/2,
link/1,
link/2,
link/4,
links/1,
event/2,
event/3,
events/1,
status/1,
status/2,
verify_and_set_term/3,
vsn_to_binary/1]).
-include("opentelemetry.hrl").
-include_lib("kernel/include/logger.hrl").
-export_type([tracer/0,
instrumentation_scope/0,
trace_id/0,
span_id/0,
hex_trace_id/0,
hex_span_id/0,
trace_flags/0,
timestamp/0,
span_name/0,
span_ctx/0,
span/0,
span_kind/0,
link/0,
attribute_key/0,
attribute_value/0,
attribute/0,
attributes_map/0,
event/0,
event_name/0,
tracestate/0,
status/0,
status_code/0,
resource/0,
text_map/0]).
-type tracer() :: {module(), term()}.
-type instrumentation_scope() :: #instrumentation_scope{}.
-type trace_id() :: non_neg_integer().
-type span_id() :: non_neg_integer().
-type hex_trace_id() :: binary().
-type hex_span_id() :: binary().
-type trace_flags() :: non_neg_integer().
-type timestamp() :: integer().
-type span_ctx() :: #span_ctx{}.
-type span() :: term().
-type span_name() :: unicode:unicode_binary() | atom().
-type attribute_key() :: unicode:unicode_binary() | atom().
-type attribute_value() :: unicode:unicode_binary() |
atom() |
number() |
boolean() |
[unicode:unicode_binary() | atom() | float() | integer() | boolean()] |
tuple().
-type attribute() :: {attribute_key(), attribute_value()}.
-type attributes_map() :: #{attribute_key() => attribute_value()} |
[attribute()].
-type span_kind() :: ?SPAN_KIND_INTERNAL |
?SPAN_KIND_SERVER |
?SPAN_KIND_CLIENT |
?SPAN_KIND_PRODUCER |
?SPAN_KIND_CONSUMER.
-type event() :: #{system_time_native => integer(),
name := event_name(),
attributes := attributes_map()}.
-type event_name() :: unicode:unicode_binary() | atom().
-type link() :: #{trace_id := trace_id(),
span_id := span_id(),
attributes := attributes_map(),
tracestate := otel_tracestate:t()}.
-type status() :: #status{}.
-type status_code() :: ?OTEL_STATUS_UNSET | ?OTEL_STATUS_OK | ?OTEL_STATUS_ERROR.
%% The key must begin with a lowercase letter, and can only contain
%% lowercase letters 'a'-'z', digits '0'-'9', underscores '_', dashes
%% '-', asterisks '*', and forward slashes '/'.
%% The value is opaque string up to 256 characters printable ASCII
%% RFC0020 characters (i.e., the range 0x20 to 0x7E) except ',' and '='.
%% Note that this also excludes tabs, newlines, carriage returns, etc.
-type tracestate() :: otel_tracestate:t().
-type resource() :: #{unicode:unicode_binary() => unicode:unicode_binary()}.
-type text_map() :: [{unicode:unicode_binary(), unicode:unicode_binary()}].
-define(TRACER_KEY(TracerProvider, Name), {?MODULE, TracerProvider, tracer, Name}).
-define(DEFAULT_TRACER_KEY(TracerProvider), ?TRACER_KEY(TracerProvider, '$__default_tracer')).
-define(MODULE_TO_APPLICATION_KEY, {?MODULE, otel_module_to_application_key}).
-define(TEXT_MAP_EXTRACTOR_KEY, {?MODULE, text_map_extractor}).
-define(TEXT_MAP_INJECTOR_KEY, {?MODULE, text_map_injector}).
%% uncomment when OTP-23 becomes the minimum required version
%% -deprecated({start_tracer_provider, 2, "start the TracerProvider through the SDK"}).
-include("gradualizer.hrl").
%% @deprecated Start the TracerProvider through the SDK"
-spec start_tracer_provider(atom(), map()) -> {ok, pid() | undefined} | {error, term()}.
start_tracer_provider(Name, Config) ->
otel_tracer_provider:start(Name, Config).
-spec set_default_tracer(tracer()) -> boolean().
set_default_tracer(Tracer) ->
set_default_tracer(?GLOBAL_TRACER_PROVIDER_NAME, Tracer).
-spec set_default_tracer(atom(), tracer()) -> boolean().
set_default_tracer(TracerProvider, Tracer) ->
verify_and_set_term(Tracer, ?DEFAULT_TRACER_KEY(TracerProvider), otel_tracer).
-spec set_tracer(atom(), tracer()) -> boolean().
set_tracer(Name, Tracer) ->
set_tracer(Name, <<>>, undefined, Tracer).
-spec set_tracer(Name, Vsn, SchemaUrl, Tracer) -> boolean() when
Name :: atom(),
Vsn :: unicode:chardata() | undefined,
SchemaUrl :: uri_string:uri_string() | undefined,
Tracer:: opentelemetry:tracer().
set_tracer(Name, Vsn, SchemaUrl, Tracer) ->
set_tracer(?GLOBAL_TRACER_PROVIDER_NAME, Name, Vsn, SchemaUrl, Tracer).
-spec set_tracer(TracerProvider, Name, Vsn, SchemaUrl, Tracer) -> boolean() when
TracerProvider :: atom(),
Name :: atom(),
Vsn :: unicode:chardata() | undefined,
SchemaUrl :: uri_string:uri_string() | undefined,
Tracer:: opentelemetry:tracer().
set_tracer(TracerProvider, Name, Vsn, SchemaUrl, Tracer) ->
verify_and_set_term(Tracer, ?TRACER_KEY(TracerProvider, {Name, Vsn, SchemaUrl}), otel_tracer).
-spec create_application_tracers([{Application, Description, Vsn}]) -> ok when
Application :: atom(),
Description :: string(),
Vsn :: string().
create_application_tracers(Applications) ->
TracerMap = lists:foldl(fun({Name, _Description, Version}, Acc) ->
Vsn = vsn_to_binary(Version),
SchemaUrl = application:get_env(Name, otel_schema_url, undefined),
_ = get_tracer(Name, Version, SchemaUrl),
maps:merge(Acc, module_to_application(Name, Vsn, SchemaUrl))
end, #{}, Applications),
persistent_term:put(?MODULE_TO_APPLICATION_KEY, TracerMap).
%% creates a map of modules to application name, version and schema_url tuple
module_to_application(Name, Version, SchemaUrl) ->
{ok, Modules} = application:get_key(Name, modules),
lists:foldl(fun(M, Acc) ->
Acc#{M => {Name, Version, SchemaUrl}}
end, #{}, Modules).
%% @doc Returns the default global tracer.
%%
%% Without the `opentelemetry' application loaded (as a dependency) and
%% started (or another SDK), this function returns the default value
%% `{otel_tracer_noop, []}'.
-spec get_tracer() -> tracer().
get_tracer() ->
get_tracer_(?GLOBAL_TRACER_PROVIDER_NAME).
-spec get_tracer_(atom()) -> tracer().
get_tracer_(TracerProvider) ->
persistent_term:get(?DEFAULT_TRACER_KEY(TracerProvider), {otel_tracer_noop, []}).
%% @doc Returns the tracer for the given name.
%%
%% Passing `{Name, Vsn, SchemaUrl}' is the same as calling
%% `get_tracer(Name, Vsn, SchemaUrl)'.
%% @see get_tracer/3
-spec get_tracer(Name) -> Tracer when
Name :: atom() | {atom(), Vsn, SchemaUrl},
Vsn :: unicode:chardata() | undefined,
SchemaUrl :: uri_string:uri_string() | undefined,
Tracer :: opentelemetry:tracer().
get_tracer('$__default_tracer') ->
get_tracer();
get_tracer({Name, Vsn, SchemaUrl}) ->
get_tracer(Name, Vsn, SchemaUrl);
get_tracer(Name) ->
get_tracer(Name, undefined, undefined).
%% @equiv get_tracer({Name, Vsn, SchemaUrl})
-spec get_tracer(Name, Vsn, SchemaUrl) -> Tracer when
Name :: atom(),
Vsn :: unicode:chardata() | undefined,
SchemaUrl :: uri_string:uri_string() | undefined,
Tracer :: opentelemetry:tracer().
get_tracer(Name, Vsn, SchemaUrl) ->
get_tracer(?GLOBAL_TRACER_PROVIDER_NAME, Name, Vsn, SchemaUrl).
-spec get_tracer(TracerProvider, Name, Vsn, SchemaUrl) -> Tracer when
TracerProvider :: atom() | pid(),
Name :: atom(),
Vsn :: unicode:chardata() | undefined,
SchemaUrl :: uri_string:uri_string() | undefined,
Tracer :: opentelemetry:tracer().
get_tracer(TracerProvider, Name, Vsn, SchemaUrl) ->
%% check cache and then use provider to get the tracer if it isn't cached yet
case persistent_term:get(?TRACER_KEY(TracerProvider, {Name, Vsn, SchemaUrl}), undefined) of
undefined ->
VsnBin = vsn_to_binary(Vsn),
Tracer = otel_tracer_provider:get_tracer(TracerProvider, Name, VsnBin, SchemaUrl),
%% cache the tracer
_ = set_tracer(Name, Vsn, SchemaUrl, Tracer),
Tracer;
Tracer ->
Tracer
end.
-spec get_application_tracer(module()) -> tracer().
get_application_tracer(ModuleName) ->
get_tracer(get_application(ModuleName)).
-spec get_application_scope(module()) -> instrumentation_scope() | undefined.
get_application_scope(ModuleName) ->
case get_application(ModuleName) of
{Name, Vsn, SchemaUrl} ->
instrumentation_scope(Name, Vsn, SchemaUrl);
_ ->
instrumentation_scope(<<>>, <<>>, <<>>)
end.
%% looks up the name, version and schema_url used to create a Trace for the OTP
%% Application a module is in. This name is used to look up a Tracer to use so
%% if none is found for the ModuleName the key used for the default tracer.
-spec get_application(module()) -> ApplicationTuple when
ApplicationTuple :: {Name, Vsn, SchemaUrl} | atom(),
Name :: atom(),
Vsn :: unicode:unicode_binary() | undefined,
SchemaUrl :: uri_string:uri_string() | undefined.
get_application(ModuleName) ->
Map = persistent_term:get(?MODULE_TO_APPLICATION_KEY, #{}),
maps:get(ModuleName, Map, '$__default_tracer').
%% setting the propagator is the same as setting the same injector and extractor
set_text_map_propagator(Propagator) ->
set_text_map_injector(Propagator),
set_text_map_extractor(Propagator).
set_text_map_extractor(Propagator) ->
persistent_term:put(?TEXT_MAP_EXTRACTOR_KEY, Propagator).
set_text_map_injector(Propagator) ->
persistent_term:put(?TEXT_MAP_INJECTOR_KEY, Propagator).
get_text_map_extractor() ->
persistent_term:get(?TEXT_MAP_EXTRACTOR_KEY, otel_propagator_text_map_noop).
get_text_map_injector() ->
persistent_term:get(?TEXT_MAP_INJECTOR_KEY, otel_propagator_text_map_noop).
%% @doc A monotonically increasing time provided by the Erlang runtime system in the native time unit.
%% This value is the most accurate and precise timestamp available from the Erlang runtime and
%% should be used for finding durations or any timestamp that can be converted to a system
%% time before being sent to another system.
%% Use {@link convert_timestamp/2} or {@link timestamp_to_nano/1} to convert a native monotonic time to a
%% system time of either nanoseconds or another {@link erlang:time_unit()}.
%% Using these functions allows timestamps to be accurate, used for duration and be exportable
%% as POSIX time when needed.
%% @end
-spec timestamp() -> integer().
timestamp() ->
erlang:monotonic_time().
%% @doc Convert a native monotonic timestamp to nanosecond POSIX time. Meaning the time since Epoch.
%% Epoch is defined to be 00:00:00 UTC, 1970-01-01.
%% @end
-spec timestamp_to_nano(timestamp()) -> pos_integer().
timestamp_to_nano(Timestamp) ->
?assert_type(convert_timestamp(Timestamp, nanosecond), pos_integer()).
%% @doc Convert a native monotonic timestamp to POSIX time of any {@link erlang:time_unit()}.
%% Meaning the time since Epoch. Epoch is defined to be 00:00:00 UTC, 1970-01-01.
%% @end
-spec convert_timestamp(timestamp(), erlang:time_unit()) -> integer().
convert_timestamp(Timestamp, Unit) ->
Offset = erlang:time_offset(),
erlang:convert_time_unit(Timestamp + Offset, native, Unit).
%% @doc Creates a list of Span links from the given `List'.
%%
%% This is equivalent to calling {@link link/2} or {@link link/4} multiple times.
-spec links([TraceIdAndSpanId | span_ctx() | {span_ctx(), Attributes}]) -> [link()] when
TraceIdAndSpanId :: {trace_id(), span_id(), Attributes, TraceState},
Attributes :: attributes_map(),
TraceState :: otel_tracestate:t() | [{string(), string()}].
links(List) when is_list(List) ->
lists:filtermap(fun({TraceId, SpanId, Attributes, TraceState}) when is_integer(TraceId) ,
is_integer(SpanId) ->
link_or_false(TraceId, SpanId, otel_attributes:process_attributes(Attributes), TraceState);
({#span_ctx{trace_id=TraceId,
span_id=SpanId,
tracestate=TraceState}, Attributes}) when is_integer(TraceId) ,
is_integer(SpanId) ->
link_or_false(TraceId, SpanId, otel_attributes:process_attributes(Attributes), TraceState);
(#span_ctx{trace_id=TraceId,
span_id=SpanId,
tracestate=TraceState}) when is_integer(TraceId) ,
is_integer(SpanId) ->
link_or_false(TraceId, SpanId, [], TraceState);
(_) ->
false
end, List);
links(_) ->
[].
%% @equiv link(SpanCtx, [])
-spec link(span_ctx() | undefined) -> link() | undefined.
link(SpanCtx) ->
link(SpanCtx, []).
%% @doc Creates a Span link to the Span represented by the given `SpanCtx'.
%%
%% The returned link can be used in the `links' field of a Span.
-spec link(span_ctx() | undefined, attributes_map()) -> link() | undefined.
link(_SpanCtx = #span_ctx{trace_id=TraceId,
span_id=SpanId,
tracestate=TraceState}, Attributes) ->
?MODULE:link(TraceId, SpanId, otel_attributes:process_attributes(Attributes), TraceState);
link(_, _) ->
undefined.
%% @doc Creates a Span link to the Span represented by the given `TraceId' and `SpanId'.
%%
%% The returned link can be used in the `links' field of a Span.
-spec link(TraceId, SpanId, Attributes, TraceState) -> link() | undefined when
TraceId :: trace_id(),
SpanId :: span_id(),
Attributes :: attributes_map(),
TraceState :: otel_tracestate:t() | otel_tracestate:members().
link(TraceId, SpanId, Attributes, TraceState) when is_integer(TraceId),
is_integer(SpanId),
(is_list(Attributes) orelse is_map(Attributes)) ->
#{trace_id => TraceId,
span_id => SpanId,
attributes => otel_attributes:process_attributes(Attributes),
tracestate => TraceState};
link(_, _, _, _) ->
undefined.
%% @equiv event(opentelemetry:timestamp(), Name, Attributes)
-spec event(Name, Attributes) -> event() | undefined when
Name :: event_name(),
Attributes :: attributes_map().
event(Name, Attributes) ->
event(opentelemetry:timestamp(), Name, Attributes).
%% @doc Creates a Span event with the given `Name' and `Attributes'.
%%
%% The Span event is marked to have happened at `Timestamp'. The returned
%% event can be used to add an event to a Span through {@link otel_span:add_events/2},
%% for example.
-spec event(Timestamp, Name, Attributes) -> event() | undefined when
Timestamp :: integer(),
Name :: event_name(),
Attributes :: attributes_map().
event(Timestamp, Name, Attributes) when is_integer(Timestamp),
(is_list(Attributes) orelse is_map(Attributes)) ->
case otel_span:is_valid_name(Name) of
true ->
#{system_time_native => Timestamp,
name => Name,
attributes => otel_attributes:process_attributes(Attributes)};
false ->
undefined
end;
event(_, _, _) ->
undefined.
%% @doc Creates a list of Span events from the given `List'.
%%
%% This is a convenience function to create a list of Span events from a list
%% of `{Time, Name, Attributes}' or `{Name, Attributes}' tuples. It's equivalent
%% to calling {@link event/2} or {@link event/3} multiple times. This function
%% also automatically filters out any invalid tuple.
-spec events([Event]) -> [event()] when
Event :: {Timestamp :: integer(), event_name(), attributes_map()} |
{event_name(), attributes_map()}.
events(Events) ->
Now = opentelemetry:timestamp(),
lists:filtermap(fun({Time, Name, Attributes}) ->
case event(Time, Name, Attributes) of
undefined ->
false;
Event ->
{true, Event}
end;
({Name, Attributes}) ->
case event(Now, Name, Attributes) of
undefined ->
false;
Event ->
{true, Event}
end;
(_) ->
false
end, Events).
%% @doc Create a Span status from the given `Code'.
%%
%% The returned status can be used to set the status of a Span through
%% {@link otel_span:set_status/2}, for example.
-spec status(Code) -> status() | undefined when
Code :: status_code().
status(Code) ->
status(Code, <<>>).
%% @doc Create a Span status from the given `Code' and with the given `Message'.
%%
%% The returned status can be used to set the status of a Span through
%% {@link otel_span:set_status/2}, for example.
-spec status(Code, Message) -> status() | undefined when
Code :: status_code(),
Message :: unicode:unicode_binary().
status(?OTEL_STATUS_ERROR, Message) when is_binary(Message) ->
#status{code=?OTEL_STATUS_ERROR, message=Message};
status(?OTEL_STATUS_OK, _Message) ->
#status{code=?OTEL_STATUS_OK};
status(?OTEL_STATUS_UNSET, _Message) ->
#status{code=?OTEL_STATUS_UNSET};
status(_, _) ->
undefined.
%% internal functions
%% @private
-spec verify_and_set_term(module() | {module(), term()}, term(), atom()) -> boolean().
verify_and_set_term(Module, TermKey, Behaviour) ->
case verify_module_exists(Module) of
true ->
persistent_term:put(TermKey, Module),
true;
false ->
?LOG_WARNING("Module ~p does not exist. "
"A noop ~p will be used.",
[Module, Behaviour]),
false
end.
%% @private
-spec verify_module_exists(module() | {module(), term()}) -> boolean().
verify_module_exists({Module, _}) ->
verify_module_exists(Module);
verify_module_exists(Module) ->
try Module:module_info() of
_ ->
true
catch
error:undef ->
false
end.
%% for use in a filtermap
%% return {true, Link} if a link is returned or return false
%% a list is supported for tracestate for backwards compatibility
%% @private
link_or_false(TraceId, SpanId, Attributes, TraceState) when is_list(TraceState) ->
link_or_false(TraceId, SpanId, Attributes, otel_tracestate:new(TraceState));
link_or_false(TraceId, SpanId, Attributes, TraceState) ->
case link(TraceId, SpanId, Attributes, TraceState) of
Link=#{}->
{true, Link};
_ ->
false
end.
instrumentation_scope(Name, Vsn, SchemaUrl) ->
case name_to_binary(Name) of
undefined ->
undefined;
BinaryName ->
#instrumentation_scope{name=BinaryName,
version=vsn_to_binary(Vsn),
schema_url=schema_url_to_binary(SchemaUrl)}
end.
%% this function remains solely to keep backwards compatibility
%% but `instrumentation_scope' should be used instead
instrumentation_library(Name, Vsn, SchemaUrl) ->
instrumentation_scope(Name, Vsn, SchemaUrl).
%% schema_url is option, so set to undefined if its not a string
schema_url_to_binary(SchemaUrl) when is_binary(SchemaUrl) ; is_list(SchemaUrl) ->
unicode:characters_to_binary(SchemaUrl);
schema_url_to_binary(_) ->
undefined.
%% Vsn can't be an atom or anything but a list or binary
%% so return empty binary string if it isn't a list or binary.
%% @private
vsn_to_binary(Vsn) when is_binary(Vsn) ; is_list(Vsn) ->
unicode:characters_to_binary(Vsn);
vsn_to_binary(_) ->
<<>>.
%% Instrumentation name can be atom, list or binary. But atom `undefined'
%% must stay as `undefined' atom.
name_to_binary(undefined)->
undefined;
name_to_binary(T) when is_atom(T) ->
atom_to_binary(T, utf8);
name_to_binary(T) when is_list(T) ->
list_to_binary(T);
name_to_binary(T) when is_binary(T) ->
T.