diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..e13cf55a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +## Unreleased + +### API + +#### Context + +- Propagators must now be implementations of a propagator type's behaviour. At + this time only the `otel_propagator_text_map` behaviour exists. Callbacks for + inject and extract take an optional "set" and "get" function for working with + a carrier. +- Configuration of propagators is now a list of atoms representing either the + name of a builtin propagator (at this time those are, `trace_context`, `b3` and + `baggage`) or the name of a module implementing the propagator's behaviour. + - Default configuration: `{text_map_propagators, [trace_context, baggage]}` +- Injectors and extractors can be configured separately instead of using the + same list of propagators for both by configuring `text_map_injectors` and + `text_map_extractors`. + - For example you may want your service to support receiving `b3` headers + but have no need for it including `b3` headers when it is propagating to + other services: + + ``` + {text_map_injectors, [trace_context, baggage]}, + {text_map_extractors, [b3, trace_context, baggage]} + ``` diff --git a/apps/opentelemetry/include/otel_span.hrl b/apps/opentelemetry/include/otel_span.hrl index c0008a01..40a2d4ee 100644 --- a/apps/opentelemetry/include/otel_span.hrl +++ b/apps/opentelemetry/include/otel_span.hrl @@ -33,7 +33,7 @@ %% 64 bit int span id span_id :: opentelemetry:span_id() | undefined, - tracestate :: opentelemetry:tracestate() | undefined, + tracestate = [] :: opentelemetry:tracestate(), %% 64 bit int parent span parent_span_id :: opentelemetry:span_id() | undefined, diff --git a/apps/opentelemetry/src/opentelemetry.app.src b/apps/opentelemetry/src/opentelemetry.app.src index 576b7a6d..9eec729f 100644 --- a/apps/opentelemetry/src/opentelemetry.app.src +++ b/apps/opentelemetry/src/opentelemetry.app.src @@ -10,8 +10,7 @@ ]}, {env, [{sampler, {parent_based, #{root => always_on}}}, % default sampler - {text_map_propagators, [fun otel_baggage:get_text_map_propagators/0, - fun otel_tracer_default:w3c_propagators/0]}, + {text_map_propagators, [trace_context, baggage]}, %% list of disabled tracers {deny_list, []}, diff --git a/apps/opentelemetry/src/opentelemetry_app.erl b/apps/opentelemetry/src/opentelemetry_app.erl index 47afd421..d8288f6d 100644 --- a/apps/opentelemetry/src/opentelemetry_app.erl +++ b/apps/opentelemetry/src/opentelemetry_app.erl @@ -54,15 +54,7 @@ stop(_State) -> setup_text_map_propagators(Opts) -> Propagators = proplists:get_value(text_map_propagators, Opts, []), - - {Extractors, Injectors} = - lists:foldl(fun(F, {ExtractorsAcc, InjectorsAcc}) -> - {Extractor, Injector} = F(), - {[Extractor | ExtractorsAcc], [Injector | InjectorsAcc]} - end, {[], []}, Propagators), - - opentelemetry:set_text_map_extractors(Extractors), - opentelemetry:set_text_map_injectors(Injectors). + opentelemetry:set_text_map_propagators(Propagators). register_loaded_application_tracers(Opts) -> RegisterLoadedApplications = proplists:get_value(register_loaded_applications, Opts, true), diff --git a/apps/opentelemetry/src/otel_configuration.erl b/apps/opentelemetry/src/otel_configuration.erl index 41373836..14aa786a 100644 --- a/apps/opentelemetry/src/otel_configuration.erl +++ b/apps/opentelemetry/src/otel_configuration.erl @@ -173,9 +173,16 @@ transform(propagators, PropagatorsString) when is_list(PropagatorsString) -> end, Propagators); transform(propagator, "tracecontext") -> - fun otel_tracer_default:w3c_propagators/0; + trace_context; transform(propagator, "baggage") -> - fun otel_baggage:get_text_map_propagators/0; + baggage; +transform(propagator, "b3") -> + b3; +%% TODO: support b3multi and jager propagator formats +%% transform(propagator, "b3multi") -> +%% b3multi; +%% transform(propagator, "jaeger") -> +%% jaeger; transform(propagator, Propagator) -> ?LOG_WARNING("Ignoring uknown propagator ~ts in OS environment variable $OTEL_PROPAGATORS", [Propagator]), diff --git a/apps/opentelemetry/src/otel_tracer_default.erl b/apps/opentelemetry/src/otel_tracer_default.erl index 88cbd93b..9ae3fb13 100644 --- a/apps/opentelemetry/src/otel_tracer_default.erl +++ b/apps/opentelemetry/src/otel_tracer_default.erl @@ -20,9 +20,8 @@ -behaviour(otel_tracer). -export([start_span/4, - with_span/5, - b3_propagators/0, - w3c_propagators/0]). + with_span/5 + ]). -include_lib("opentelemetry_api/include/opentelemetry.hrl"). -include("otel_tracer.hrl"). @@ -59,10 +58,3 @@ with_span(Ctx, Tracer, SpanName, Opts, Fun) -> otel_ctx:detach(Ctx) end. --spec b3_propagators() -> {otel_propagator:text_map_extractor(), otel_propagator:text_map_injector()}. -b3_propagators() -> - otel_tracer:text_map_propagators(otel_propagator_http_b3). - --spec w3c_propagators() -> {otel_propagator:text_map_extractor(), otel_propagator:text_map_injector()}. -w3c_propagators() -> - otel_tracer:text_map_propagators(otel_propagator_http_w3c). diff --git a/apps/opentelemetry/test/opentelemetry_SUITE.erl b/apps/opentelemetry/test/opentelemetry_SUITE.erl index 778316f3..05d74512 100644 --- a/apps/opentelemetry/test/opentelemetry_SUITE.erl +++ b/apps/opentelemetry/test/opentelemetry_SUITE.erl @@ -14,21 +14,12 @@ -include("otel_span_ets.hrl"). all() -> - [all_testcases(), - {group, w3c}, - {group, b3}]. - -all_testcases() -> [disable_auto_registration, registered_tracers, with_span, macros, child_spans, update_span_data, tracer_instrumentation_library, tracer_previous_ctx, stop_temporary_app, reset_after, attach_ctx, default_sampler, non_recording_ets_table, root_span_sampling_always_on, root_span_sampling_always_off, record_but_not_sample, record_exception_works, record_exception_with_message_works]. -groups() -> - [{w3c, [], [propagation]}, - {b3, [], [propagation]}]. - init_per_suite(Config) -> application:load(opentelemetry), Config. @@ -37,26 +28,6 @@ end_per_suite(_Config) -> application:unload(opentelemetry), ok. -init_per_group(Propagator, Config) when Propagator =:= w3c ; - Propagator =:= b3 -> - application:set_env(opentelemetry, processors, [{otel_batch_processor, #{scheduled_delay_ms => 1}}]), - {ok, _} = application:ensure_all_started(opentelemetry), - - {BaggageTextMapExtractor, BaggageTextMapInjector} = otel_baggage:get_text_map_propagators(), - {TraceTextMapExtractor, TraceTextMapInjector} = case Propagator of - w3c -> otel_tracer_default:w3c_propagators(); - b3 -> otel_tracer_default:b3_propagators() - end, - opentelemetry:set_text_map_extractors([BaggageTextMapExtractor, - TraceTextMapExtractor]), - opentelemetry:set_text_map_injectors([BaggageTextMapInjector, - TraceTextMapInjector]), - - [{propagator, Propagator} | Config]. - -end_per_group(_, _Config) -> - _ = application:stop(opentelemetry). - init_per_testcase(disable_auto_registration, Config) -> application:set_env(opentelemetry, register_loaded_applications, false), {ok, _} = application:ensure_all_started(opentelemetry), @@ -233,67 +204,6 @@ update_span_data(Config) -> events=Events, _='_'})). -propagation(Config) -> - Propagator = ?config(propagator, Config), - SpanCtx=#span_ctx{trace_id=TraceId, - span_id=SpanId} = ?start_span(<<"span-1">>), - ?set_current_span(SpanCtx), - - ?assertMatch(#span_ctx{trace_flags=1}, ?current_span_ctx), - ?assertMatch(#span_ctx{is_recording=true}, ?current_span_ctx), - - - otel_baggage:set("key-1", <<"value=1">>, []), - %% TODO: should the whole baggage entry be dropped if metadata is bad? - %% drop bad metadata (the `1'). - otel_baggage:set(<<"key-2">>, <<"value-2">>, [<<"metadata">>, 1, {<<"md-k-1">>, <<"md-v-1">>}]), - %% drop baggage with bad value - otel_baggage:set(<<"key-3">>, value3), - - Headers = otel_propagator:text_map_inject([{<<"existing-header">>, <<"I exist">>}]), - - EncodedTraceId = io_lib:format("~32.16.0b", [TraceId]), - EncodedSpanId = io_lib:format("~16.16.0b", [SpanId]), - - ?assertListsEqual([{<<"baggage">>, <<"key-2=value-2;metadata;md-k-1=md-v-1,key-1=value%3D1">>}, - {<<"existing-header">>, <<"I exist">>} | - trace_context(Propagator, EncodedTraceId, EncodedSpanId)], Headers), - - ?end_span(SpanCtx), - - ?assertEqual(#{<<"key-1">> => {<<"value=1">>, []}, - <<"key-2">> => {<<"value-2">>, [<<"metadata">>, {<<"md-k-1">>, <<"md-v-1">>}]}}, - otel_baggage:get_all()), - - %% ?end_span doesn't remove the span from the context - ?assertEqual(SpanCtx, ?current_span_ctx), - ?set_current_span(undefined), - ?assertEqual(undefined, ?current_span_ctx), - - %% clear our baggage from the context to test extraction - otel_baggage:clear(), - ?assertEqual(#{}, otel_baggage:get_all()), - - %% make header keys uppercase to validate the extractor is case insensitive - BinaryHeaders = [{string:uppercase(Key), iolist_to_binary(Value)} || {Key, Value} <- Headers], - otel_propagator:text_map_extract(BinaryHeaders), - - ?assertEqual(#{<<"key-1">> => {<<"value=1">>, []}, - <<"key-2">> => {<<"value-2">>, [<<"metadata">>, {<<"md-k-1">>, <<"md-v-1">>}]}}, - otel_baggage:get_all()), - - %% extracted remote spans are set to the active span - %% but with `is_recording' false - ?assertMatch(#span_ctx{is_recording=false}, ?current_span_ctx), - - #span_ctx{trace_id=TraceId2, - span_id=_SpanId2} = ?start_span(<<"span-2">>), - - %% new span should be a child of the extracted span - ?assertEqual(TraceId, TraceId2), - - ok. - tracer_instrumentation_library(Config) -> Tid = ?config(tid, Config), @@ -598,10 +508,3 @@ assert_not_exported(Tid, #span_ctx{trace_id=TraceId, span_id=SpanId, _='_'})). -trace_context(w3c, EncodedTraceId, EncodedSpanId) -> - [{<<"traceparent">>, - iolist_to_binary([<<"00">>, "-", EncodedTraceId,"-", EncodedSpanId, "-", <<"01">>])}]; -trace_context(b3, EncodedTraceId, EncodedSpanId) -> - [{<<"X-B3-Sampled">>, <<"1">>}, - {<<"X-B3-SpanId">>, iolist_to_binary(EncodedSpanId)}, - {<<"X-B3-TraceId">>, iolist_to_binary(EncodedTraceId)}]. diff --git a/apps/opentelemetry/test/otel_configuration_SUITE.erl b/apps/opentelemetry/test/otel_configuration_SUITE.erl index 62f9a02e..f930aa35 100644 --- a/apps/opentelemetry/test/otel_configuration_SUITE.erl +++ b/apps/opentelemetry/test/otel_configuration_SUITE.erl @@ -107,8 +107,7 @@ end_per_testcase(_, Config) -> empty_os_environment(_Config) -> ?assertIsSubset([{log_level,info}, - {propagators,[fun otel_tracer_default:w3c_propagators/0, - fun otel_baggage:get_text_map_propagators/0]}, + {propagators,[trace_context, baggage]}, {sampler,{parent_based,#{root => always_on}}}], otel_configuration:merge_with_os([])), @@ -163,7 +162,7 @@ log_level(_Config) -> propagators(_Config) -> %% TODO: can make this a better error message when it fails with a custom assert macro ?assertIsSubset([{log_level, error}, - {propagators, [fun otel_baggage:get_text_map_propagators/0]}], + {propagators, [baggage]}], otel_configuration:merge_with_os([{log_level, error}])), ok. diff --git a/apps/opentelemetry/test/otel_propagation_SUITE.erl b/apps/opentelemetry/test/otel_propagation_SUITE.erl new file mode 100644 index 00000000..c4bf1fe2 --- /dev/null +++ b/apps/opentelemetry/test/otel_propagation_SUITE.erl @@ -0,0 +1,192 @@ +-module(otel_propagation_SUITE). + +-compile(export_all). + +-include_lib("stdlib/include/assert.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-include_lib("opentelemetry_api/include/opentelemetry.hrl"). +-include_lib("opentelemetry_api/include/otel_tracer.hrl"). +-include("otel_tracer.hrl"). +-include("otel_span.hrl"). +-include("otel_test_utils.hrl"). +-include("otel_sampler.hrl"). +-include("otel_span_ets.hrl"). + +all() -> + [override_propagators, + {group, w3c}, + {group, b3}]. + +groups() -> + [{w3c, [], [propagation]}, + {b3, [], [propagation]}]. + +init_per_suite(Config) -> + application:load(opentelemetry), + application:set_env(opentelemetry, processors, [{otel_batch_processor, #{scheduled_delay_ms => 1}}]), + {ok, _} = application:ensure_all_started(opentelemetry), + Config. + +end_per_suite(_Config) -> + application:unload(opentelemetry), + ok. + +init_per_group(Propagator, Config) when Propagator =:= w3c ; + Propagator =:= b3 -> + %% start in group as well since we must stop it after each group run + {ok, _} = application:ensure_all_started(opentelemetry), + + case Propagator of + w3c -> + opentelemetry:set_text_map_propagators([otel_propagator_baggage, + otel_propagator_trace_context]); + b3 -> + opentelemetry:set_text_map_propagators([otel_propagator_baggage, + otel_propagator_b3]) + end, + + [{propagator, Propagator} | Config]. + +end_per_group(_, _Config) -> + _ = application:stop(opentelemetry). + +propagation(Config) -> + Propagator = ?config(propagator, Config), + SpanCtx=#span_ctx{trace_id=TraceId, + span_id=SpanId} = ?start_span(<<"span-1">>), + ?set_current_span(SpanCtx), + + ?assertMatch(#span_ctx{trace_flags=1}, ?current_span_ctx), + ?assertMatch(#span_ctx{is_recording=true}, ?current_span_ctx), + + + otel_baggage:set("key-1", <<"value=1">>, []), + %% TODO: should the whole baggage entry be dropped if metadata is bad? + %% drop bad metadata (the `1'). + otel_baggage:set(<<"key-2">>, <<"value-2">>, [<<"metadata">>, 1, {<<"md-k-1">>, <<"md-v-1">>}]), + %% drop baggage with bad value + otel_baggage:set(<<"key-3">>, value3), + + Headers = otel_propagator_text_map:inject([{<<"existing-header">>, <<"I exist">>}]), + + EncodedTraceId = io_lib:format("~32.16.0b", [TraceId]), + EncodedSpanId = io_lib:format("~16.16.0b", [SpanId]), + + ?assertListsEqual([{<<"baggage">>, <<"key-2=value-2;metadata;md-k-1=md-v-1,key-1=value%3D1">>}, + {<<"existing-header">>, <<"I exist">>} | + trace_context(Propagator, EncodedTraceId, EncodedSpanId)], Headers), + + ?end_span(SpanCtx), + + ?assertEqual(#{<<"key-1">> => {<<"value=1">>, []}, + <<"key-2">> => {<<"value-2">>, [<<"metadata">>, {<<"md-k-1">>, <<"md-v-1">>}]}}, + otel_baggage:get_all()), + + %% ?end_span doesn't remove the span from the context + ?assertEqual(SpanCtx, ?current_span_ctx), + ?set_current_span(undefined), + ?assertEqual(undefined, ?current_span_ctx), + + %% clear our baggage from the context to test extraction + otel_baggage:clear(), + ?assertEqual(#{}, otel_baggage:get_all()), + + %% make header keys uppercase to validate the extractor is case insensitive + BinaryHeaders = [{string:uppercase(Key), iolist_to_binary(Value)} || {Key, Value} <- Headers], + otel_propagator_text_map:extract(BinaryHeaders), + + ?assertEqual(#{<<"key-1">> => {<<"value=1">>, []}, + <<"key-2">> => {<<"value-2">>, [<<"metadata">>, {<<"md-k-1">>, <<"md-v-1">>}]}}, + otel_baggage:get_all()), + + %% extracted remote spans are set to the active span + %% but with `is_recording' false + ?assertMatch(#span_ctx{is_recording=false}, ?current_span_ctx), + + #span_ctx{trace_id=TraceId2, + span_id=_SpanId2} = ?start_span(<<"span-2">>), + + %% new span should be a child of the extracted span + ?assertEqual(TraceId, TraceId2), + + ok. + +override_propagators(_Config) -> + SpanCtx=#span_ctx{} = ?start_span(<<"span-1">>), + ?set_current_span(SpanCtx), + + ?assertMatch(#span_ctx{trace_flags=1}, ?current_span_ctx), + ?assertMatch(#span_ctx{is_recording=true}, ?current_span_ctx), + + + otel_baggage:set("key-1", <<"value=1">>, []), + %% TODO: should the whole baggage entry be dropped if metadata is bad? + %% drop bad metadata (the `1'). + otel_baggage:set(<<"key-2">>, <<"value-2">>, [<<"metadata">>, 1, {<<"md-k-1">>, <<"md-v-1">>}]), + %% drop baggage with bad value + otel_baggage:set(<<"key-3">>, value3), + + Headers = otel_propagator_text_map:inject([{<<"existing-header">>, <<"I exist">>}], + #{propagators => [otel_propagator_baggage]}), + + %% the manually set propagators does not include trace_context or b3 + %% so header must only have the existing-header and the baggage + ?assertListsEqual([{<<"baggage">>, <<"key-2=value-2;metadata;md-k-1=md-v-1,key-1=value%3D1">>}, + {<<"existing-header">>, <<"I exist">>}], Headers), + + ?end_span(SpanCtx), + + ?assertEqual(#{<<"key-1">> => {<<"value=1">>, []}, + <<"key-2">> => {<<"value-2">>, [<<"metadata">>, {<<"md-k-1">>, <<"md-v-1">>}]}}, + otel_baggage:get_all()), + + %% ?end_span doesn't remove the span from the context + ?assertEqual(SpanCtx, ?current_span_ctx), + ?set_current_span(undefined), + ?assertEqual(undefined, ?current_span_ctx), + + %% clear our baggage from the context to test extraction + otel_baggage:clear(), + ?assertEqual(#{}, otel_baggage:get_all()), + + %% make header keys uppercase to validate the extractor is case insensitive + BinaryHeaders = [{string:uppercase(Key), iolist_to_binary(Value)} || {Key, Value} <- Headers], + otel_propagator_text_map:extract(BinaryHeaders, #{propagators => [otel_propagator_baggage]}), + + ?assertEqual(#{<<"key-1">> => {<<"value=1">>, []}, + <<"key-2">> => {<<"value-2">>, [<<"metadata">>, {<<"md-k-1">>, <<"md-v-1">>}]}}, + otel_baggage:get_all()), + + %% no trace extractor used so current span ctx remains undefined + ?assertEqual(undefined, ?current_span_ctx), + + ok. + +%% + +assert_all_exported(Tid, SpanCtxs) -> + [assert_exported(Tid, SpanCtx) || SpanCtx <- SpanCtxs]. + +assert_exported(Tid, #span_ctx{trace_id=TraceId, + span_id=SpanId}) -> + ?UNTIL_NOT_EQUAL([], ets:match_object(Tid, #span{trace_id=TraceId, + span_id=SpanId, + _='_'})). + +assert_not_exported(Tid, #span_ctx{trace_id=TraceId, + span_id=SpanId}) -> + %% sleep so exporter has run before we check + %% since we can't do like when checking it exists with UNTIL + timer:sleep(100), + ?assertMatch([], ets:match(Tid, #span{trace_id=TraceId, + span_id=SpanId, + _='_'})). + +trace_context(w3c, EncodedTraceId, EncodedSpanId) -> + [{<<"traceparent">>, + iolist_to_binary([<<"00">>, "-", EncodedTraceId,"-", EncodedSpanId, "-", <<"01">>])}]; +trace_context(b3, EncodedTraceId, EncodedSpanId) -> + [{<<"X-B3-Sampled">>, <<"1">>}, + {<<"X-B3-SpanId">>, iolist_to_binary(EncodedSpanId)}, + {<<"X-B3-TraceId">>, iolist_to_binary(EncodedTraceId)}]. diff --git a/apps/opentelemetry_api/include/opentelemetry.hrl b/apps/opentelemetry_api/include/opentelemetry.hrl index d8606ddf..cd51a2ad 100644 --- a/apps/opentelemetry_api/include/opentelemetry.hrl +++ b/apps/opentelemetry_api/include/opentelemetry.hrl @@ -44,7 +44,7 @@ %% 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, + tracestate = [] :: opentelemetry:tracestate(), %% 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, diff --git a/apps/opentelemetry_api/lib/open_telemetry/baggage.ex b/apps/opentelemetry_api/lib/open_telemetry/baggage.ex index 877d183b..13f40a23 100644 --- a/apps/opentelemetry_api/lib/open_telemetry/baggage.ex +++ b/apps/opentelemetry_api/lib/open_telemetry/baggage.ex @@ -13,5 +13,4 @@ defmodule OpenTelemetry.Baggage do defdelegate get_all(ctx), to: :otel_baggage defdelegate clear(), to: :otel_baggage defdelegate clear(ctx), to: :otel_baggage - defdelegate get_text_map_propagators(), to: :otel_baggage end diff --git a/apps/opentelemetry_api/src/opentelemetry.erl b/apps/opentelemetry_api/src/opentelemetry.erl index 9dbbbffb..990c1f8d 100644 --- a/apps/opentelemetry_api/src/opentelemetry.erl +++ b/apps/opentelemetry_api/src/opentelemetry.erl @@ -34,6 +34,7 @@ register_application_tracer/1, get_tracer/0, get_tracer/1, + set_text_map_propagators/1, set_text_map_extractors/1, get_text_map_extractors/0, set_text_map_injectors/1, @@ -149,13 +150,23 @@ get_tracer() -> get_tracer(Name) -> persistent_term:get({?MODULE, Name}, get_tracer()). +%% setting the propagators is the same as setting the same list for +%% injectors and extractors +set_text_map_propagators(List) when is_list(List) -> + set_text_map_injectors(List), + set_text_map_extractors(List); +set_text_map_propagators(_) -> + ok. + set_text_map_extractors(List) when is_list(List) -> - persistent_term:put({?MODULE, text_map_extractors}, List); + ParsedList = otel_propagator:builtins_to_modules(List), + persistent_term:put({?MODULE, text_map_extractors}, ParsedList); set_text_map_extractors(_) -> ok. set_text_map_injectors(List) when is_list(List) -> - persistent_term:put({?MODULE, text_map_injectors}, List); + ParsedList = otel_propagator:builtins_to_modules(List), + persistent_term:put({?MODULE, text_map_injectors}, ParsedList); set_text_map_injectors(_) -> ok. @@ -245,7 +256,7 @@ link(_, _) -> link(TraceId, SpanId, Attributes, TraceState) when is_integer(TraceId), is_integer(SpanId), is_list(Attributes), - (is_list(TraceState) orelse TraceState =:= undefined)-> + is_list(TraceState) -> #link{trace_id=TraceId, span_id=SpanId, attributes=Attributes, diff --git a/apps/opentelemetry_api/src/otel_baggage.erl b/apps/opentelemetry_api/src/otel_baggage.erl index 250f85a5..3e352484 100644 --- a/apps/opentelemetry_api/src/otel_baggage.erl +++ b/apps/opentelemetry_api/src/otel_baggage.erl @@ -26,8 +26,7 @@ get_all/0, get_all/1, clear/0, - clear/1, - get_text_map_propagators/0]). + clear/1]). %% keys and values are UTF-8 binaries -type key() :: unicode:unicode_binary(). @@ -40,19 +39,7 @@ key/0, value/0]). --define(DEC2HEX(X), - if ((X) >= 0) andalso ((X) =< 9) -> (X) + $0; - ((X) >= 10) andalso ((X) =< 15) -> (X) + $A - 10 - end). - --define(HEX2DEC(X), - if ((X) >= $0) andalso ((X) =< $9) -> (X) - $0; - ((X) >= $A) andalso ((X) =< $F) -> (X) - $A + 10; - ((X) >= $a) andalso ((X) =< $f) -> (X) - $a + 10 - end). - -define(BAGGAGE_KEY, '$__otel_baggage_ctx_key'). --define(BAGGAGE_HEADER, <<"baggage">>). -spec set(#{key() => value()} | [{key(), value()}]) -> ok. set(KeyValues) when is_list(KeyValues) -> @@ -101,37 +88,6 @@ clear() -> clear(Ctx) -> otel_ctx:set_value(Ctx, ?BAGGAGE_KEY, #{}). --spec get_text_map_propagators() -> {otel_propagator:text_map_extractor(), otel_propagator:text_map_injector()}. -get_text_map_propagators() -> - ToText = fun(Baggage) when is_map(Baggage) -> - case maps:fold(fun(Key, Value, Acc) -> - [$,, [encode_key(Key), "=", encode_value(Value)] | Acc] - end, [], Baggage) of - [$, | List] -> - [{?BAGGAGE_HEADER, unicode:characters_to_binary(List)}]; - _ -> - [] - end; - (_) -> - [] - end, - FromText = fun(Headers, CurrentBaggage) -> - case lookup(?BAGGAGE_HEADER, Headers) of - undefined -> - CurrentBaggage; - String -> - Pairs = string:lexemes(String, [$,]), - lists:foldl(fun(Pair, Acc) -> - [Key, Value] = string:split(Pair, "="), - Acc#{decode_key(Key) => - decode_value(Value)} - end, CurrentBaggage, Pairs) - end - end, - Inject = otel_ctx:text_map_injector(?BAGGAGE_KEY, ToText), - Extract = otel_ctx:text_map_extractor(?BAGGAGE_KEY, FromText), - {Extract, Inject}. - %% %% checks the keys, values and metadata are valid and drops them if they are not @@ -195,160 +151,3 @@ verify_metadata(M) when is_binary(M) -> true; verify_metadata(_) -> false. - -%% find a header in a list, ignoring case -lookup(_, []) -> - undefined; -lookup(Header, [{H, Value} | Rest]) -> - case string:equal(Header, H, true, none) of - true -> - Value; - false -> - lookup(Header, Rest) - end. - -encode_key(Key) -> - form_urlencode(Key, [{encoding, utf8}]). - -encode_value({Value, Metadata}) -> - EncodedMetadata = encode_metadata(Metadata), - EncodedValue = form_urlencode(Value, [{encoding, utf8}]), - unicode:characters_to_binary(lists:join(<<";">>, [EncodedValue | EncodedMetadata])). - -encode_metadata(Metadata) when is_list(Metadata) -> - lists:filtermap(fun({MK, MV}) when is_binary(MK) , is_binary(MV) -> - {true, [MK, <<"=">>, MV]}; - (M) when is_binary(M) -> - {true, M}; - (_) -> - false - end, Metadata); -encode_metadata(_) -> - []. - -decode_key(Key) -> - percent_decode(string:trim(unicode:characters_to_binary(Key))). - -decode_value(ValueAndMetadata) -> - [Value | MetadataList] = string:lexemes(ValueAndMetadata, [$;]), - {string_decode(Value), lists:filtermap(fun metadata_decode/1, MetadataList)}. - -metadata_decode(Metadata) -> - case string:split(Metadata, "=") of - [MetadataKey] -> - {true, string_decode(MetadataKey)}; - [MetadataKey, MetadataValue] -> - {true, {string_decode(MetadataKey), string_decode(MetadataValue)}}; - _ -> - false - end. - -string_decode(S) -> - percent_decode(string:trim(unicode:characters_to_binary(S))). - -%% TODO: call `uri_string:percent_decode' and remove this when OTP-23 is -%% the oldest version we maintain support for --spec percent_decode(URI) -> Result when - URI :: uri_string:uri_string(), - Result :: uri_string:uri_string() | - {error, {invalid, {atom(), {term(), term()}}}}. -percent_decode(URI) when is_list(URI) orelse - is_binary(URI) -> - raw_decode(URI). - -%% TODO: call `uri_string:percent_encode' when it is added to OTP and -%% available in the oldest version we support -form_urlencode(Cs, [{encoding, Encoding}]) - when is_list(Cs), Encoding =:= utf8; Encoding =:= unicode -> - B = convert_to_binary(Cs, utf8, Encoding), - html5_byte_encode(B); -form_urlencode(Cs, [{encoding, Encoding}]) - when is_binary(Cs), Encoding =:= utf8; Encoding =:= unicode -> - html5_byte_encode(Cs); -form_urlencode(Cs, [{encoding, Encoding}]) when is_list(Cs); is_binary(Cs) -> - throw({error,invalid_encoding, Encoding}); -form_urlencode(Cs, _) -> - throw({error,invalid_input, Cs}). - -html5_byte_encode(B) -> - html5_byte_encode(B, <<>>). -%% -html5_byte_encode(<<>>, Acc) -> - Acc; -html5_byte_encode(<<$ ,T/binary>>, Acc) -> - html5_byte_encode(T, <>); -html5_byte_encode(<>, Acc) -> - case is_url_char(H) of - true -> - html5_byte_encode(T, <>); - false -> - <> = <>, - html5_byte_encode(T, <>) - end; -html5_byte_encode(H, _Acc) -> - throw({error,invalid_input, H}). - - -%% Return true if input char can appear in form-urlencoded string -%% Allowed chararacters: -%% 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, -%% 0x5F, 0x61 to 0x7A -is_url_char(C) - when C =:= 16#2A; C =:= 16#2D; - C =:= 16#2E; C =:= 16#5F; - 16#30 =< C, C =< 16#39; - 16#41 =< C, C =< 16#5A; - 16#61 =< C, C =< 16#7A -> true; -is_url_char(_) -> false. - -%% Convert to binary -convert_to_binary(Binary, InEncoding, OutEncoding) -> - case unicode:characters_to_binary(Binary, InEncoding, OutEncoding) of - {error, _List, RestData} -> - throw({error, invalid_input, RestData}); - {incomplete, _List, RestData} -> - throw({error, invalid_input, RestData}); - Result -> - Result - end. - --spec raw_decode(list()|binary()) -> list() | binary() | uri_string:error(). -raw_decode(Cs) -> - raw_decode(Cs, <<>>). -%% -raw_decode(L, Acc) when is_list(L) -> - try - B0 = unicode:characters_to_binary(L), - B1 = raw_decode(B0, Acc), - unicode:characters_to_list(B1) - catch - throw:{error, Atom, RestData} -> - {error, Atom, RestData} - end; -raw_decode(<<$%,C0,C1,Cs/binary>>, Acc) -> - case is_hex_digit(C0) andalso is_hex_digit(C1) of - true -> - B = ?HEX2DEC(C0)*16+?HEX2DEC(C1), - raw_decode(Cs, <>); - false -> - throw({error,invalid_percent_encoding,<<$%,C0,C1>>}) - end; -raw_decode(<>, Acc) -> - raw_decode(Cs, <>); -raw_decode(<<>>, Acc) -> - check_utf8(Acc). - -%% Returns Cs if it is utf8 encoded. -check_utf8(Cs) -> - case unicode:characters_to_list(Cs) of - {incomplete,_,_} -> - throw({error,invalid_utf8,Cs}); - {error,_,_} -> - throw({error,invalid_utf8,Cs}); - _ -> Cs - end. - --spec is_hex_digit(char()) -> boolean(). -is_hex_digit(C) - when $0 =< C, C =< $9;$a =< C, C =< $f;$A =< C, C =< $F -> true; -is_hex_digit(_) -> false. diff --git a/apps/opentelemetry_api/src/otel_propagator.erl b/apps/opentelemetry_api/src/otel_propagator.erl index 78fc3891..56405bdb 100644 --- a/apps/opentelemetry_api/src/otel_propagator.erl +++ b/apps/opentelemetry_api/src/otel_propagator.erl @@ -12,55 +12,71 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %% -%% @doc +%% @doc A Propagator injects or extracts data from a Context so information +%% like baggage and trace context can be transported along with cross service +%% requests, like an HTTP request. +%% +%% Propagators are defined based on the type of encoding they inject and +%% extract. At this time there is only a TextMapPropagator, +%% {@link otel_propagator_text_map}, which works on ASCII keys and values. +%% +%% This behaviour is only for defining the callbacks used by each propagator +%% per type and is only used by developers adding a new type of propagator +%% (like for binary protocols), not implementations of propagators themselves +%% (like B3 or W3C TraceContext). +%% +%% Users configure and call propagators based on their type. See the docs +%% for {@link otel_propagator_text_map} for more details. %% @end %%%------------------------------------------------------------------------- -module(otel_propagator). --export([text_map_inject/1, - text_map_extract/1]). - --callback inject(term()) -> carrier(). --callback extract(carrier(), term()) -> term(). - --type text_map() :: [{binary(), binary()}]. +-export([builtins_to_modules/1, + builtin_to_module/1]). -%% TODO add binary carrier when it is included in the otel spec --type carrier() :: text_map(). +%% Sets a value into a carrier +-callback inject(carrier()) -> carrier(). +-callback inject(carrier(), propagator_options()) -> carrier(). +-callback inject_from(otel_ctx:t(), carrier()) -> carrier(). +-callback inject_from(otel_ctx:t(), carrier(), propagator_options()) -> carrier(). +%% extracts values from a carrier and sets them in the context +-callback extract(carrier()) -> otel_ctx:t(). +-callback extract(carrier(), propagator_options()) -> otel_ctx:t(). +-callback extract_to(otel_ctx:t(), carrier()) -> otel_ctx:t(). +-callback extract_to(otel_ctx:t(), carrier(), propagator_options()) -> otel_ctx:t(). -%% T is a carrier() --type extractor(T) :: {fun((T, term(), fun((carrier(), term()) -> term())) -> ok), term()}. --type injector(T) :: {fun((T, term(), fun((term()) -> carrier())) -> T), term()}. +-type propagator_options() :: #{propagators => [t()]}. --type text_map_injector() :: injector(text_map()). --type text_map_extractor() :: extractor(text_map()). +-type t() :: builtin() | module(). --export_type([carrier/0, - extractor/1, - injector/1, - text_map_injector/0, - text_map_extractor/0, - text_map/0]). +-type builtin() :: trace_context | tracecontext | b3. %% multib3 | jaeger -text_map_inject(TextMap) -> - Injectors = opentelemetry:get_text_map_injectors(), - run_injectors(TextMap, Injectors). +%% a carrier can be any type +-type carrier() :: term(). -text_map_extract(TextMap) -> - Extractors = opentelemetry:get_text_map_extractors(), - run_extractors(TextMap, Extractors). +-export_type([t/0, + builtin/0, + carrier/0]). -run_extractors(TextMap, Extractors) -> - lists:foldl(fun({Extract, {Key, FromText}}, ok) -> - Extract(TextMap, Key, FromText), - ok; - (_, ok) -> - ok - end, ok, Extractors). +%% convert the short name of a propagator to its module name if it is a builtin +%% if the name doens't match a builtin it is assumed to be a module +%% @hidden +-spec builtins_to_modules([t()]) -> [module()]. +builtins_to_modules(Propagators) -> + [builtin_to_module(P) || P <- Propagators]. -run_injectors(TextMap, Injectors) -> - lists:foldl(fun({Inject, {Key, ToText}}, TextMapAcc) -> - Inject(TextMapAcc, Key, ToText); - (_, TextMapAcc) -> - TextMapAcc - end, TextMap, Injectors). +%% @hidden +-spec builtin_to_module(builtin() | module()) -> module(). +builtin_to_module(tracecontext) -> + otel_propagator_trace_context; +builtin_to_module(trace_context) -> + otel_propagator_trace_context; +builtin_to_module(b3) -> + otel_propagator_b3; +%% TODO: add multib3 and jaeger as builtin propagators +%% builtin_to_module(multib3) -> +%% otel_propagator_multib3; +%% builtin_to_module(jaeger) -> +%% otel_propagator_jaeger; +builtin_to_module(Module) -> + Module. diff --git a/apps/opentelemetry_api/src/otel_propagator_b3.erl b/apps/opentelemetry_api/src/otel_propagator_b3.erl new file mode 100644 index 00000000..9a26eaf2 --- /dev/null +++ b/apps/opentelemetry_api/src/otel_propagator_b3.erl @@ -0,0 +1,134 @@ +%%%------------------------------------------------------------------------ +%% 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 An implementation of {@link otel_propagator_text_map} that injects and +%% extracts trace context using the B3 specification from Zipkin. +%% +%% Since `trace_context' and `baggage' are the two default propagators the +%% global TextMap Propagators must be configured if B3 is to be used for +%% propagation: +%% +%% ``` +%% {text_map_propagators, [b3, baggage]}, +%% ''' +%% +%% ``` +%% opentelemetry:set_text_map_propagators([b3]). +%% ''' +%% +%% It is also possible to set a separate list of injectors or extractors. +%% For example, if the service should extract B3 encoded context but you +%% only want to inject context encoded with the W3C TraceContext format +%% (maybe you have some services only supporting B3 that are making requests +%% to your server but you have no reason to continue propagating in both +%% formats when communicating to other services further down the stack). +%% In that case you would instead set configuration like: +%% +%% +%% ``` +%% {text_map_extractors, [b3, trace_context, baggage]}, +%% {text_map_injectors, [trace_context, baggage]}, +%% ''' +%% +%% Or using calls to {@link opentelemetry} at runtime: +%% +%% ``` +%% opentelemetry:set_text_map_extractors([b3, trace_context, baggage]), +%% opentelemetry:set_text_map_injectors([trace_context, baggage]). +%% ''' +%% @end +%%%----------------------------------------------------------------------- +-module(otel_propagator_b3). + +-behaviour(otel_propagator_text_map). + +-export([fields/0, + inject/3, + extract/4]). + +-include("opentelemetry.hrl"). + +-define(B3_TRACE_ID, <<"X-B3-TraceId">>). +-define(B3_SPAN_ID, <<"X-B3-SpanId">>). +-define(B3_SAMPLED, <<"X-B3-Sampled">>). + +-define(B3_IS_SAMPLED(S), S =:= "1" orelse S =:= <<"1">> orelse S =:= "true" orelse S =:= <<"true">>). + +fields() -> + [?B3_TRACE_ID, ?B3_SPAN_ID, ?B3_SAMPLED]. + +inject(Ctx, Carrier, CarrierSet) -> + case otel_tracer:current_span_ctx(Ctx) of + #span_ctx{trace_id=TraceId, + span_id=SpanId, + trace_flags=TraceOptions} when TraceId =/= 0 andalso SpanId =/= 0 -> + Options = case TraceOptions band 1 of 1 -> <<"1">>; _ -> <<"0">> end, + EncodedTraceId = io_lib:format("~32.16.0b", [TraceId]), + EncodedSpanId = io_lib:format("~16.16.0b", [SpanId]), + CarrierSet(?B3_TRACE_ID, iolist_to_binary(EncodedTraceId), + CarrierSet(?B3_SPAN_ID, iolist_to_binary(EncodedSpanId), + CarrierSet(?B3_SAMPLED, Options, Carrier))); + _ -> + Carrier + end. + +extract(Ctx, Carrier, _CarrierKeysFun, CarrierGet) -> + try + TraceId = trace_id(Carrier, CarrierGet), + SpanId = span_id(Carrier, CarrierGet), + Sampled = CarrierGet(?B3_SAMPLED, Carrier), + SpanCtx = + otel_tracer:from_remote_span(string_to_integer(TraceId, 16), + string_to_integer(SpanId, 16), + case Sampled of True when ?B3_IS_SAMPLED(True) -> 1; _ -> 0 end), + otel_tracer:set_current_span(Ctx, SpanCtx) + catch + throw:invalid -> + Ctx; + + %% thrown if _to_integer fails + error:badarg -> + Ctx + end. + +trace_id(Carrier, CarrierGet) -> + case CarrierGet(?B3_TRACE_ID, Carrier) of + TraceId when is_list(TraceId) orelse is_binary(TraceId) -> + case string:length(TraceId) =:= 32 orelse string:length(TraceId) =:= 16 of + true -> + TraceId; + _ -> + throw(invalid) + end; + _ -> + throw(invalid) + end. + +span_id(Carrier, CarrierGet) -> + case CarrierGet(?B3_SPAN_ID, Carrier) of + SpanId when is_list(SpanId) orelse is_binary(SpanId) -> + case string:length(SpanId) =:= 16 of + true -> + SpanId; + _ -> + throw(invalid) + end; + _ -> + throw(invalid) + end. + +string_to_integer(S, Base) when is_binary(S) -> + binary_to_integer(S, Base); +string_to_integer(S, Base) when is_list(S) -> + list_to_integer(S, Base). diff --git a/apps/opentelemetry_api/src/otel_propagator_baggage.erl b/apps/opentelemetry_api/src/otel_propagator_baggage.erl new file mode 100644 index 00000000..92f5064c --- /dev/null +++ b/apps/opentelemetry_api/src/otel_propagator_baggage.erl @@ -0,0 +1,228 @@ +%%%------------------------------------------------------------------------ +%% Copyright 2021, 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 An implementation of {@link otel_propagator_text_map} that injects and +%% extracts baggage using the +%% W3C Baggage format. +%% +%% This propagator along with {@link otel_propagator_trace_context} are used +%% by default. The global TextMap Propagators can be configured in the +%% application environment: +%% +%% ``` +%% {text_map_propagators, [trace_context, baggage]}, +%% ''' +%% +%% Or by calling {@link opentelemetry:set_text_map_propagators/1}. +%% @end +%%%----------------------------------------------------------------------- +-module(otel_propagator_baggage). + +-behaviour(otel_propagator_text_map). + +-export([fields/0, + inject/3, + extract/4]). + +-include("opentelemetry.hrl"). + +-define(DEC2HEX(X), + if ((X) >= 0) andalso ((X) =< 9) -> (X) + $0; + ((X) >= 10) andalso ((X) =< 15) -> (X) + $A - 10 + end). + +-define(HEX2DEC(X), + if ((X) >= $0) andalso ((X) =< $9) -> (X) - $0; + ((X) >= $A) andalso ((X) =< $F) -> (X) - $A + 10; + ((X) >= $a) andalso ((X) =< $f) -> (X) - $a + 10 + end). + +-define(BAGGAGE_HEADER, <<"baggage">>). + +fields() -> + [?BAGGAGE_HEADER]. + +inject(Ctx, Carrier, CarrierSet) -> + Baggage = otel_baggage:get_all(Ctx), + case maps:fold(fun(Key, Value, Acc) -> + [$,, [encode_key(Key), "=", encode_value(Value)] | Acc] + end, [], Baggage) of + [$, | List] -> + CarrierSet(?BAGGAGE_HEADER, unicode:characters_to_binary(List), Carrier); + _ -> + Carrier + end. + +extract(Ctx, Carrier, _CarrierKeysFun, CarrierGet) -> + case CarrierGet(?BAGGAGE_HEADER, Carrier) of + undefined -> + Ctx; + String -> + Pairs = string:lexemes(String, [$,]), + DecodedBaggage = + lists:foldl(fun(Pair, Acc) -> + [Key, Value] = string:split(Pair, "="), + Acc#{decode_key(Key) => decode_value(Value)} + + end, #{}, Pairs), + otel_baggage:set(Ctx, DecodedBaggage) + end. + +%% + +encode_key(Key) -> + form_urlencode(Key, [{encoding, utf8}]). + +encode_value({Value, Metadata}) -> + EncodedMetadata = encode_metadata(Metadata), + EncodedValue = form_urlencode(Value, [{encoding, utf8}]), + unicode:characters_to_binary(lists:join(<<";">>, [EncodedValue | EncodedMetadata])). + +encode_metadata(Metadata) when is_list(Metadata) -> + lists:filtermap(fun({MK, MV}) when is_binary(MK) , is_binary(MV) -> + {true, [MK, <<"=">>, MV]}; + (M) when is_binary(M) -> + {true, M}; + (_) -> + false + end, Metadata); +encode_metadata(_) -> + []. + +decode_key(Key) -> + percent_decode(string:trim(unicode:characters_to_binary(Key))). + +decode_value(ValueAndMetadata) -> + [Value | MetadataList] = string:lexemes(ValueAndMetadata, [$;]), + {string_decode(Value), lists:filtermap(fun metadata_decode/1, MetadataList)}. + +metadata_decode(Metadata) -> + case string:split(Metadata, "=") of + [MetadataKey] -> + {true, string_decode(MetadataKey)}; + [MetadataKey, MetadataValue] -> + {true, {string_decode(MetadataKey), string_decode(MetadataValue)}}; + _ -> + false + end. + +string_decode(S) -> + percent_decode(string:trim(unicode:characters_to_binary(S))). + +%% TODO: call `uri_string:percent_decode' and remove this when OTP-23 is +%% the oldest version we maintain support for +-spec percent_decode(URI) -> Result when + URI :: uri_string:uri_string(), + Result :: uri_string:uri_string() | + {error, {invalid, {atom(), {term(), term()}}}}. +percent_decode(URI) when is_list(URI) orelse + is_binary(URI) -> + raw_decode(URI). + +%% TODO: call `uri_string:percent_encode' when it is added to OTP and +%% available in the oldest version we support +form_urlencode(Cs, [{encoding, Encoding}]) + when is_list(Cs), Encoding =:= utf8; Encoding =:= unicode -> + B = convert_to_binary(Cs, utf8, Encoding), + html5_byte_encode(B); +form_urlencode(Cs, [{encoding, Encoding}]) + when is_binary(Cs), Encoding =:= utf8; Encoding =:= unicode -> + html5_byte_encode(Cs); +form_urlencode(Cs, [{encoding, Encoding}]) when is_list(Cs); is_binary(Cs) -> + throw({error,invalid_encoding, Encoding}); +form_urlencode(Cs, _) -> + throw({error,invalid_input, Cs}). + +html5_byte_encode(B) -> + html5_byte_encode(B, <<>>). +%% +html5_byte_encode(<<>>, Acc) -> + Acc; +html5_byte_encode(<<$ ,T/binary>>, Acc) -> + html5_byte_encode(T, <>); +html5_byte_encode(<>, Acc) -> + case is_url_char(H) of + true -> + html5_byte_encode(T, <>); + false -> + <> = <>, + html5_byte_encode(T, <>) + end; +html5_byte_encode(H, _Acc) -> + throw({error,invalid_input, H}). + + +%% Return true if input char can appear in form-urlencoded string +%% Allowed chararacters: +%% 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, +%% 0x5F, 0x61 to 0x7A +is_url_char(C) + when C =:= 16#2A; C =:= 16#2D; + C =:= 16#2E; C =:= 16#5F; + 16#30 =< C, C =< 16#39; + 16#41 =< C, C =< 16#5A; + 16#61 =< C, C =< 16#7A -> true; +is_url_char(_) -> false. + +%% Convert to binary +convert_to_binary(Binary, InEncoding, OutEncoding) -> + case unicode:characters_to_binary(Binary, InEncoding, OutEncoding) of + {error, _List, RestData} -> + throw({error, invalid_input, RestData}); + {incomplete, _List, RestData} -> + throw({error, invalid_input, RestData}); + Result -> + Result + end. + +-spec raw_decode(list()|binary()) -> list() | binary() | uri_string:error(). +raw_decode(Cs) -> + raw_decode(Cs, <<>>). +%% +raw_decode(L, Acc) when is_list(L) -> + try + B0 = unicode:characters_to_binary(L), + B1 = raw_decode(B0, Acc), + unicode:characters_to_list(B1) + catch + throw:{error, Atom, RestData} -> + {error, Atom, RestData} + end; +raw_decode(<<$%,C0,C1,Cs/binary>>, Acc) -> + case is_hex_digit(C0) andalso is_hex_digit(C1) of + true -> + B = ?HEX2DEC(C0)*16+?HEX2DEC(C1), + raw_decode(Cs, <>); + false -> + throw({error,invalid_percent_encoding,<<$%,C0,C1>>}) + end; +raw_decode(<>, Acc) -> + raw_decode(Cs, <>); +raw_decode(<<>>, Acc) -> + check_utf8(Acc). + +%% Returns Cs if it is utf8 encoded. +check_utf8(Cs) -> + case unicode:characters_to_list(Cs) of + {incomplete,_,_} -> + throw({error,invalid_utf8,Cs}); + {error,_,_} -> + throw({error,invalid_utf8,Cs}); + _ -> Cs + end. + +-spec is_hex_digit(char()) -> boolean(). +is_hex_digit(C) + when $0 =< C, C =< $9;$a =< C, C =< $f;$A =< C, C =< $F -> true; +is_hex_digit(_) -> false. diff --git a/apps/opentelemetry_api/src/otel_propagator_http_b3.erl b/apps/opentelemetry_api/src/otel_propagator_http_b3.erl deleted file mode 100644 index 364f1986..00000000 --- a/apps/opentelemetry_api/src/otel_propagator_http_b3.erl +++ /dev/null @@ -1,110 +0,0 @@ -%%%------------------------------------------------------------------------ -%% 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_propagator_http_b3). - --behaviour(otel_propagator). - --export([inject/1, - extract/2]). - --include("opentelemetry.hrl"). - --define(B3_TRACE_ID, <<"X-B3-TraceId">>). --define(B3_SPAN_ID, <<"X-B3-SpanId">>). --define(B3_SAMPLED, <<"X-B3-Sampled">>). - --define(B3_IS_SAMPLED(S), S =:= "1" orelse S =:= <<"1">> orelse S =:= "true" orelse S =:= <<"true">>). - --spec inject(opentelemetry:span_ctx() | undefined) -> otel_propagator:text_map(). -inject(#span_ctx{trace_id=TraceId, - span_id=SpanId}) when TraceId =:= 0 - ; SpanId =:= 0 -> - []; -inject(#span_ctx{trace_id=TraceId, - span_id=SpanId, - trace_flags=TraceOptions}) -> - Options = case TraceOptions band 1 of 1 -> <<"1">>; _ -> <<"0">> end, - EncodedTraceId = io_lib:format("~32.16.0b", [TraceId]), - EncodedSpanId = io_lib:format("~16.16.0b", [SpanId]), - [{?B3_TRACE_ID, iolist_to_binary(EncodedTraceId)}, - {?B3_SPAN_ID, iolist_to_binary(EncodedSpanId)}, - {?B3_SAMPLED, Options}]; -inject(undefined) -> - []. - --spec extract(otel_propagator:text_map(), term()) -> opentelemetry:span_ctx() | undefined. -extract(Headers, _) when is_list(Headers) -> - try - TraceId = trace_id(Headers), - SpanId = span_id(Headers), - Sampled = lookup(?B3_SAMPLED, Headers), - otel_tracer:from_remote_span(string_to_integer(TraceId, 16), - string_to_integer(SpanId, 16), - case Sampled of True when ?B3_IS_SAMPLED(True) -> 1; _ -> 0 end) - catch - throw:invalid -> - undefined; - - %% thrown if _to_integer fails - error:badarg -> - undefined - end; -extract(_, _) -> - undefined. - -trace_id(Headers) -> - case lookup(?B3_TRACE_ID, Headers) of - TraceId when is_list(TraceId) orelse is_binary(TraceId) -> - case string:length(TraceId) =:= 32 orelse string:length(TraceId) =:= 16 of - true -> - TraceId; - _ -> - throw(invalid) - end; - _ -> - throw(invalid) - end. - -span_id(Headers) -> - case lookup(?B3_SPAN_ID, Headers) of - SpanId when is_list(SpanId) orelse is_binary(SpanId) -> - case string:length(SpanId) =:= 16 of - true -> - SpanId; - _ -> - throw(invalid) - end; - _ -> - throw(invalid) - end. - -%% find a header in a list, ignoring case -lookup(_, []) -> - undefined; -lookup(Header, [{H, Value} | Rest]) -> - case string:equal(Header, H, true, none) of - true -> - Value; - false -> - lookup(Header, Rest) - end. - -string_to_integer(S, Base) when is_binary(S) -> - binary_to_integer(S, Base); -string_to_integer(S, Base) when is_list(S) -> - list_to_integer(S, Base). diff --git a/apps/opentelemetry_api/src/otel_propagator_text_map.erl b/apps/opentelemetry_api/src/otel_propagator_text_map.erl new file mode 100644 index 00000000..36dba3bb --- /dev/null +++ b/apps/opentelemetry_api/src/otel_propagator_text_map.erl @@ -0,0 +1,217 @@ +%%%------------------------------------------------------------------------ +%% Copyright 2020, 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 A TextMap Propagator is a Propagator that performs injection and +%% extraction with ASCII keys and values. +%% +%% An example of +%% configuring the TextMap Propagator to inject and extract Baggage and +%% TraceContext: +%% +%% ``` +%% {text_map_propagators, [trace_context, baggage]}, +%% ''' +%% +%% The propagators are then used at the points that cross service +%% communication is performed. By default `inject' and `extract' work on a +%% generic list of 2-tuple's with binary string keys and values. A user +%% defined function for setting a key/value in the carrier and for getting +%% the value of a key from a carrier can be passed as an argument. For +%% example, injecting and extracting to and from Hackney headers could be +%% done with Hackney specific functions: +%% +%% ``` +%% set_header(Headers, Key, Value) -> +%% hackney_headers:store(Key, Value, Headers). +%% +%% some_fun_calling_hackney() -> +%% Headers = otel_propagator_text_map:inject(hackney_headers:new(), fun set_header/2), +%% ... +%% ''' +%% +%% An example of extraction in an Elli request handler: +%% +%% ``` +%% get_header(Req, Key) -> +%% elli_request:get_header(Key, Req, Default). +%% +%% handle(Req, _Args) -> +%% otel_propagator_text_map:extract(Req, fun get_header/2), +%% ... +%% {ok, [], <<"hello world">>}. +%% ''' +%% @end +%%%------------------------------------------------------------------------- +-module(otel_propagator_text_map). + +-behaviour(otel_propagator). + +-export([inject/1, + inject/2, + inject_from/2, + inject_from/3, + extract/1, + extract/2, + extract_to/2, + extract_to/3]). + +-export([default_carrier_get/2, + default_carrier_set/3, + default_carrier_keys/1]). + +-include_lib("kernel/include/logger.hrl"). + +%% Sets a value into a carrier +-callback inject(otel_ctx:t(), otel_propagator:carrier(), carrier_set()) -> otel_propagator:carrier(). + +%% Extracts values from a carrier and sets them in the context +-callback extract(otel_ctx:t(), otel_propagator:carrier(), carrier_keys(), carrier_get()) -> term(). + +%% Returns all the keys the propagator sets with `inject' +-callback fields() -> [field_key()]. + +-type field_key() :: unicode:latin1_binary(). +-type field_value() :: unicode:latin1_binary(). + +%% return all matching keys from the carrier +%% for example: with the jaeger propagation format this would be +%% all keys found with prefix "uberctx-" +-type carrier_keys() :: fun((otel_propagator:carrier()) -> [unicode:latin1_binary()]). +-type carrier_get() :: fun((otel_propagator:carrier(), unicode:latin1_binary()) -> unicode:latin1_binary() | undefined). +-type carrier_set() :: fun((otel_propagator:carrier(), unicode:latin1_binary(), unicode:latin1_binary()) -> otel_propagator:carrier()). + +-type default_text_map_carrier() :: [{unicode:latin1_binary(), unicode:latin1_binary()}]. + +-type inject_options() :: #{carrier_set_fun => carrier_set(), + propagators => [otel_propagator:t()]}. + +-type extract_options() :: #{carrier_get_fun => carrier_get(), + carrier_keys_fun => carrier_keys(), + propagators => [otel_propagator:t()]}. +-export_type([]). + +-spec inject(otel_propagator:carrier()) -> otel_propagator:carrier(). +inject(Carrier) -> + inject(Carrier, #{}). + +-spec inject(otel_propagator:carrier(), inject_options()) -> otel_propagator:carrier(). +inject(Carrier, InjectOptions) -> + Context = otel_ctx:get_current(), + inject_from(Context, Carrier, InjectOptions). + +-spec inject_from(otel_ctx:t(), otel_propagator:carrier()) -> otel_propagator:carrier(). +inject_from(Context, Carrier) -> + inject_from(Context, Carrier, #{}). + +-spec inject_from(otel_ctx:t(), otel_propagator:carrier(), inject_options()) -> otel_propagator:carrier(). +inject_from(Context, Carrier, InjectOptions) when is_map(InjectOptions)-> + Injectors = maps:get(propagators, InjectOptions, opentelemetry:get_text_map_injectors()), + CarrierSetFun = maps:get(carrier_set_fun, InjectOptions, fun ?MODULE:default_carrier_set/3), + run_injectors(Context, Injectors, Carrier, CarrierSetFun); +inject_from(_Context, Carrier, InjectOptions) -> + ?LOG_INFO("inject failed. InjectOptions must be a map but instead got: ~p", [InjectOptions]), + Carrier. + +-spec extract(otel_propagator:carrier()) -> otel_ctx:t(). +extract(Carrier) -> + extract(Carrier, #{}). + +-spec extract(otel_propagator:carrier(), extract_options()) -> otel_ctx:t(). +extract(Carrier, ExtractOptions) -> + Context = otel_ctx:get_current(), + Context1 = extract_to(Context, Carrier, ExtractOptions), + otel_ctx:attach(Context1). + +-spec extract_to(otel_ctx:t(), otel_propagator:carrier()) -> otel_ctx:t(). +extract_to(Context, Carrier) -> + extract_to(Context, Carrier, #{}). + +-spec extract_to(otel_ctx:t(), otel_propagator:carrier(), extract_options()) -> otel_ctx:t(). +extract_to(Context, Carrier, ExtractOptions) when is_map(ExtractOptions) -> + Extractors = maps:get(propagators, ExtractOptions, opentelemetry:get_text_map_extractors()), + CarrierKeysFun = maps:get(carrier_keys_fun, ExtractOptions, fun ?MODULE:default_carrier_keys/1), + CarrierGetFun = maps:get(carrier_get_fun, ExtractOptions, fun ?MODULE:default_carrier_get/2), + run_extractors(Context, Extractors, Carrier, CarrierKeysFun, CarrierGetFun); +extract_to(Context, _Carrier, ExtractOptions) -> + ?LOG_INFO("extract failed. ExtractOptions must be a map but instead got: ~p", [ExtractOptions]), + Context. + +run_extractors(Context, Extractors, Carrier, CarrierKeysFun, CarrierGetFun) when is_list(Extractors) -> + lists:foldl(fun(Extract, ContextAcc) -> + try Extract:extract(ContextAcc, Carrier, CarrierKeysFun, CarrierGetFun) + catch + C:E:S -> + ?LOG_INFO("text map propagator failed to extract from carrier", + #{extractor => Extract, carrier => Carrier, + class => C, exception => E, stacktrace => S}), + ContextAcc + end + end, Context, otel_propagator:builtins_to_modules(Extractors)); +run_extractors(Context, Extractors, _, _, _) -> + ?LOG_INFO("extract failed. Extractors must be a list but instead got: ~p", [Extractors]), + Context. + +run_injectors(Context, Injectors, Carrier, Setter) when is_list(Injectors) -> + lists:foldl(fun(Inject, CarrierAcc) -> + try Inject:inject(Context, CarrierAcc, Setter) + catch + C:E:S -> + ?LOG_INFO("text map propagator failed to inject to carrier", + #{injector => Inject, carrier => CarrierAcc, + class => C, exception => E, stacktrace => S}), + CarrierAcc + end + end, Carrier, otel_propagator:builtins_to_modules(Injectors)); +run_injectors(_, Injectors, Carrier, _) -> + ?LOG_INFO("inject failed. Injectors must be a list but instead got: ~p", [Injectors]), + Carrier. + +%% case-insensitive finding of a key string in a list of ASCII strings +%% if there are multiple entries in the list for the same key the values +%% will be combined and separated by commas. This is the method defined +%% in RFC7230 for HTTP headers. +-spec default_carrier_get(field_key(), default_text_map_carrier()) -> field_value() | undefined. +default_carrier_get(Key, List) -> + default_carrier_get(Key, List, []). + +default_carrier_get(_, [], []) -> + undefined; +default_carrier_get(_, [], Result) -> + unicode:characters_to_binary(lists:join($,, lists:reverse(Result)), latin1); +default_carrier_get(Key, [{H, V} | Rest], Result) -> + case string:equal(Key, H, true, none) of + true -> + default_carrier_get(Key, Rest, [V | Result]); + false -> + default_carrier_get(Key, Rest, Result) + end. + +%% case-insensitive ASCII string based lists:keyreplace +-spec default_carrier_set(field_key(), field_value(), default_text_map_carrier()) + -> default_text_map_carrier(). +default_carrier_set(Key, Value, []) -> + [{Key, Value}]; +default_carrier_set(Key, Value, [{H, _}=Elem | Rest]) -> + case string:equal(Key, H, true, none) of + true -> + [{Key, Value} | Rest]; + false -> + [Elem | default_carrier_set(Key, Value, Rest)] + end. + +-spec default_carrier_keys(default_text_map_carrier()) -> [field_key()]. +default_carrier_keys([]) -> + []; +default_carrier_keys([{K, _} | Rest]) -> + [K | default_carrier_keys(Rest)]. diff --git a/apps/opentelemetry_api/src/otel_propagator_http_w3c.erl b/apps/opentelemetry_api/src/otel_propagator_trace_context.erl similarity index 58% rename from apps/opentelemetry_api/src/otel_propagator_http_w3c.erl rename to apps/opentelemetry_api/src/otel_propagator_trace_context.erl index 35988693..469fd772 100644 --- a/apps/opentelemetry_api/src/otel_propagator_http_w3c.erl +++ b/apps/opentelemetry_api/src/otel_propagator_trace_context.erl @@ -12,17 +12,28 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %% -%% @doc +%% @doc An implementation of {@link otel_propagator_text_map} that injects and +%% extracts trace context using the +%% W3C TraceContext format. +%% +%% This propagator along with {@link otel_propagator_baggage} are used +%% by default. The global TextMap Propagators can be configured in the +%% application environment: +%% +%% ``` +%% {text_map_propagators, [trace_context, baggage]}, +%% ''' +%% +%% Or by calling {@link opentelemetry:set_text_map_propagators/1}. %% @end %%%----------------------------------------------------------------------- --module(otel_propagator_http_w3c). +-module(otel_propagator_trace_context). --behaviour(otel_propagator). +-behaviour(otel_propagator_text_map). --export([inject/1, - encode/1, - extract/2, - decode/1]). +-export([fields/0, + inject/3, + extract/4]). -include("opentelemetry.hrl"). @@ -39,73 +50,54 @@ -define(MAX_TRACESTATE_PAIRS, 32). --spec inject(opentelemetry:span_ctx() | undefined) -> otel_propagator:text_map(). -inject(#span_ctx{trace_id=TraceId, - span_id=SpanId}) - when TraceId =:= 0 orelse SpanId =:= 0 -> - []; -inject(SpanCtx=#span_ctx{}) -> - EncodedValue = encode(SpanCtx), - [{?HEADER_KEY, EncodedValue} | encode_tracestate(SpanCtx)]; -inject(undefined) -> - []. +fields() -> + [?HEADER_KEY, ?STATE_HEADER_KEY]. + +inject(Ctx, Carrier, CarrierSet) -> + case otel_tracer:current_span_ctx(Ctx) of + SpanCtx=#span_ctx{trace_id=TraceId, + span_id=SpanId} when TraceId =/= 0 andalso SpanId =/= 0 -> + {TraceParent, TraceState} = encode_span_ctx(SpanCtx), + Carrier1 = CarrierSet(?HEADER_KEY, TraceParent, Carrier), + case TraceState of + <<>> -> + Carrier1; + _ -> + CarrierSet(?STATE_HEADER_KEY, TraceState, Carrier1) + end; + _ -> + Carrier + end. --spec encode(opentelemetry:span_ctx()) -> binary(). -encode(#span_ctx{trace_id=TraceId, - span_id=SpanId, - trace_flags=TraceOptions}) -> +extract(Ctx, Carrier, _CarrierKeysFun, CarrierGet) -> + SpanCtxString = CarrierGet(?HEADER_KEY, Carrier), + case decode(string:trim(SpanCtxString)) of + undefined -> + Ctx; + SpanCtx -> + TraceStateString = CarrierGet(?STATE_HEADER_KEY, Carrier), + Tracestate = tracestate_decode(TraceStateString), + otel_tracer:set_current_span(Ctx, SpanCtx#span_ctx{tracestate=Tracestate}) + end. + +%% + +-spec encode_span_ctx(opentelemetry:span_ctx()) -> {unicode:latin1_binary(), unicode:latin1_binary()}. +encode_span_ctx(#span_ctx{trace_id=TraceId, + span_id=SpanId, + trace_flags=TraceOptions, + tracestate=TraceState}) -> + {encode_traceparent(TraceId, SpanId, TraceOptions), encode_tracestate(TraceState)}. + +encode_traceparent(TraceId, SpanId, TraceOptions) -> Options = case TraceOptions band 1 of 1 -> <<"01">>; _ -> <<"00">> end, EncodedTraceId = io_lib:format("~32.16.0b", [TraceId]), EncodedSpanId = io_lib:format("~16.16.0b", [SpanId]), iolist_to_binary([?VERSION, "-", EncodedTraceId, "-", EncodedSpanId, "-", Options]). -encode_tracestate(#span_ctx{tracestate=undefined}) -> - []; -encode_tracestate(#span_ctx{tracestate=Entries}) -> +encode_tracestate(Entries) -> StateHeaderValue = lists:join($,, [[Key, $=, Value] || {Key, Value} <- Entries]), - [{?STATE_HEADER_KEY, unicode:characters_to_binary(StateHeaderValue)}]. - --spec extract(otel_propagator:text_map(), term()) -> opentelemetry:span_ctx()| undefined. -extract(Headers, _) when is_list(Headers) -> - case header_take(?HEADER_KEY, Headers) of - [{_, Value} | RestHeaders] -> - case header_member(?HEADER_KEY, RestHeaders) of - true -> - %% duplicate traceparent header found - undefined; - false -> - case decode(string:trim(Value)) of - undefined -> - undefined; - SpanCtx -> - Tracestate = tracestate_from_headers(Headers), - SpanCtx#span_ctx{tracestate=Tracestate} - end - end; - _ -> - undefined - end; -extract(_, _) -> - undefined. - -tracestate_from_headers(Headers) -> - %% could be multiple tracestate headers. Combine them all with comma separators - case combine_headers(?STATE_HEADER_KEY, Headers) of - [] -> - undefined; - FieldValue -> - tracestate_decode(FieldValue) - end. - -combine_headers(Key, Headers) -> - lists:foldl(fun({K, V}, Acc) -> - case string:equal(Key, string:casefold(K)) of - true -> - [Acc, $, | V]; - false -> - Acc - end - end, [], Headers). + unicode:characters_to_binary(StateHeaderValue). split(Pair) -> case string:split(Pair, "=", all) of @@ -127,7 +119,7 @@ decode(<>) when Version > ?VERSION andalso Version =/= <<"ff">> -> - to_span_ctx(Version, TraceId, SpanId, Opts); + to_span_ctx(Version, TraceId, SpanId, Opts); decode(_) -> undefined. @@ -144,13 +136,15 @@ to_span_ctx(Version, TraceId, SpanId, Opts) -> undefined end. +tracestate_decode(undefined) -> + []; tracestate_decode(Value) -> parse_pairs(string:lexemes(Value, [$,])). parse_pairs(Pairs) when length(Pairs) =< ?MAX_TRACESTATE_PAIRS -> parse_pairs(Pairs, []); parse_pairs(_) -> - undefined. + []. parse_pairs([], Acc) -> Acc; @@ -169,14 +163,3 @@ parse_pairs([Pair | Rest], Acc) -> undefined -> undefined end. -%% - -header_take(Key, Headers) -> - lists:dropwhile(fun({K, _}) -> - not string:equal(Key, string:casefold(K)) - end, Headers). - -header_member(_, []) -> - false; -header_member(Key, [{K, _} | T]) -> - string:equal(Key, string:casefold(K)) orelse header_member(Key, T). diff --git a/apps/opentelemetry_api/src/otel_span.erl b/apps/opentelemetry_api/src/otel_span.erl index 468e1ba6..4beeea87 100644 --- a/apps/opentelemetry_api/src/otel_span.erl +++ b/apps/opentelemetry_api/src/otel_span.erl @@ -70,8 +70,6 @@ span_id(#span_ctx{span_id=SpanId }) -> SpanId. -spec tracestate(opentelemetry:span_ctx() | undefined) -> opentelemetry:tracestate(). -tracestate(#span_ctx{tracestate=undefined}) -> - []; tracestate(#span_ctx{tracestate=Tracestate}) -> Tracestate; tracestate(_) -> diff --git a/apps/opentelemetry_api/src/otel_tracer.erl b/apps/opentelemetry_api/src/otel_tracer.erl index 647f4401..2cd2c7c0 100644 --- a/apps/opentelemetry_api/src/otel_tracer.erl +++ b/apps/opentelemetry_api/src/otel_tracer.erl @@ -27,7 +27,6 @@ set_current_span/2, current_span_ctx/0, current_span_ctx/1, - text_map_propagators/1, end_span/0, set_attribute/2, set_attributes/1, @@ -90,6 +89,7 @@ non_recording_span(TraceId, SpanId, Traceflags) -> from_remote_span(TraceId, SpanId, Traceflags) -> #span_ctx{trace_id=TraceId, span_id=SpanId, + is_valid=true, is_recording=false, is_remote=true, trace_flags=Traceflags}. @@ -110,14 +110,6 @@ current_span_ctx() -> current_span_ctx(Ctx) -> otel_ctx:get_value(Ctx, ?CURRENT_SPAN_CTX, undefined). --spec text_map_propagators(module()) -> {otel_propagator:text_map_extractor(), otel_propagator:text_map_injector()}. -text_map_propagators(Module) -> - ToText = fun Module:inject/1, - FromText = fun Module:extract/2, - Injector = otel_ctx:text_map_injector(?CURRENT_SPAN_CTX, ToText), - Extractor = otel_ctx:text_map_extractor(?CURRENT_SPAN_CTX, FromText), - {Extractor, Injector}. - %% Span operations -spec end_span() -> opentelemetry:span_ctx(). diff --git a/apps/opentelemetry_api/test/custom_propagator.erl b/apps/opentelemetry_api/test/custom_propagator.erl index 6f401396..75bdc496 100644 --- a/apps/opentelemetry_api/test/custom_propagator.erl +++ b/apps/opentelemetry_api/test/custom_propagator.erl @@ -17,7 +17,7 @@ %%%----------------------------------------------------------------------- -module(custom_propagator). --behaviour(otel_propagator). +-behaviour(otel_propagator_text_map). %% functions for interacting with the custom context key/value -export([add_to_context/1, @@ -26,12 +26,16 @@ %% functions for setting up the injector and extractor for custom context key %% as well as the propagator behaviour implementation inject/extract -export([propagators/0, - inject/1, - extract/2]). + fields/0, + inject/3, + extract/4]). -define(SOMETHING_CTX_KEY, ?MODULE). -define(SOMETHING_TEXT_ID, <<"something-header-id">>). +fields() -> + [?SOMETHING_TEXT_ID]. + add_to_context(Something) -> otel_ctx:set_value(?SOMETHING_CTX_KEY, Something). @@ -46,20 +50,15 @@ propagators() -> {Extract, Inject}. -inject(undefined) -> - []; -inject(Something) -> - [{?SOMETHING_TEXT_ID, Something}]. - -extract(TextMap, _) when is_list(TextMap) -> - case lists:search(fun({Key, _Value}) -> - string:equal(Key, ?SOMETHING_TEXT_ID, true, none) - end, TextMap) of - {value, {_, Value}} -> - Value; - false -> - undefined +inject(Ctx, Carrier, CarrierSet) -> + case otel_ctx:get_value(Ctx, ?SOMETHING_CTX_KEY, undefined) of + undefined -> + Carrier; + Value -> + CarrierSet(?SOMETHING_TEXT_ID, Value, Carrier) end. -%% +extract(Ctx, Carrier, _CarrierKeysFun, CarrierGet) -> + Value = CarrierGet(?SOMETHING_TEXT_ID, Carrier), + otel_ctx:set_value(Ctx, ?SOMETHING_CTX_KEY, Value). diff --git a/apps/opentelemetry_api/test/otel_propagators_SUITE.erl b/apps/opentelemetry_api/test/otel_propagators_SUITE.erl index baa73217..a748de97 100644 --- a/apps/opentelemetry_api/test/otel_propagators_SUITE.erl +++ b/apps/opentelemetry_api/test/otel_propagators_SUITE.erl @@ -16,7 +16,7 @@ <<"00-10000000000000000000000000000000-1000000000000000-00">>}]). all() -> - [{group, absence_of_an_installed_sdk}, custom_propagator]. + [rewrite, {group, absence_of_an_installed_sdk}, custom_propagator]. groups() -> %% Tests of Behavior of the API in the absence of an installed SDK @@ -27,16 +27,46 @@ groups() -> init_per_suite(Config) -> application:load(opentelemetry_api), - {Extractor, Injector} = custom_propagator:propagators(), - {W3CExtractor, W3CInjector} = otel_tracer_default:w3c_propagators(), - opentelemetry:set_text_map_extractors([Extractor, W3CExtractor]), - opentelemetry:set_text_map_injectors([Injector, W3CInjector]), - + opentelemetry:set_text_map_propagators([custom_propagator, otel_propagator_trace_context]), Config. end_per_suite(_Config) -> ok. +rewrite(_Config) -> + otel_ctx:clear(), + + RecordingSpanCtx = #span_ctx{trace_id=21267647932558653966460912964485513216, + span_id=1152921504606846976, + is_valid=true, + is_recording=true}, + otel_tracer:set_current_span(RecordingSpanCtx), + + Ctx = otel_ctx:get_current(), + ?assertMatch([{<<"traceparent">>, + <<"00-10000000000000000000000000000000-1000000000000000-00">>}], + otel_propagator_trace_context:inject(Ctx, [], + fun otel_propagator_text_map:default_carrier_set/3)), + + ?assertMatch([{<<"traceparent">>, + <<"00-10000000000000000000000000000000-1000000000000000-00">>}], + otel_propagator_text_map:inject([])), + + ?assertMatch(<<"c=d,a=b">>, + otel_propagator_text_map:default_carrier_get(<<"tracestate">>, + [{<<"tracestate">>,<<"c=d">>}, + {<<"traceparent">>, + <<"00-10000000000000000000000000000000-1000000000000000-00">>}, + {<<"tracestate">>,<<"a=b">>}])), + + + otel_propagator_text_map:extract([{<<"traceparent">>, + <<"00-10000000000000000000000000000000-1000000000000000-00">>}]), + ?assertEqual(RecordingSpanCtx#span_ctx{is_recording=false, + is_remote=true}, otel_tracer:current_span_ctx()), + + ok. + invalid_span_no_sdk_propagation(_Config) -> ct:comment("Test that a start_span called with an invalid span parent " "and no SDK results in the same invalid span as the child"), @@ -56,7 +86,7 @@ invalid_span_no_sdk_propagation(_Config) -> end), ?assertEqual(InvalidSpanCtx, otel_tracer:current_span_ctx()), - BinaryHeaders = otel_propagator:text_map_inject([]), + BinaryHeaders = otel_propagator_text_map:inject([]), %% invalid span contexts are skipped when injecting ?assertMatch([], BinaryHeaders), @@ -77,7 +107,7 @@ recording_no_sdk_propagation(_Config) -> ?assertNotEqual(RecordingSpanCtx, otel_tracer:current_span_ctx()) end), ?assertEqual(RecordingSpanCtx, otel_tracer:current_span_ctx()), - BinaryHeaders = otel_propagator:text_map_inject([]), + BinaryHeaders = otel_propagator_text_map:inject([]), ?assertMatch(?EXPECTED_HEADERS, BinaryHeaders), ok. @@ -90,11 +120,12 @@ nonrecording_no_sdk_propagation(_Config) -> NonRecordingSpanCtx = #span_ctx{trace_id=21267647932558653966460912964485513216, span_id=1152921504606846976, + is_valid=true, is_recording=false}, ?set_current_span(NonRecordingSpanCtx), - BinaryHeaders = otel_propagator:text_map_inject([]), + BinaryHeaders = otel_propagator_text_map:inject([]), %% is_recording will always be false in extracted `span_ctx' - otel_propagator:text_map_extract(BinaryHeaders), + otel_propagator_text_map:extract(BinaryHeaders), %% after being extracted `is_remote' will be set to `true' RemoteSpanCtx = NonRecordingSpanCtx#span_ctx{is_remote=true}, @@ -107,7 +138,7 @@ nonrecording_no_sdk_propagation(_Config) -> end), ?assertEqual(RemoteSpanCtx, otel_tracer:current_span_ctx()), - BinaryHeaders = otel_propagator:text_map_inject([]), + BinaryHeaders = otel_propagator_text_map:inject([]), ?assertMatch(?EXPECTED_HEADERS, BinaryHeaders), ok. @@ -116,7 +147,7 @@ custom_propagator(_Config) -> Something = <<"hello">>, custom_propagator:add_to_context(Something), - Headers = otel_propagator:text_map_inject([{<<"existing-header">>, <<"I exist">>}]), + Headers = otel_propagator_text_map:inject([{<<"existing-header">>, <<"I exist">>}]), ?assertListsMatch([{<<"something-header-id">>, Something}, {<<"existing-header">>, <<"I exist">>}], Headers), @@ -126,7 +157,7 @@ custom_propagator(_Config) -> %% make header keys uppercase to validate the extractor is case insensitive BinaryHeaders = [{string:uppercase(Key), iolist_to_binary(Value)} || {Key, Value} <- Headers], - otel_propagator:text_map_extract(BinaryHeaders), + otel_propagator_text_map:extract(BinaryHeaders), ?assertEqual(Something, custom_propagator:context_content()), diff --git a/apps/opentelemetry_exporter/src/opentelemetry_exporter.erl b/apps/opentelemetry_exporter/src/opentelemetry_exporter.erl index b2e5666c..582c5224 100644 --- a/apps/opentelemetry_exporter/src/opentelemetry_exporter.erl +++ b/apps/opentelemetry_exporter/src/opentelemetry_exporter.erl @@ -439,8 +439,6 @@ to_links([#link{trace_id=TraceId, attributes => to_attributes(Attributes), dropped_attributes_count => 0} | Acc]). -to_tracestate_string(undefined) -> - ""; to_tracestate_string(List) -> lists:join($,, [[Key, $=, Value] || {Key, Value} <- List]). diff --git a/website_docs/instrumentation.md b/website_docs/instrumentation.md index 57433436..830cc6dd 100644 --- a/website_docs/instrumentation.md +++ b/website_docs/instrumentation.md @@ -339,17 +339,15 @@ registered with OpenTelemetry. This can be done through configuration of the {{< tab >}} %% sys.config ... -{text_map_propagators, [fun otel_baggage:get_text_map_propagators/0, - fun otel_tracer_default:w3c_propagators/0]}, +{text_map_propagators, [baggage, + trace_context]}, ... {{< /tab >}} {{< tab >}} # runtime.exs ... -text_map_propagators: - [&:otel_baggage.get_text_map_propagators/0, - &:otel_tracer_default.w3c_propagators/0], +text_map_propagators: [:baggage, :tracer_context], ... {{< /tab >}} @@ -357,11 +355,8 @@ text_map_propagators: If you instead need to use the [B3 specification](https://github.com/openzipkin/b3-propagation), originally from -the [Zipkin project](https://zipkin.io/), then replace -`otel_tracer_default:w3c_propagators/0` and -`&:otel_tracer_default.w3c_propagators/0` with `fun -otel_tracer_default:b3_propagators/0` and -`&:otel_tracer_default.b3_propagators/0` for Erlang or Elixir respectively. +the [Zipkin project](https://zipkin.io/), then replace `trace_context` and +`:trace_context` with `b3` and `:b3` for Erlang or Elixir respectively. # Library Instrumentation