diff --git a/propagation/env.go b/propagation/env.go new file mode 100644 index 00000000000..b8de0058171 --- /dev/null +++ b/propagation/env.go @@ -0,0 +1,58 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package propagation // import "go.opentelemetry.io/otel/propagation" + +import ( + "os" + "strings" +) + +// EnvCarrier is a TextMapCarrier that uses the environment variables as a +// storage medium for propagated key-value pairs. The keys are uppercased +// before being used to access the environment variables. +// This is useful for propagating values that are set in the environment +// and need to be accessed by different processes or services. +// The keys are uppercased to avoid case sensitivity issues across different +// operating systems and environments. +type EnvCarrier struct { + // SetEnvFunc is a function that sets the environment variable. + // Usually, you want to set the environment variable for processes + // that are spawned by the current process. + // By default implementation, it does nothing. + SetEnvFunc func(key, value string) error +} + +var _ TextMapCarrier = EnvCarrier{} + +// Get returns the value associated with the passed key. +// The key is uppercased before being used to access the environment variable. +func (EnvCarrier) Get(key string) string { + k := strings.ToUpper(key) + return os.Getenv(k) +} + +// Set stores the key-value pair in the environment variable. +// The key is uppercased before being used to set the environment variable. +// If SetEnvFunc is not set, this method does nothing. +func (e EnvCarrier) Set(key, value string) { + if e.SetEnvFunc == nil { + return + } + k := strings.ToUpper(key) + _ = e.SetEnvFunc(k, value) +} + +// Keys lists the keys stored in this carrier. +// This returns all the keys in the environment variables. +func (EnvCarrier) Keys() []string { + keys := make([]string, 0, len(os.Environ())) + for _, kv := range os.Environ() { + kvPair := strings.SplitN(kv, "=", 2) + if len(kvPair) < 1 { + continue + } + keys = append(keys, kvPair[0]) + } + return keys +} diff --git a/propagation/env_test.go b/propagation/env_test.go new file mode 100644 index 00000000000..853edabe1da --- /dev/null +++ b/propagation/env_test.go @@ -0,0 +1,124 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package propagation_test + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +func TestExtractValidTraceContextEnvCarrier(t *testing.T) { + stateStr := "key1=value1,key2=value2" + state, err := trace.ParseTraceState(stateStr) + require.NoError(t, err) + + tests := []struct { + name string + envs map[string]string + want trace.SpanContext + }{ + { + name: "sampled", + envs: map[string]string{ + "TRACEPARENT": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + }, + want: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "valid tracestate", + envs: map[string]string{ + "TRACEPARENT": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + "TRACESTATE": stateStr, + }, + want: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceState: state, + Remote: true, + }), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + for k, v := range tc.envs { + t.Setenv(k, v) + } + ctx = prop.Extract(ctx, propagation.EnvCarrier{}) + assert.Equal(t, tc.want, trace.SpanContextFromContext(ctx)) + }) + } +} + +func TestInjectTraceContextEnvCarrier(t *testing.T) { + stateStr := "key1=value1,key2=value2" + state, err := trace.ParseTraceState(stateStr) + require.NoError(t, err) + + tests := []struct { + name string + want map[string]string + sc trace.SpanContext + }{ + { + name: "sampled", + want: map[string]string{ + "TRACEPARENT": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + }, + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "with tracestate", + want: map[string]string{ + "TRACEPARENT": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", + "TRACESTATE": stateStr, + }, + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceState: state, + Remote: true, + }), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + ctx = trace.ContextWithRemoteSpanContext(ctx, tc.sc) + c := propagation.EnvCarrier{ + SetEnvFunc: func(key, value string) error { + t.Setenv(key, value) + return nil + }, + } + + prop.Inject(ctx, c) + + for k, v := range tc.want { + if got := os.Getenv(k); got != v { + t.Errorf("got %s=%s, want %s=%s", k, got, k, v) + } + } + }) + } +}