-
Notifications
You must be signed in to change notification settings - Fork 525
[API] Environment Variables as Context Propagation Carriers #3834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
marcalff
merged 11 commits into
open-telemetry:main
from
perhapsmaple:environment-carrier
Feb 24, 2026
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
3f222ea
Environment Variables as Context Propagation Carriers
perhapsmaple 6905b6f
Merge branch 'main' into environment-carrier
ThomsonTan 4b3095a
Redesign EnvironmentCarrier to address review comments
perhapsmaple 72b6b9a
Unit tests for EnvironmentCarrier
perhapsmaple e963aaf
Example for EnvironmentCarrier
perhapsmaple c99b867
Fix CI issues
perhapsmaple 52f8cd5
Remove ifdef from example
perhapsmaple dcce272
Fix iwyu ci
perhapsmaple 2ed4737
Merge branch 'main' into environment-carrier
marcalff 061209c
Address review comments
perhapsmaple 5b005eb
Merge branch 'main' into environment-carrier
marcalff File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
108 changes: 108 additions & 0 deletions
108
api/include/opentelemetry/context/propagation/environment_carrier.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <algorithm> | ||
| #include <cctype> | ||
| #include <cstdlib> | ||
| #include <map> | ||
| #include <memory> | ||
| #include <string> | ||
|
|
||
| #include "opentelemetry/context/propagation/text_map_propagator.h" | ||
| #include "opentelemetry/nostd/string_view.h" | ||
| #include "opentelemetry/version.h" | ||
|
|
||
| OPENTELEMETRY_BEGIN_NAMESPACE | ||
| namespace context | ||
| { | ||
| namespace propagation | ||
| { | ||
|
|
||
| // EnvironmentCarrier is a TextMapCarrier that reads from and writes to environment variables. | ||
| // | ||
| // This carrier enables context propagation across process boundaries using environment variables | ||
| // as specified in the OpenTelemetry specification: | ||
| // https://opentelemetry.io/docs/specs/otel/context/env-carriers/ | ||
| // | ||
| // The carrier supports two usage scenarios: | ||
| // | ||
| // 1. Extract (default constructor): Reads context from environment variables. | ||
| // Get() reads from TRACEPARENT, TRACESTATE, BAGGAGE environment variables. | ||
| // Set() is a no-op. Values are cached on first access for lifetime management. | ||
| // | ||
| // 2. Inject (shared_ptr constructor): Writes context to a provided map. | ||
| // Set() writes to the provided std::map. Keys are automatically converted | ||
| // from lowercase header names to uppercase environment variable names. | ||
|
|
||
| class EnvironmentCarrier : public TextMapCarrier | ||
| { | ||
| public: | ||
| // Constructs an EnvironmentCarrier for Extract operations. | ||
| EnvironmentCarrier() noexcept = default; | ||
|
|
||
| // Constructs an EnvironmentCarrier for Inject operations. | ||
| explicit EnvironmentCarrier(std::shared_ptr<std::map<std::string, std::string>> env_map) noexcept | ||
| : env_map_ptr_(std::move(env_map)) | ||
| {} | ||
|
|
||
| // Returns the value associated with the passed key. | ||
| // Always reads from process environment variables (with caching). | ||
| // The key is automatically converted to uppercase. | ||
| nostd::string_view Get(nostd::string_view key) const noexcept override | ||
| { | ||
| std::string env_name = ToEnvName(key); | ||
|
|
||
| // Check cache first | ||
| auto cache_it = cache_.find(std::string(key)); | ||
| if (cache_it != cache_.end()) | ||
| { | ||
| return cache_it->second; | ||
| } | ||
|
|
||
| // Read from environment | ||
| const char *value = std::getenv(env_name.c_str()); | ||
| if (value != nullptr) | ||
| { | ||
| // Cache for lifetime management (string_view requires stable storage) | ||
| cache_[std::string(key)] = std::string(value); | ||
| return cache_[std::string(key)]; | ||
| } | ||
| return ""; | ||
| } | ||
|
|
||
| // Stores the key-value pair in the map if one was provided at construction. | ||
| // Otherwise, this operation is a no-op. | ||
| // The key is automatically converted to uppercase. | ||
| void Set(nostd::string_view key, nostd::string_view value) noexcept override | ||
| { | ||
| if (!env_map_ptr_) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| std::string env_name = ToEnvName(key); | ||
| env_map_ptr_->operator[](env_name) = std::string(value); | ||
| } | ||
|
|
||
| private: | ||
| std::shared_ptr<std::map<std::string, std::string>> env_map_ptr_; | ||
| mutable std::map<std::string, std::string> cache_; | ||
|
|
||
| // Converts a header name to an environment variable name. | ||
marcalff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // e.g., "traceparent" -> "TRACEPARENT", "my-key" -> "MY_KEY", | ||
| // "my.complex.key" -> "MY_COMPLEX_KEY" | ||
| static std::string ToEnvName(nostd::string_view key) | ||
| { | ||
| std::string env_name(key); | ||
| std::transform(env_name.begin(), env_name.end(), env_name.begin(), [](unsigned char c) { | ||
| return static_cast<char>(std::isalnum(c) ? std::toupper(c) : '_'); | ||
| }); | ||
| return env_name; | ||
| } | ||
| }; | ||
|
|
||
| } // namespace propagation | ||
| } // namespace context | ||
| OPENTELEMETRY_END_NAMESPACE | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
252 changes: 252 additions & 0 deletions
252
api/test/context/propagation/environment_carrier_test.cc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,252 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| #include <gtest/gtest.h> | ||
| #include <stdint.h> | ||
| #include <stdlib.h> | ||
| #include <map> | ||
| #include <string> | ||
| #include <utility> | ||
|
|
||
| #include "opentelemetry/context/context.h" | ||
| #include "opentelemetry/context/propagation/environment_carrier.h" | ||
| #include "opentelemetry/context/runtime_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_trace_context.h" | ||
| #include "opentelemetry/trace/scope.h" | ||
| #include "opentelemetry/trace/span.h" | ||
| #include "opentelemetry/trace/span_context.h" | ||
| #include "opentelemetry/trace/span_id.h" | ||
| #include "opentelemetry/trace/span_metadata.h" | ||
| #include "opentelemetry/trace/trace_flags.h" | ||
| #include "opentelemetry/trace/trace_id.h" | ||
| #include "opentelemetry/trace/trace_state.h" | ||
|
|
||
| // Platform-portable setenv/unsetenv wrappers | ||
| #ifdef _WIN32 | ||
| static void test_setenv(const char *name, const char *value) | ||
| { | ||
| _putenv_s(name, value); | ||
| } | ||
| static void test_unsetenv(const char *name) | ||
| { | ||
| _putenv_s(name, ""); | ||
| } | ||
| #else | ||
| static void test_setenv(const char *name, const char *value) | ||
| { | ||
| ::setenv(name, value, 1); | ||
| } | ||
| static void test_unsetenv(const char *name) | ||
| { | ||
| ::unsetenv(name); | ||
| } | ||
| #endif | ||
|
|
||
| using namespace opentelemetry; | ||
|
|
||
| template <typename T> | ||
| static std::string Hex(const T &id_item) | ||
| { | ||
| char buf[T::kSize * 2]; | ||
| id_item.ToLowerBase16(buf); | ||
| return std::string(buf, sizeof(buf)); | ||
| } | ||
|
|
||
| class EnvironmentCarrierTest : public ::testing::Test | ||
| { | ||
| protected: | ||
| void SetUp() override | ||
| { | ||
| test_unsetenv("TRACEPARENT"); | ||
| test_unsetenv("TRACESTATE"); | ||
| test_unsetenv("BAGGAGE"); | ||
| } | ||
|
|
||
| void TearDown() override | ||
| { | ||
| test_unsetenv("TRACEPARENT"); | ||
| test_unsetenv("TRACESTATE"); | ||
| test_unsetenv("BAGGAGE"); | ||
| } | ||
| }; | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, GetReadsFromEnvironment) | ||
| { | ||
| test_setenv("TRACEPARENT", "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
|
|
||
| context::propagation::EnvironmentCarrier carrier; | ||
| auto value = carrier.Get("traceparent"); | ||
| EXPECT_EQ(value, "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, GetReturnsEmptyForMissingKey) | ||
| { | ||
| context::propagation::EnvironmentCarrier carrier; | ||
| auto value = carrier.Get("traceparent"); | ||
| EXPECT_EQ(value, ""); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, GetCachesValues) | ||
| { | ||
| test_setenv("TRACEPARENT", "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
|
|
||
| context::propagation::EnvironmentCarrier carrier; | ||
|
|
||
| // First call reads from environment | ||
| auto value1 = carrier.Get("traceparent"); | ||
| EXPECT_EQ(value1, "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
|
|
||
| // Change environment - cached value should be returned | ||
| test_setenv("TRACEPARENT", "changed-value"); | ||
| auto value2 = carrier.Get("traceparent"); | ||
| EXPECT_EQ(value2, "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, SetNoOpWithoutMap) | ||
| { | ||
| context::propagation::EnvironmentCarrier carrier; | ||
| // Should not crash | ||
| carrier.Set("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, SetWritesToMap) | ||
| { | ||
| auto env_map = std::make_shared<std::map<std::string, std::string>>(); | ||
| context::propagation::EnvironmentCarrier carrier(env_map); | ||
|
|
||
| carrier.Set("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
| EXPECT_EQ(env_map->at("TRACEPARENT"), "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, SetUppercaseConversion) | ||
| { | ||
| auto env_map = std::make_shared<std::map<std::string, std::string>>(); | ||
| context::propagation::EnvironmentCarrier carrier(env_map); | ||
|
|
||
| carrier.Set("tracestate", "key=value"); | ||
| EXPECT_EQ(env_map->count("TRACESTATE"), 1u); | ||
| EXPECT_EQ(env_map->at("TRACESTATE"), "key=value"); | ||
|
|
||
| // Original lowercase key should not be in the map | ||
| EXPECT_EQ(env_map->count("tracestate"), 0u); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, ExtractTraceContext) | ||
| { | ||
| test_setenv("TRACEPARENT", "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
|
|
||
| context::propagation::EnvironmentCarrier carrier; | ||
| trace::propagation::HttpTraceContext propagator; | ||
| context::Context ctx; | ||
| auto new_ctx = propagator.Extract(carrier, ctx); | ||
|
|
||
| auto span_ctx_val = new_ctx.GetValue(trace::kSpanKey); | ||
| EXPECT_TRUE(nostd::holds_alternative<nostd::shared_ptr<trace::Span>>(span_ctx_val)); | ||
|
|
||
| auto span = nostd::get<nostd::shared_ptr<trace::Span>>(span_ctx_val); | ||
| EXPECT_EQ(Hex(span->GetContext().trace_id()), "4bf92f3577b34da6a3ce929d0e0e4736"); | ||
| EXPECT_EQ(Hex(span->GetContext().span_id()), "0102030405060708"); | ||
| EXPECT_TRUE(span->GetContext().IsSampled()); | ||
| EXPECT_TRUE(span->GetContext().IsRemote()); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, ExtractWithTraceState) | ||
| { | ||
| test_setenv("TRACEPARENT", "00-4bf92f3577b34da6a3ce929d0e0e4736-0102030405060708-01"); | ||
| test_setenv("TRACESTATE", "congo=t61rcWkgMzE"); | ||
|
|
||
| context::propagation::EnvironmentCarrier carrier; | ||
| trace::propagation::HttpTraceContext propagator; | ||
| context::Context ctx; | ||
| auto new_ctx = propagator.Extract(carrier, ctx); | ||
|
|
||
| auto span_ctx_val = new_ctx.GetValue(trace::kSpanKey); | ||
| auto span = nostd::get<nostd::shared_ptr<trace::Span>>(span_ctx_val); | ||
|
|
||
| EXPECT_EQ(Hex(span->GetContext().trace_id()), "4bf92f3577b34da6a3ce929d0e0e4736"); | ||
|
|
||
| auto trace_state = span->GetContext().trace_state(); | ||
| ASSERT_NE(trace_state, nullptr); | ||
|
|
||
| std::string congo_val; | ||
| trace_state->Get("congo", congo_val); | ||
| EXPECT_EQ(congo_val, "t61rcWkgMzE"); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, ExtractNoEnvVars) | ||
| { | ||
| context::propagation::EnvironmentCarrier carrier; | ||
| trace::propagation::HttpTraceContext propagator; | ||
| context::Context ctx; | ||
| auto new_ctx = propagator.Extract(carrier, ctx); | ||
|
|
||
| auto span_ctx_val = new_ctx.GetValue(trace::kSpanKey); | ||
| // With no env vars, Extract should produce an invalid span context | ||
| if (nostd::holds_alternative<nostd::shared_ptr<trace::Span>>(span_ctx_val)) | ||
| { | ||
| auto span = nostd::get<nostd::shared_ptr<trace::Span>>(span_ctx_val); | ||
| EXPECT_FALSE(span->GetContext().IsValid()); | ||
| } | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, InjectTraceContext) | ||
| { | ||
| 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<trace::Span> sp{new trace::DefaultSpan{span_context}}; | ||
|
|
||
| trace::Scope scoped_span{sp}; | ||
|
|
||
| auto env_map = std::make_shared<std::map<std::string, std::string>>(); | ||
| context::propagation::EnvironmentCarrier carrier(env_map); | ||
| trace::propagation::HttpTraceContext propagator; | ||
|
|
||
| propagator.Inject(carrier, context::RuntimeContext::GetCurrent()); | ||
|
|
||
| EXPECT_EQ(env_map->at("TRACEPARENT"), "00-0102030405060708090a0b0c0d0e0f10-0102030405060708-01"); | ||
| } | ||
|
|
||
| TEST_F(EnvironmentCarrierTest, RoundTrip) | ||
| { | ||
| // Step 1: Create a span context and inject it into a map | ||
| constexpr uint8_t buf_span[] = {0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x9A}; | ||
| constexpr uint8_t buf_trace[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, | ||
| 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}; | ||
| trace::SpanContext span_context{trace::TraceId{buf_trace}, trace::SpanId{buf_span}, | ||
| trace::TraceFlags{true}, false}; | ||
| nostd::shared_ptr<trace::Span> sp{new trace::DefaultSpan{span_context}}; | ||
| trace::Scope scoped_span{sp}; | ||
|
|
||
| auto env_map = std::make_shared<std::map<std::string, std::string>>(); | ||
| context::propagation::EnvironmentCarrier inject_carrier(env_map); | ||
| trace::propagation::HttpTraceContext propagator; | ||
|
|
||
| propagator.Inject(inject_carrier, context::RuntimeContext::GetCurrent()); | ||
|
|
||
| // Step 2: Set the injected values as environment variables | ||
| for (const auto &entry : *env_map) | ||
| { | ||
| test_setenv(entry.first.c_str(), entry.second.c_str()); | ||
| } | ||
|
|
||
| // Step 3: Extract from environment using a new carrier | ||
| context::propagation::EnvironmentCarrier extract_carrier; | ||
| context::Context ctx; | ||
| auto new_ctx = propagator.Extract(extract_carrier, ctx); | ||
|
|
||
| auto span_ctx_val = new_ctx.GetValue(trace::kSpanKey); | ||
| ASSERT_TRUE(nostd::holds_alternative<nostd::shared_ptr<trace::Span>>(span_ctx_val)); | ||
|
|
||
| auto extracted_span = nostd::get<nostd::shared_ptr<trace::Span>>(span_ctx_val); | ||
| EXPECT_EQ(Hex(extracted_span->GetContext().trace_id()), Hex(span_context.trace_id())); | ||
| EXPECT_EQ(Hex(extracted_span->GetContext().span_id()), Hex(span_context.span_id())); | ||
| EXPECT_EQ(extracted_span->GetContext().IsSampled(), span_context.IsSampled()); | ||
| EXPECT_TRUE(extracted_span->GetContext().IsRemote()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Get is marked as
const, but it does mutate the internal statecache_. Perhaps this should be documented in the API doc?