diff --git a/README.md b/README.md index ec280a32..71e50b92 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,7 @@ OpenTelemetry OpenTelemetry stats collection and distributed tracing framework for Erlang. +## Design + +## Contributing diff --git a/include/opentelemetry.hrl b/include/opentelemetry.hrl new file mode 100644 index 00000000..9617e9b2 --- /dev/null +++ b/include/opentelemetry.hrl @@ -0,0 +1,137 @@ +%%%------------------------------------------------------------------------ +%% 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. +%%%------------------------------------------------------------------------ + +%% These records 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). + +%% for use in guards: sampling bit is the first bit in 8-bit trace options +-define(IS_SPAN_ENABLED(X), (X band 1) =/= 0). + +-define(MESSAGE_EVENT_TYPE_UNSPECIFIED, 'TYPE_UNSPECIFIED'). +-define(MESSAGE_EVENT_TYPE_SENT, 'SENT'). +-define(MESSAGE_EVENT_TYPE_RECEIVED, 'RECEIVED'). + +-define(SPAN_KIND_UNSPECIFIED, 'SPAN_KIND_UNSPECIFIED'). +-define(SPAN_KIND_INTERNAL, 'INTERNAL'). +-define(SPAN_KIND_SERVER, 'SERVER'). +-define(SPAN_KIND_CLIENT, 'CLIENT'). +-define(SPAN_KIND_PRODUCER, 'PRODUCER'). +-define(SPAN_KIND_CONSUMER, 'CONSUMER'). + +-record(span_ctx, { + %% 128 bit int trace id + trace_id :: opentelemetry:trace_id() | undefined, + %% 64 bit int span id + span_id :: opentelemetry:span_id() | undefined, + %% 8-bit integer, lowest bit is if it is sampled + trace_options = 1 :: integer() | undefined, + %% Tracestate represents tracing-system specific context in a list of key-value pairs. + %% Tracestate allows different vendors propagate additional information and + %% inter-operate with their legacy Id formats. + tracestate :: opentelemetry:tracestate() | undefined, + %% IsValid is a boolean flag which returns true if the SpanContext has a non-zero + %% TraceID and a non-zero SpanID. + is_valid :: boolean() | undefined + }). + +-record(span, { + %% name of the span + name :: unicode:unicode_binary(), + + %% 128 bit int trace id + trace_id :: opentelemetry:trace_id() | undefined, + + %% 64 bit int span id + span_id :: opentelemetry:span_id() | undefined, + %% 64 bit int parent span + parent_span_id :: opentelemetry:span_id() | undefined, + + tracestate :: opentelemetry:tracestate() | undefined, + + %% 8-bit integer, lowest bit is if it is sampled + trace_options = 1 :: integer() | undefined, + + kind = ?SPAN_KIND_UNSPECIFIED :: opentelemetry:span_kind() | undefined, + + start_time :: wts:timestamp(), + end_time :: wts:timestamp() | undefined, + + %% A set of attributes on the span. + %% Kept as a list so ets:select_replace/2 can be used to add new elements + attributes = [] :: opentelemetry:attributes() | undefined, + + %% optional stacktrace from where the span was started + stack_trace :: opentelemetry:stack_trace() | undefined, + + %% A time-stamped annotation or message event in the Span. + time_events = [] :: opentelemetry:time_events(), + + %% links to spans in other traces + links = [] :: opentelemetry:links(), + + %% An optional final status for this span. + status :: opentelemetry:status() | undefined, + + %% An optional resource that is associated with this span. If not set, this span + %% should be part of a batch that does include the resource information, unless resource + %% information is unknown. + resource :: opentelemetry:resource() | undefined, + + %% A highly recommended but not required flag that identifies when a trace + %% crosses a process boundary. True when the parent_span belongs to the + %% same process as the current span. + same_process_as_parent_span = undefined :: boolean() | undefined, + + %% An optional number of child spans that were generated while this span + %% was active. If set, allows implementation to detect missing child spans. + child_span_count = undefined :: integer() | undefined + }). + +-record(link, { + trace_id :: opentelemetry:trace_id(), + span_id :: opentelemetry:span_id(), + attributes :: opentelemetry:attributes(), + tracestate :: opentelemetry:tracestate() + }). + +-record(message_event, { + %% type of MessageEvent. Indicates whether the RPC message was sent or received. + type = 'TYPE_UNSPECIFIED' :: opentelemetry:message_event_type(), + + %% identifier for the message, which must be unique in this span. + id :: integer(), + + %% number of uncompressed bytes sent or received + uncompressed_size :: integer(), + + %% number of compressed bytes sent or received + compressed_size :: integer() + }). + +-record(annotation, { + description :: unicode:unicode_binary() | undefined, + attributes :: opentelemetry:attributes() | undefined + }). + +-record(status, { + code :: integer(), + %% developer-facing error message + message :: unicode:unicode_binary() + }). diff --git a/src/opentelemetry.erl b/src/opentelemetry.erl new file mode 100644 index 00000000..a3ea7095 --- /dev/null +++ b/src/opentelemetry.erl @@ -0,0 +1,124 @@ +%%%------------------------------------------------------------------------ +%% 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([generate_trace_id/0, + generate_span_id/0]). + +-include("opentelemetry.hrl"). + +-export_type([trace_id/0, + span_id/0, + span_name/0, + span_ctx/0, + span/0, + span_kind/0, + link/0, + links/0, + attributes/0, + annotation/0, + time_events/0, + message_event/0, + message_event_type/0, + stack_trace/0, + tracestate/0, + status/0, + resource/0, + http_headers/0]). + +-type trace_id() :: non_neg_integer(). +-type span_id() :: non_neg_integer(). + +-type span_ctx() :: #span_ctx{}. +-type span() :: #span{}. +-type span_name() :: unicode:unicode_binary(). + +-type attribute_value() :: any(). +-type attributes() :: [{unicode:unicode_binary(), attribute_value()}]. + +-type annotation() :: #annotation{}. +-type span_kind() :: ?SPAN_KIND_INTERNAL | + ?SPAN_KIND_SERVER | + ?SPAN_KIND_CLIENT. +-type message_event() :: #message_event{}. +-type message_event_type() :: ?MESSAGE_EVENT_TYPE_UNSPECIFIED | + ?MESSAGE_EVENT_TYPE_SENT | + ?MESSAGE_EVENT_TYPE_RECEIVED. +-type time_events() :: [{wts:timestamp(), annotation() | message_event()}]. +-type link() :: #link{}. +-type links() :: [#link{}]. +-type status() :: #status{}. + +%% 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() :: [{unicode:latin1_chardata(), unicode:latin1_chardata()}]. + +-type stack_trace() :: [erlang:stack_item()]. + +-type resource() :: #{unicode:unicode_binary() => unicode:unicode_binary()}. + +-type http_headers() :: [{unicode:unicode_binary(), unicode:unicode_binary()}]. + +%%-------------------------------------------------------------------- +%% @doc +%% Generates a 128 bit random integer to use as a trace id. +%% @end +%%-------------------------------------------------------------------- +-spec generate_trace_id() -> trace_id(). +generate_trace_id() -> + uniform(2 bsl 127 - 1). %% 2 shifted left by 127 == 2 ^ 128 + +%%-------------------------------------------------------------------- +%% @doc +%% Generates a 64 bit random integer to use as a span id. +%% @end +%%-------------------------------------------------------------------- +-spec generate_span_id() -> span_id(). +generate_span_id() -> + uniform(2 bsl 63 - 1). %% 2 shifted left by 63 == 2 ^ 64 + +%% Before OTP-20 rand:uniform could not give precision higher than 2^56. +%% Here we do a compile time check for support of this feature and will +%% combine multiple calls to rand if on an OTP version older than 20.0 +-ifdef(OTP_RELEASE). +uniform(X) -> + rand:uniform(X). +-else. +-define(TWO_POW_56, 2 bsl 55). + +uniform(X) when X =< ?TWO_POW_56 -> + rand:uniform(X); +uniform(X) -> + R = rand:uniform(?TWO_POW_56), + (uniform(X bsr 56) bsl 56) + R. +-endif. diff --git a/src/opentelemetry_app.erl b/src/opentelemetry_app.erl index b57eb0b9..8084b128 100644 --- a/src/opentelemetry_app.erl +++ b/src/opentelemetry_app.erl @@ -22,7 +22,8 @@ -export([start/2, stop/1]). start(_StartType, _StartArgs) -> - opentelemetry_sup:start_link(). + Opts = application:get_all_env(opentelemetry), + opentelemetry_sup:start_link(Opts). stop(_State) -> ok. diff --git a/src/opentelemetry_sup.erl b/src/opentelemetry_sup.erl index 4d746405..22d8b827 100644 --- a/src/opentelemetry_sup.erl +++ b/src/opentelemetry_sup.erl @@ -19,14 +19,14 @@ -behaviour(supervisor). --export([start_link/0]). +-export([start_link/1]). -export([init/1]). -define(SERVER, ?MODULE). -start_link() -> - supervisor:start_link({local, ?SERVER}, ?MODULE, []). +start_link(Opts) -> + supervisor:start_link({local, ?SERVER}, ?MODULE, [Opts]). %% sup_flags() = #{strategy => strategy(), % optional %% intensity => non_neg_integer(), % optional @@ -37,11 +37,12 @@ start_link() -> %% shutdown => shutdown(), % optional %% type => worker(), % optional %% modules => modules()} % optional -init([]) -> - SupFlags = #{strategy => one_for_all, +init([Opts]) -> + SupFlags = #{strategy => one_for_one, intensity => 0, period => 1}, - ChildSpecs = [], + ChildSpecs = [#{id => ot_span_ets, + start => {ot_span_ets, start_link, [Opts]}}], {ok, {SupFlags, ChildSpecs}}. %% internal functions diff --git a/src/ot_ctx.erl b/src/ot_ctx.erl new file mode 100644 index 00000000..eddca048 --- /dev/null +++ b/src/ot_ctx.erl @@ -0,0 +1,22 @@ +%%%------------------------------------------------------------------------ +%% 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(ot_ctx). + +-callback get(any()) -> any(). +-callback with_value(any(), any()) -> ok. +-callback with_value(any(), any(), fun()) -> ok. diff --git a/src/ot_ctx_pdict.erl b/src/ot_ctx_pdict.erl new file mode 100644 index 00000000..03568233 --- /dev/null +++ b/src/ot_ctx_pdict.erl @@ -0,0 +1,39 @@ +%%%------------------------------------------------------------------------ +%% 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(ot_ctx_pdict). + +-behaviour(ot_ctx). + +-export([with_value/2, + with_value/3, + get/1]). + +get(Key) -> + erlang:get(Key). + +with_value(Key, Value) -> + erlang:put(Key, Value). + +with_value(Key, Value, Fun) -> + Orig = erlang:get(Key), + try + erlang:put(Key, Value), + Fun() + after + erlang:put(Key, Orig) + end. diff --git a/src/ot_ctx_seqtrace.erl b/src/ot_ctx_seqtrace.erl new file mode 100644 index 00000000..9b7f397a --- /dev/null +++ b/src/ot_ctx_seqtrace.erl @@ -0,0 +1,36 @@ +%%%------------------------------------------------------------------------ +%% 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(ot_ctx_seqtrace). + +-behaviour(ot_ctx). + +-export([get/1, + with_value/2, + with_value/3]). + +-spec get(any()) -> any(). +get(_Key) -> + ok. + +-spec with_value(any(), any()) -> ok. +with_value(_Key, _Value) -> + ok. + +-spec with_value(any(), any(), fun()) -> ok. +with_value(_Key, _Value, Fun) -> + Fun(). diff --git a/src/ot_span.erl b/src/ot_span.erl new file mode 100644 index 00000000..251e33e9 --- /dev/null +++ b/src/ot_span.erl @@ -0,0 +1,122 @@ +%%%------------------------------------------------------------------------ +%% 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 +%% Functional interface for span record and callbacks for implementations of +%% span storage to follow. +%% @end +%%%------------------------------------------------------------------------- +-module(ot_span). + +-export([start_span/1, + start_span/2, + end_span/1]). + +-include("opentelemetry.hrl"). + +-type start_opts() :: #{parent => undefined | opentelemetry:span() | opentelemetry:span_ctx(), + sampler => module(), + links => opentelemetry:links(), + is_recorded => boolean(), + kind => opentelemetry:span_kind()}. + +-export_type([start_opts/0]). + +-callback start_span(opentelemetry:span_name(), start_opts()) -> opentelemetry:span(). +-callback get_ctx(opentelemetry:span()) -> opentelemetry:span_ctx(). +-callback is_recording_events(opentelemetry:span_ctx()) -> boolean(). +-callback set_attributes(opentelemetry:span_ctx(), opentelemetry:attributes()) -> ok. +-callback add_events(opentelemetry:span_ctx(), opentelemetry:time_events()) -> ok. +-callback add_links(opentelemetry:span_ctx(), opentelemetry:links()) -> ok. +-callback set_status(opentelemetry:span_ctx(), opentelemetry:status()) -> ok. +-callback update_name(opentelemetry:span_ctx(), opentelemetry:span_name()) -> ok. +-callback finish_span(opentelemetry:span_ctx()) -> ok. + +%% sampling bit is the first bit in 8-bit trace options +-define(IS_ENABLED(X), (X band 1) =/= 0). + +-spec start_span(opentelemetry:span_name()) -> {opentelemetry:span_ctx(), opentelemetry:span()}. +start_span(Name) -> + start_span(Name, #{}). + +-spec start_span(opentelemetry:span_name(), start_opts()) + -> {opentelemetry:span_ctx(), opentelemetry:span() | undefined}. +start_span(Name, Opts) -> + Parent = maps:get(parent, Opts, undefined), + Attributes = maps:get(attributes, Opts, []), + Links = maps:get(links, Opts, []), + Kind = maps:get(kind, Opts, ?SPAN_KIND_UNSPECIFIED), + + %% TODO: support overriding the sampler + _Sampler = maps:get(sampler, Opts, undefined), + + new_span_(Name, Parent, Kind, Attributes, Links). + +%% if parent is undefined, first run sampler +new_span_(Name, undefined, Kind, Attributes, Links) -> + TraceId = opentelemetry:generate_trace_id(), + Span = #span_ctx{trace_id=TraceId, + trace_options=0}, + TraceOptions = update_trace_options(should_sample, Span), + new_span_(Name, Span#span_ctx{trace_options=TraceOptions}, Kind, Attributes, Links); +%% if parent is remote, first run sampler +%% new_span_(Name, Span=#span_ctx{}, Kind, Attributes) %% when RemoteParent =:= true +%% -> +%% TraceOptions = update_trace_options(should_sample, Span), +%% new_span_(Name, Span#span_ctx{trace_options=TraceOptions}, Kind, Attributes); +new_span_(Name, Parent=#span_ctx{trace_id=TraceId, + trace_options=TraceOptions, + tracestate=Tracestate, + span_id=ParentSpanId}, Kind, Attributes, Links) + when ?IS_ENABLED(TraceOptions) -> + SpanId = opentelemetry:generate_span_id(), + Span = #span{trace_id=TraceId, + span_id=SpanId, + tracestate=Tracestate, + start_time=wts:timestamp(), + parent_span_id=ParentSpanId, + kind=Kind, + name=Name, + attributes=Attributes, + links=Links}, + {Parent#span_ctx{span_id=SpanId}, Span}; +new_span_(_Name, Parent, _Kind, _, _) -> + SpanId = opentelemetry:generate_span_id(), + %% since discarded by sampler, create no span + {Parent#span_ctx{span_id=SpanId}, undefined}. + +%% +update_trace_options(should_sample, #span_ctx{trace_id=_TraceId, + span_id=_ParentSpanId, + trace_options=_ParentTraceOptions}) -> + 1. + %% case oc_sampler:should_sample(TraceId, ParentSpanId, ?IS_ENABLED(ParentTraceOptions)) of + %% true -> + %% 1; + %% false -> + %% 0 + %% end. + +%%-------------------------------------------------------------------- +%% @doc +%% Set the end time for a span if it hasn't been set before. +%% @end +%%-------------------------------------------------------------------- +-spec end_span(opentelemetry:span()) -> opentelemetry:span(). +end_span(Span=#span{end_time=undefined, + trace_options=TraceOptions}) when ?IS_ENABLED(TraceOptions) -> + EndTime = wts:timestamp(), + Span#span{end_time=EndTime}; +end_span(Span) -> + Span. diff --git a/src/ot_span_ets.erl b/src/ot_span_ets.erl new file mode 100644 index 00000000..fb1ef8b4 --- /dev/null +++ b/src/ot_span_ets.erl @@ -0,0 +1,96 @@ +%%%------------------------------------------------------------------------ +%% 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 +%% ETS backed interface for working with spans. +%% @end +%%%------------------------------------------------------------------------- +-module(ot_span_ets). + +-behaviour(gen_server). + +-export([start_link/1, + init/1, + handle_call/3, + handle_cast/2]). + +-export([start_span/1, + start_span/2, + finish_span/1]). + +-include("opentelemetry.hrl"). + +-record(state, {}). + +%% table to store active spans +-define(SPAN_TAB, otel_span_table). + +start_link(Opts) -> + gen_server:start_link(?MODULE, Opts, []). + +%% @equiv start_span(Name, #{}) +-spec start_span(opentelemetry:span_name()) -> opentelemetry:span_ctx(). +start_span(Name) -> + start_span(Name, #{}). + + +%% @doc Start a span and insert into the active span ets table. +-spec start_span(opentelemetry:span_name(), ot_span:start_opts()) -> opentelemetry:span_ctx(). +start_span(Name, Opts) -> + {SpanCtx, Span} = ot_span:start_span(Name, Opts), + _ = storage_insert(Span), + SpanCtx. + +%% @doc Finish a span based on its context and send to reporter. +-spec finish_span(opentelemetry:span_ctx()) -> ok. +finish_span(#span_ctx{span_id=SpanId, + tracestate=Tracestate, + trace_options=TraceOptions}) when ?IS_SPAN_ENABLED(TraceOptions) -> + case ets:take(?SPAN_TAB, SpanId) of + [Span] -> + _Span1 = ot_span:end_span(Span#span{tracestate=Tracestate}), + %% oc_reporter:store_span(Span1), + ok; + _ -> + ok + end; +finish_span(_) -> + ok. + +%% + +storage_insert(undefined) -> + ok; +storage_insert(Span) -> + ets:insert(?SPAN_TAB, Span). + +init(_Opts) -> + %% 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(?SPAN_TAB, name) of + undefined -> + ets:new(?SPAN_TAB, [named_table, public, + {write_concurrency, true}, + {keypos, #span.span_id}]); + _ -> + ok + end, + + {ok, #state{}}. + +handle_call(_Msg, _From, State) -> + {noreply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. diff --git a/src/ot_tracer.erl b/src/ot_tracer.erl new file mode 100644 index 00000000..ff71d93f --- /dev/null +++ b/src/ot_tracer.erl @@ -0,0 +1,66 @@ +%%%------------------------------------------------------------------------ +%% 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(ot_tracer). + +-export([start_span/1, + start_span/2, + with_span/1, + current_span_ctx/0, + finish/0, + get_binary_format/0, + get_http_text_format/0]). + +-include("opentelemetry.hrl"). + +-define(tracer, (persistent_term:get({opentelemetry, tracer}, ot_tracer_sdk))). + +-callback start_span(opentelemetry:span_name(), ot_span:start_opts()) -> opentelemetry:span_ctx(). +-callback with_span(opentelemetry:span_ctx()) -> ok. +-callback finish() -> ok. +-callback current_span_ctx() -> opentelemetry:span_ctx(). +%% -callback get_current_span() -> opentelemetry:span(). +-callback get_binary_format() -> binary(). +-callback get_http_text_format() -> opentelemetry:http_headers(). + +-spec start_span(opentelemetry:span_name()) -> opentelemetry:span_ctx(). +start_span(Name) -> + start_span(Name, #{}). + +-spec start_span(opentelemetry:span_name(), ot_span:start_opts()) -> opentelemetry:span_ctx(). +start_span(Name, Opts) -> + ?tracer:start_span(Name, Opts). + +-spec with_span(opentelemetry:span_ctx()) -> ok. +with_span(Span) -> + ?tracer:with_span(Span). + +-spec finish() -> ok. +finish() -> + ?tracer:finish(). + +-spec current_span_ctx() -> opentelemetry:span_ctx(). +current_span_ctx() -> + ?tracer:current_span_ctx(). + +-spec get_binary_format() -> binary(). +get_binary_format() -> + ?tracer:get_binary_format(). + +-spec get_http_text_format() -> opentelemetry:http_headers(). +get_http_text_format() -> + ?tracer:get_http_text_format(). diff --git a/src/ot_tracer_noop.erl b/src/ot_tracer_noop.erl new file mode 100644 index 00000000..7a957d30 --- /dev/null +++ b/src/ot_tracer_noop.erl @@ -0,0 +1,65 @@ +%%%------------------------------------------------------------------------ +%% 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(ot_tracer_noop). + +-behaviour(ot_tracer). + +-export([start_span/2, + with_span/1, + with_span/2, + finish/0, + current_span_ctx/0, + get_binary_format/0, + get_http_text_format/0]). + +-include("opentelemetry.hrl"). + +-spec start_span(opentelemetry:span_name(), ot_span:start_opts()) -> opentelemetry:span_ctx(). +start_span(_Name, _) -> + #span_ctx{trace_id=0, + span_id=0, + trace_options=0, + tracestate=[], + is_valid=false}. + +-spec with_span(opentelemetry:span_ctx()) -> ok. +with_span(_SpanCtx) -> + ok. + +-spec with_span(opentelemetry:span_ctx(), fun()) -> ok. +with_span(_SpanCtx, _) -> + ok. + +-spec current_span_ctx() -> opentelemetry:span_ctx(). +current_span_ctx() -> + #span_ctx{trace_id=0, + span_id=0, + trace_options=0, + tracestate=[]}. + +-spec finish() -> ok. +finish() -> + ok. + +-spec get_binary_format() -> binary(). +get_binary_format() -> + <<>>. + +-spec get_http_text_format() -> opentelemetry:http_headers(). +get_http_text_format() -> + []. diff --git a/src/ot_tracer_sdk.erl b/src/ot_tracer_sdk.erl new file mode 100644 index 00000000..116d39a9 --- /dev/null +++ b/src/ot_tracer_sdk.erl @@ -0,0 +1,90 @@ +%%%------------------------------------------------------------------------ +%% 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(ot_tracer_sdk). + +-behaviour(ot_tracer). + +-export([start_span/2, + with_span/1, + with_span/2, + finish/0, + current_span_ctx/0, + get_binary_format/0, + get_http_text_format/0]). + +-define(SPAN_CTX, ot_span_ctx_key). + +-type pdict_trace_ctx() :: {opentelemetry:span_ctx(), pdict_trace_ctx() | undefined}. + +-spec start_span(opentelemetry:span_name(), ot_span:start_opts()) -> opentelemetry:span_ctx(). +start_span(Name, Opts) -> + case ot_ctx_pdict:get(?SPAN_CTX) of + {SpanCtx, _}=Ctx -> + SpanCtx1 = ot_span_ets:start_span(Name, Opts#{parent => SpanCtx}), + ot_ctx_pdict:with_value(?SPAN_CTX, {SpanCtx1, Ctx}), + SpanCtx1; + _ -> + SpanCtx = ot_span_ets:start_span(Name, Opts#{parent => undefined}), + ot_ctx_pdict:with_value(?SPAN_CTX, {SpanCtx, undefined}), + SpanCtx + end. + +-spec with_span(opentelemetry:span_ctx()) -> ok. +with_span(SpanCtx) -> + ot_ctx_pdict:with_value(?SPAN_CTX, {SpanCtx, undefined}). + +-spec with_span(opentelemetry:span_ctx(), fun()) -> ok. +with_span(SpanCtx, Fun) -> + ot_ctx_pdict:with_value(?SPAN_CTX, {SpanCtx, undefined}, Fun). + +-spec current_span_ctx() -> opentelemetry:span_ctx(). +current_span_ctx() -> + case ot_ctx_pdict:get(?SPAN_CTX) of + {SpanCtx, _ParentPdictSpanCtx} -> + SpanCtx; + _ -> + undefined + end. + +%% Internal function that returns the current trace context. +%% The pdict ctx stores both the current span ctx and the +%% parent trace context, which contains its parent and so on. +-spec current_ctx() -> pdict_trace_ctx(). +current_ctx() -> + ot_ctx_pdict:get(?SPAN_CTX). + +%%-------------------------------------------------------------------- +%% @doc +%% Finishes the span in the current pdict context. And sets the parent +%% as the current span ctx. +%% @end +%%-------------------------------------------------------------------- +-spec finish() -> ok. +finish() -> + {SpanCtx, ParentCtx} = current_ctx(), + ot_span_ets:finish_span(SpanCtx), + ot_ctx_pdict:with_value(?SPAN_CTX, ParentCtx), + ok. + +-spec get_binary_format() -> binary(). +get_binary_format() -> + <<>>. + +-spec get_http_text_format() -> opentelemetry:http_headers(). +get_http_text_format() -> + []. diff --git a/src/ot_tracer_sdk_dyn.erl b/src/ot_tracer_sdk_dyn.erl new file mode 100644 index 00000000..7b58f796 --- /dev/null +++ b/src/ot_tracer_sdk_dyn.erl @@ -0,0 +1,93 @@ +%%%------------------------------------------------------------------------ +%% 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(ot_tracer_sdk_dyn). + +-behaviour(ot_tracer). + +-export([start_span/2, + with_span/1, + with_span/2, + finish/0, + current_span_ctx/0, + get_binary_format/0, + get_http_text_format/0]). + +-define(span, (persistent_term:get({opentelemetry, span}, ot_span_ets))). +-define(ctx, (persistent_term:get({opentelemetry, ctx}, ot_ctx_pdict))). + +-define(SPAN_CTX, ot_tracer_dyn_span_ctx_key). + +-type pdict_trace_ctx() :: {opentelemetry:span_ctx(), pdict_trace_ctx() | undefined}. + +-spec start_span(opentelemetry:span_name(), ot_span:start_opts()) -> opentelemetry:span_ctx(). +start_span(Name, Opts) -> + case ?ctx:get(?SPAN_CTX) of + {SpanCtx, _}=Ctx -> + SpanCtx1 = ?span:start_span(Name, Opts#{parent => SpanCtx}), + ?ctx:with_value(?SPAN_CTX, {SpanCtx1, Ctx}), + SpanCtx1; + _ -> + SpanCtx = ?span:start_span(Name, Opts#{parent => undefined}), + ?ctx:with_value(?SPAN_CTX, {SpanCtx, undefined}), + SpanCtx + end. + +-spec with_span(opentelemetry:span_ctx()) -> ok. +with_span(SpanCtx) -> + ?ctx:with_value(?SPAN_CTX, {SpanCtx, undefined}). + +-spec with_span(opentelemetry:span_ctx(), fun()) -> ok. +with_span(SpanCtx, Fun) -> + ?ctx:with_value(?SPAN_CTX, {SpanCtx, undefined}, Fun). + +-spec current_span_ctx() -> opentelemetry:span_ctx(). +current_span_ctx() -> + case ?ctx:get(?SPAN_CTX) of + {SpanCtx, _ParentPdictSpanCtx} -> + SpanCtx; + _ -> + undefined + end. + +%% Internal function that returns the current trace context. +%% The pdict ctx stores both the current span ctx and the +%% parent trace context, which contains its parent and so on. +-spec current_ctx() -> pdict_trace_ctx(). +current_ctx() -> + ?ctx:get(?SPAN_CTX). + +%%-------------------------------------------------------------------- +%% @doc +%% Finishes the span in the current pdict context. And sets the parent +%% as the current span ctx or undefined if there is no local parent. +%% @end +%%-------------------------------------------------------------------- +-spec finish() -> ok. +finish() -> + {SpanCtx, ParentCtx} = current_ctx(), + ?span:finish_span(SpanCtx), + ?ctx:with_value(?SPAN_CTX, ParentCtx), + ok. + +-spec get_binary_format() -> binary(). +get_binary_format() -> + <<>>. + +-spec get_http_text_format() -> opentelemetry:http_headers(). +get_http_text_format() -> + []. diff --git a/test/opentelemetry_SUITE.erl b/test/opentelemetry_SUITE.erl index f7a1a143..d2b784de 100644 --- a/test/opentelemetry_SUITE.erl +++ b/test/opentelemetry_SUITE.erl @@ -6,16 +6,16 @@ -include_lib("common_test/include/ct.hrl"). all() -> - []. + [child_spans]. init_per_suite(Config) -> - application:load(opencensus), + application:load(opentelemetry), %% set application environment variables - {ok, _} = application:ensure_all_started(opencensus), + {ok, _} = application:ensure_all_started(opentelemetry), Config. end_per_suite(_Config) -> - ok = application:stop(opencensus). + ok = application:stop(opentelemetry). init_per_testcase(_, Config) -> Config. @@ -23,3 +23,31 @@ init_per_testcase(_, Config) -> end_per_testcase(_, _Config) -> ok. +child_spans(_Config) -> + %% start a span and 2 children + SpanCtx1 = ot_tracer:start_span(<<"span-1">>), + SpanCtx2 = ot_tracer:start_span(<<"span-2">>), + SpanCtx3 = ot_tracer:start_span(<<"span-3">>), + + %% finish the 3rd span + ?assertMatch(SpanCtx3, ot_tracer:current_span_ctx()), + ot_tracer:finish(), + + %% 2nd span should be the current span ctx now + ?assertMatch(SpanCtx2, ot_tracer:current_span_ctx()), + + %% start another child of the 2nd span + SpanCtx4 = ot_tracer:start_span(<<"span-4">>), + ?assertMatch(SpanCtx4, ot_tracer:current_span_ctx()), + + %% finish 4th span and 2nd should be current + ot_tracer:finish(), + ?assertMatch(SpanCtx2, ot_tracer:current_span_ctx()), + + %% finish 2th span and 1st should be current + ot_tracer:finish(), + ?assertMatch(SpanCtx1, ot_tracer:current_span_ctx()), + + %% finish first and no span should be current ctx + ot_tracer:finish(), + ?assertMatch(undefined, ot_tracer:current_span_ctx()).