diff --git a/api/include/opentelemetry/nostd/string_view.h b/api/include/opentelemetry/nostd/string_view.h index 3f68b029b3..c4ed15be09 100644 --- a/api/include/opentelemetry/nostd/string_view.h +++ b/api/include/opentelemetry/nostd/string_view.h @@ -120,6 +120,20 @@ class string_view return substr(pos1, count1).compare(string_view(s, count2)); }; + size_type find(char ch, size_type pos = 0) const noexcept + { + size_type res = npos; + if (pos < length()) + { + auto found = Traits::find(data() + pos, length() - pos, ch); + if (found) + { + res = found - data(); + } + } + return res; + } + bool operator<(const string_view v) const noexcept { return compare(v) < 0; } bool operator>(const string_view v) const noexcept { return compare(v) > 0; } diff --git a/api/include/opentelemetry/trace/propagation/b3_propagator.h b/api/include/opentelemetry/trace/propagation/b3_propagator.h new file mode 100644 index 0000000000..bf69663dc2 --- /dev/null +++ b/api/include/opentelemetry/trace/propagation/b3_propagator.h @@ -0,0 +1,273 @@ +#pragma once + +#include +#include +#include +#include +#include "opentelemetry/common/key_value_iterable.h" +#include "opentelemetry/context/context.h" +#include "opentelemetry/nostd/shared_ptr.h" +#include "opentelemetry/nostd/span.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/nostd/variant.h" +#include "opentelemetry/trace/default_span.h" +#include "opentelemetry/trace/propagation/http_text_format.h" +#include "opentelemetry/trace/span.h" +#include "opentelemetry/trace/span_context.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace trace +{ +namespace propagation +{ + +static const nostd::string_view kB3CombinedHeader = "b3"; + +static const nostd::string_view kB3TraceIdHeader = "X-B3-TraceId"; +static const nostd::string_view kB3SpanIdHeader = "X-B3-SpanId"; +static const nostd::string_view kB3SampledHeader = "X-B3-Sampled"; + +/* + B3, single header: + b3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^ + 0 TraceId 31 33 SpanId 48 | 52 ParentSpanId 68 + 50 Debug flag + Multiheader version: X-B3-Sampled + X-B3-TraceId X-B3-SpanId X-B3-ParentSpanId (ignored) +*/ + +static const int kTraceIdHexStrLength = 32; +static const int kSpanIdHexStrLength = 16; +static const int kTraceFlagHexStrLength = 1; + +// The B3PropagatorExtractor class provides an interface that enables extracting context from +// headers of HTTP requests. HTTP frameworks and clients can integrate with B3Propagator by +// providing the object containing the headers, and a getter function for the extraction. Based on: +// https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/context/api-propagators.md#b3-extract +template +class B3PropagatorExtractor : public HTTPTextFormat +{ +public: + // Rules that manages how context will be extracted from carrier. + using Getter = nostd::string_view (*)(const T &carrier, nostd::string_view trace_type); + + // Returns the context that is stored in the HTTP header carrier with the getter as extractor. + context::Context Extract(Getter getter, + const T &carrier, + context::Context &context) noexcept override + { + SpanContext span_context = ExtractImpl(getter, carrier); + nostd::shared_ptr sp{new DefaultSpan(span_context)}; + return context.SetValue(kSpanKey, sp); + } + + static SpanContext GetCurrentSpan(const context::Context &context) + { + context::Context ctx(context); + context::ContextValue span = ctx.GetValue(kSpanKey); + if (nostd::holds_alternative>(span)) + { + return nostd::get>(span).get()->GetContext(); + } + return SpanContext::GetInvalid(); + } + + static TraceId GenerateTraceIdFromString(nostd::string_view trace_id) + { + uint8_t buf[kTraceIdHexStrLength / 2]; + GenerateBuffFromHexStrPad0(trace_id, sizeof(buf), buf); + return TraceId(buf); + } + + static SpanId GenerateSpanIdFromString(nostd::string_view span_id) + { + uint8_t buf[kSpanIdHexStrLength / 2]; + GenerateBuffFromHexStrPad0(span_id, sizeof(buf), buf); + return SpanId(buf); + } + + static TraceFlags GenerateTraceFlagsFromString(nostd::string_view trace_flags) + { + if (trace_flags.length() != 1 || (trace_flags[0] != '1' && trace_flags[0] != 'd')) + { // check for invalid length of flags and treat 'd' as sampled + return TraceFlags(0); + } + return TraceFlags(TraceFlags::kIsSampled); + } + +private: + // Converts hex numbers (string_view) into bytes stored in a buffer and pads buffer with 0. + static void GenerateBuffFromHexStrPad0(nostd::string_view hexStr, int bufSize, uint8_t *buf) + { // we are doing this starting from "right" side for left-padding + nostd::string_view::size_type posInp = hexStr.length(); + int posOut = bufSize; + while (posOut--) + { + int val = 0; + if (posInp) + { + int hexDigit2 = HexToInt(hexStr[--posInp]); // low nibble + int hexDigit1 = 0; + if (posInp) + { + hexDigit1 = HexToInt(hexStr[--posInp]); + } + if (hexDigit1 < 0 || hexDigit2 < 0) + { // malformed hex sequence. Fill entire buffer with zeroes. + for (int j = 0; j < bufSize; j++) + { + buf[j] = 0; + } + return; + } + val = hexDigit1 * 16 + hexDigit2; + } + buf[posOut] = val; + } + } + + // Converts a single character to a corresponding integer (e.g. '1' to 1), return -1 + // if the character is not a valid number in hex. + static int8_t HexToInt(char c) + { + if (c >= '0' && c <= '9') + { + return (int8_t)(c - '0'); + } + else if (c >= 'a' && c <= 'f') + { + return (int8_t)(c - 'a' + 10); + } + else if (c >= 'A' && c <= 'F') + { + return (int8_t)(c - 'A' + 10); + } + else + { + return -1; + } + } + + static SpanContext ExtractImpl(Getter getter, const T &carrier) + { + // all these are hex values + nostd::string_view trace_id; + nostd::string_view span_id; + nostd::string_view trace_flags; + + // first let's try a single-header variant + auto singleB3Header = getter(carrier, kB3CombinedHeader); + if (!singleB3Header.empty()) + { + // From: https://github.com/openzipkin/b3-propagation/blob/master/RATIONALE.md + // trace_id can be 16 or 32 chars + auto firstSep = singleB3Header.find('-'); + trace_id = singleB3Header.substr(0, firstSep); + if (firstSep != nostd::string_view::npos) + { // at least two fields are required + auto secondSep = singleB3Header.find('-', firstSep + 1); + if (secondSep != nostd::string_view::npos) + { // more than two fields - check also trace_flags + span_id = singleB3Header.substr(firstSep + 1, secondSep - firstSep - 1); + if (secondSep + 1 < singleB3Header.size()) + { + trace_flags = singleB3Header.substr(secondSep + 1, kTraceFlagHexStrLength); + } + } + else + { + span_id = singleB3Header.substr(firstSep + 1); + } + } + } + else + { + trace_id = getter(carrier, kB3TraceIdHeader); + span_id = getter(carrier, kB3SpanIdHeader); + trace_flags = getter(carrier, kB3SampledHeader); + } + + // now convert hex to objects + TraceId trace_id_obj = GenerateTraceIdFromString(trace_id); + SpanId span_id_obj = GenerateSpanIdFromString(span_id); + if (!trace_id_obj.IsValid() || !span_id_obj.IsValid()) + { + return SpanContext(false, false); + } + TraceFlags trace_flags_obj = GenerateTraceFlagsFromString(trace_flags); + return SpanContext(trace_id_obj, span_id_obj, trace_flags_obj, true); + } +}; + +// The B3Propagator class provides interface that enables extracting and injecting context into +// single header of HTTP Request. +template +class B3Propagator : public B3PropagatorExtractor +{ +public: + // Rules that manages how context will be injected to carrier. + using Setter = void (*)(T &carrier, + nostd::string_view trace_type, + nostd::string_view trace_description); + // Sets the context for a HTTP header carrier with self defined rules. + void Inject(Setter setter, T &carrier, const context::Context &context) noexcept override + { + SpanContext span_context = B3PropagatorExtractor::GetCurrentSpan(context); + if (!span_context.IsValid()) + { + return; + } + char trace_id[kTraceIdHexStrLength]; + TraceId(span_context.trace_id()).ToLowerBase16(trace_id); + char span_id[kSpanIdHexStrLength]; + SpanId(span_context.span_id()).ToLowerBase16(span_id); + char trace_flags[2]; + TraceFlags(span_context.trace_flags()).ToLowerBase16(trace_flags); + // Note: This is only temporary replacement for appendable string + std::string hex_string = ""; + for (int i = 0; i < 32; i++) + { + hex_string.push_back(trace_id[i]); + } + hex_string.push_back('-'); + for (int i = 0; i < 16; i++) + { + hex_string.push_back(span_id[i]); + } + hex_string.push_back('-'); + hex_string.push_back(trace_flags[1]); + setter(carrier, kB3CombinedHeader, hex_string); + } +}; + +template +class B3PropagatorMultiHeader : public B3PropagatorExtractor +{ +public: + // Rules that manages how context will be injected to carrier. + using Setter = void (*)(T &carrier, + nostd::string_view trace_type, + nostd::string_view trace_description); + void Inject(Setter setter, T &carrier, const context::Context &context) noexcept override + { + SpanContext span_context = B3PropagatorExtractor::GetCurrentSpan(context); + if (!span_context.IsValid()) + { + return; + } + char trace_id[32]; + TraceId(span_context.trace_id()).ToLowerBase16(trace_id); + char span_id[16]; + SpanId(span_context.span_id()).ToLowerBase16(span_id); + char trace_flags[2]; + TraceFlags(span_context.trace_flags()).ToLowerBase16(trace_flags); + setter(carrier, kB3TraceIdHeader, nostd::string_view(trace_id, sizeof(trace_id))); + setter(carrier, kB3SpanIdHeader, nostd::string_view(span_id, sizeof(span_id))); + setter(carrier, kB3SampledHeader, nostd::string_view(trace_flags + 1, 1)); + } +}; + +} // namespace propagation +} // namespace trace +OPENTELEMETRY_END_NAMESPACE diff --git a/api/test/nostd/string_view_test.cc b/api/test/nostd/string_view_test.cc index 3673082cca..4e796c0db4 100644 --- a/api/test/nostd/string_view_test.cc +++ b/api/test/nostd/string_view_test.cc @@ -75,6 +75,24 @@ TEST(StringViewTest, SubstrOutOfRange) #endif } +TEST(StringViewTest, FindSingleCharacter) +{ + string_view s = "abc"; + + // starting from 0-th position (default) + EXPECT_EQ(s.find('a'), 0); + EXPECT_EQ(s.find('b'), 1); + EXPECT_EQ(s.find('c'), 2); + EXPECT_EQ(s.find('d'), -1); // FIXME: string_view:npos - problem with linker + + // starting from given index + EXPECT_EQ(s.find('a', 1), -1); + EXPECT_EQ(s.find('b', 1), 1); + + // out of index + EXPECT_EQ(s.find('a', 10), -1); +} + TEST(StringViewTest, Compare) { string_view s1 = "aaa"; diff --git a/api/test/trace/propagation/CMakeLists.txt b/api/test/trace/propagation/CMakeLists.txt index 8f13cd68de..99de61f1c4 100644 --- a/api/test/trace/propagation/CMakeLists.txt +++ b/api/test/trace/propagation/CMakeLists.txt @@ -1,4 +1,4 @@ -foreach(testname http_text_format_test) +foreach(testname http_text_format_test b3_propagation_test) add_executable(${testname} "${testname}.cc") target_link_libraries( ${testname} ${GTEST_BOTH_LIBRARIES} ${CORE_RUNTIME_LIBS} diff --git a/api/test/trace/propagation/b3_propagation_test.cc b/api/test/trace/propagation/b3_propagation_test.cc new file mode 100644 index 0000000000..ebf178d6a2 --- /dev/null +++ b/api/test/trace/propagation/b3_propagation_test.cc @@ -0,0 +1,218 @@ +#include "opentelemetry/context/context.h" +#include "opentelemetry/nostd/shared_ptr.h" +#include "opentelemetry/nostd/span.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/trace/default_span.h" +#include "opentelemetry/trace/noop.h" +#include "opentelemetry/trace/span.h" +#include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/trace_id.h" +#include "opentelemetry/trace/tracer.h" + +#include +#include +#include + +#include + +#include "opentelemetry/trace/default_span.h" +#include "opentelemetry/trace/propagation/b3_propagator.h" +#include "opentelemetry/trace/propagation/http_text_format.h" + +using namespace opentelemetry; + +template +static std::string Hex(const T &id_item) +{ + char buf[T::kSize * 2]; + id_item.ToLowerBase16(buf); + return std::string(buf, sizeof(buf)); +} + +static nostd::string_view Getter(const std::map &carrier, + nostd::string_view key) +{ + auto it = carrier.find(std::string(key)); + if (it != carrier.end()) + { + return nostd::string_view(it->second); + } + return ""; +} + +static void Setter(std::map &carrier, + nostd::string_view key, + nostd::string_view value = "") +{ + carrier[std::string(key)] = std::string(value); +} + +using MapB3Context = trace::propagation::B3Propagator>; + +static MapB3Context format = MapB3Context(); + +using MapB3ContextMultiHeader = + trace::propagation::B3PropagatorMultiHeader>; + +static MapB3ContextMultiHeader formatMultiHeader = MapB3ContextMultiHeader(); + +TEST(B3PropagationTest, TraceIdBufferGeneration) +{ + constexpr uint8_t buf[] = {1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}; + EXPECT_EQ(MapB3Context::GenerateTraceIdFromString("01020304050607080807aabbccddeeff"), + trace::TraceId(buf)); +} + +TEST(B3PropagationTest, SpanIdBufferGeneration) +{ + constexpr uint8_t buf[] = {1, 2, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}; + EXPECT_EQ(MapB3Context::GenerateSpanIdFromString("0102aabbccddeeff"), trace::SpanId(buf)); +} + +TEST(B3PropagationTest, TraceFlagsBufferGeneration) +{ + EXPECT_EQ(MapB3Context::GenerateTraceFlagsFromString("0"), trace::TraceFlags()); + EXPECT_EQ(MapB3Context::GenerateTraceFlagsFromString("1"), + trace::TraceFlags(trace::TraceFlags::kIsSampled)); +} + +TEST(B3PropagationTest, PropagateInvalidContext) +{ + // Do not propagate invalid trace context. + std::map carrier = {}; + context::Context ctx{ + "current-span", + nostd::shared_ptr(new trace::DefaultSpan(trace::SpanContext::GetInvalid()))}; + format.Inject(Setter, carrier, ctx); + EXPECT_TRUE(carrier.count("b3") == 0); +} + +TEST(B3PropagationTest, ExtractInvalidContext) +{ + const std::map carrier = { + {"b3", "00000000000000000000000000000000-0000000000000000-0"}}; + context::Context ctx1 = context::Context{}; + context::Context ctx2 = format.Extract(Getter, carrier, ctx1); + auto ctx2_span = ctx2.GetValue(trace::kSpanKey); + auto span = nostd::get>(ctx2_span); + EXPECT_EQ(span->GetContext().IsRemote(), false); +} + +TEST(B3PropagationTest, DoNotExtractWithInvalidHex) +{ + const std::map carrier = { + {"b3", "0000000zzz0000000000000000000000-0000000zzz000000-1"}}; + context::Context ctx1 = context::Context{}; + context::Context ctx2 = format.Extract(Getter, carrier, ctx1); + auto ctx2_span = ctx2.GetValue(trace::kSpanKey); + auto span = nostd::get>(ctx2_span); + EXPECT_EQ(span->GetContext().IsRemote(), false); +} + +TEST(B3PropagationTest, SetRemoteSpan) +{ + const std::map carrier = { + {"b3", "80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90"}}; + context::Context ctx1 = context::Context{}; + context::Context ctx2 = format.Extract(Getter, carrier, ctx1); + + auto ctx2_span = ctx2.GetValue(trace::kSpanKey); + EXPECT_TRUE(nostd::holds_alternative>(ctx2_span)); + + auto span = nostd::get>(ctx2_span); + + EXPECT_EQ(Hex(span->GetContext().trace_id()), "80f198ee56343ba864fe8b2a57d3eff7"); + EXPECT_EQ(Hex(span->GetContext().span_id()), "e457b5a2e4d86bd1"); + EXPECT_EQ(span->GetContext().IsSampled(), true); + EXPECT_EQ(span->GetContext().IsRemote(), true); +} + +TEST(B3PropagationTest, SetRemoteSpan_TraceIdShort) +{ + const std::map carrier = { + {"b3", "80f198ee56343ba8-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90"}}; + context::Context ctx1 = context::Context{}; + context::Context ctx2 = format.Extract(Getter, carrier, ctx1); + + auto ctx2_span = ctx2.GetValue(trace::kSpanKey); + EXPECT_TRUE(nostd::holds_alternative>(ctx2_span)); + + auto span = nostd::get>(ctx2_span); + + EXPECT_EQ(Hex(span->GetContext().trace_id()), "000000000000000080f198ee56343ba8"); + EXPECT_EQ(Hex(span->GetContext().span_id()), "e457b5a2e4d86bd1"); + EXPECT_EQ(span->GetContext().IsSampled(), true); + EXPECT_EQ(span->GetContext().IsRemote(), true); +} + +TEST(B3PropagationTest, SetRemoteSpan_SingleHeaderNoFlags) +{ + const std::map carrier = { + {"b3", "80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1"}}; + context::Context ctx1 = context::Context{}; + context::Context ctx2 = format.Extract(Getter, carrier, ctx1); + + auto ctx2_span = ctx2.GetValue(trace::kSpanKey); + EXPECT_TRUE(nostd::holds_alternative>(ctx2_span)); + + auto span = nostd::get>(ctx2_span); + + EXPECT_EQ(Hex(span->GetContext().trace_id()), "80f198ee56343ba864fe8b2a57d3eff7"); + EXPECT_EQ(Hex(span->GetContext().span_id()), "e457b5a2e4d86bd1"); + EXPECT_EQ(span->GetContext().IsSampled(), false); + EXPECT_EQ(span->GetContext().IsRemote(), true); +} + +TEST(B3PropagationTest, SetRemoteSpanMultiHeader) +{ + const std::map carrier = { + {"X-B3-TraceId", "80f198ee56343ba864fe8b2a57d3eff7"}, + {"X-B3-SpanId", "e457b5a2e4d86bd1"}, + {"X-B3-Sampled", "1"}}; + context::Context ctx1 = context::Context{}; + context::Context ctx2 = format.Extract(Getter, carrier, ctx1); + + auto ctx2_span = ctx2.GetValue(trace::kSpanKey); + EXPECT_TRUE(nostd::holds_alternative>(ctx2_span)); + + auto span = nostd::get>(ctx2_span); + + EXPECT_EQ(Hex(span->GetContext().trace_id()), "80f198ee56343ba864fe8b2a57d3eff7"); + EXPECT_EQ(Hex(span->GetContext().span_id()), "e457b5a2e4d86bd1"); + EXPECT_EQ(span->GetContext().IsSampled(), true); + EXPECT_EQ(span->GetContext().IsRemote(), true); +} + +TEST(B3PropagationTest, GetCurrentSpan) +{ + constexpr uint8_t buf_span[] = {1, 2, 3, 4, 5, 6, 7, 8}; + constexpr uint8_t buf_trace[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + trace::SpanContext span_context{trace::TraceId{buf_trace}, trace::SpanId{buf_span}, + trace::TraceFlags{true}, false}; + nostd::shared_ptr sp{new trace::DefaultSpan{span_context}}; + + // Set `sp` as the currently active span, which must be used by `Inject`. + trace::Scope scoped_span{sp}; + + std::map headers = {}; + format.Inject(Setter, headers, context::RuntimeContext::GetCurrent()); + EXPECT_EQ(headers["b3"], "0102030405060708090a0b0c0d0e0f10-0102030405060708-1"); +} + +TEST(B3PropagationTest, GetCurrentSpanMultiHeader) +{ + constexpr uint8_t buf_span[] = {1, 2, 3, 4, 5, 6, 7, 8}; + constexpr uint8_t buf_trace[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + trace::SpanContext span_context{trace::TraceId{buf_trace}, trace::SpanId{buf_span}, + trace::TraceFlags{true}, false}; + nostd::shared_ptr sp{new trace::DefaultSpan{span_context}}; + + // Set `sp` as the currently active span, which must be used by `Inject`. + trace::Scope scoped_span{sp}; + + std::map headers = {}; + formatMultiHeader.Inject(Setter, headers, context::RuntimeContext::GetCurrent()); + EXPECT_EQ(headers["X-B3-TraceId"], "0102030405060708090a0b0c0d0e0f10"); + EXPECT_EQ(headers["X-B3-SpanId"], "0102030405060708"); + EXPECT_EQ(headers["X-B3-Sampled"], "1"); +}