diff --git a/CHANGELOG.md b/CHANGELOG.md index 783092515a2..d91fb793463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- Add environment variables propagation carrier in `go.opentelemetry.io/contrib/propagators/envcar`. (#8442) + ### Changed - Upgrade `go.opentelemetry.io/otel/semconv` to `v1.40.0`, including updates across instrumentation and detector modules. (#8631) diff --git a/CODEOWNERS b/CODEOWNERS index f888d2bd35d..9667da505a6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -64,6 +64,7 @@ processors/minsev @open-te propagators/autoprop/ @open-telemetry/go-approvers @MrAlias propagators/aws/ @open-telemetry/go-approvers @akats7 propagators/b3/ @open-telemetry/go-approvers @pellared +propagators/envcar/ @open-telemetry/go-approvers @Joibel @pellared propagators/jaeger/ @open-telemetry/go-approvers @yurishkuro propagators/opencensus/ @open-telemetry/go-approvers @dashpole propagators/ot/ @open-telemetry/go-approvers @pellared diff --git a/propagators/envcar/carrier.go b/propagators/envcar/carrier.go new file mode 100644 index 00000000000..309845db017 --- /dev/null +++ b/propagators/envcar/carrier.go @@ -0,0 +1,103 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package envcar // import "go.opentelemetry.io/contrib/propagators/envcar" + +import ( + "os" + "strings" + "sync" + + "go.opentelemetry.io/otel/propagation" +) + +// upperWithUnderscores converts a string so that A-Z and 0-9 and _ are kept +// as-is, a-z is uppercased, and all other characters are replaced with _. +func upperWithUnderscores(s string) string { + b := make([]byte, 0, len(s)) + for _, r := range s { + switch { + case r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_': + b = append(b, byte(r)) + case r >= 'a' && r <= 'z': + b = append(b, byte(r+'A'-'a')) + default: + b = append(b, '_') + } + } + return string(b) +} + +// Carrier is a TextMapCarrier that uses the environment variables as a +// storage medium for propagated key-value pairs. The keys are normalised +// 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. +// +// If you do not set SetEnvFunc, [Carrier.Set] will do nothing. +// Using [os.Setenv] here is discouraged as the environment should +// be immutable: +// https://opentelemetry.io/docs/specs/otel/context/env-carriers/#environment-variable-immutability +type Carrier struct { + // SetEnvFunc is the function that sets the environment variable. + // Usually, you want to set the environment variables for processes + // that are spawned by the current process. + SetEnvFunc func(key, value string) + values map[string]string + once sync.Once +} + +// Compile time check that Carrier implements the TextMapCarrier. +var _ propagation.TextMapCarrier = (*Carrier)(nil) + +// fetch runs once on first access, and stores the environment in the +// carrier. +func (c *Carrier) fetch() { + c.once.Do(func() { + environ := os.Environ() + c.values = make(map[string]string, len(environ)) + for _, kv := range environ { + kvPair := strings.SplitN(kv, "=", 2) + c.values[kvPair[0]] = kvPair[1] + } + }) +} + +// Get returns the value associated with the normalized passed key. +// The first call to [Carrier.Get] or [Carrier.Keys] for a +// given Carrier will read and store the values from the +// environment and all future reads will be from that store. +func (c *Carrier) Get(key string) string { + c.fetch() + return c.values[upperWithUnderscores(key)] +} + +// Set stores the key-value pair in the environment variable. +// The key is normalized before being used to set the +// environment variable. +// If SetEnvFunc is not set, this method does nothing. +func (c *Carrier) Set(key, value string) { + if c.SetEnvFunc == nil { + return + } + k := upperWithUnderscores(key) + c.SetEnvFunc(k, value) +} + +// Keys lists the keys stored in this carrier. +// This returns all the keys in the environment variables. +// The first call to [Carrier.Get] or [Carrier.Keys] for a +// given Carrier will read and store the values from the +// environment and all future reads will be from that store. +// Keys are returned as is, without any normalization, but +// this behavior is subject to change. +func (c *Carrier) Keys() []string { + c.fetch() + keys := make([]string, 0, len(c.values)) + for key := range c.values { + keys = append(keys, key) + } + return keys +} diff --git a/propagators/envcar/carrier_example_test.go b/propagators/envcar/carrier_example_test.go new file mode 100644 index 00000000000..12efb328342 --- /dev/null +++ b/propagators/envcar/carrier_example_test.go @@ -0,0 +1,81 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package envcar_test + +import ( + "context" + "fmt" + "os" + "os/exec" + + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + + "go.opentelemetry.io/contrib/propagators/envcar" +) + +// This example is a go program where the environment variables are carrying the +// trace information, and we're going to pick them up into our context. +func ExampleCarrier_extractFromParent() { + // Simulate environment variables set by a parent process. + // In practice, these would already be set when this process starts. + _ = os.Setenv("TRACEPARENT", "00-0102030405060708090a0b0c0d0e0f10-0102030405060708-01") + + // Create a carrier to read trace context from environment variables. + carrier := envcar.Carrier{} + + // Extract trace context that was propagated by the parent process. + prop := propagation.TraceContext{} + ctx := prop.Extract(context.Background(), &carrier) + + // The context now contains the span context from the parent. + spanCtx := trace.SpanContextFromContext(ctx) + fmt.Printf("Trace ID: %s\n", spanCtx.TraceID()) + fmt.Printf("Span ID: %s\n", spanCtx.SpanID()) + fmt.Printf("Sampled: %t\n", spanCtx.IsSampled()) + // Output: + // Trace ID: 0102030405060708090a0b0c0d0e0f10 + // Span ID: 0102030405060708 + // Sampled: true +} + +// This example is a go program where we have a trace and we'd like to inject it +// into a command we're going to run. +func ExampleCarrier_childProcess() { + // Create a span context with a known trace ID. + traceID := trace.TraceID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10} + spanID := trace.SpanID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + spanCtx := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }) + ctx := trace.ContextWithSpanContext(context.Background(), spanCtx) + + // Prepare a command that prints the TRACEPARENT environment variable. + cmd := exec.Command("printenv", "TRACEPARENT") + cmd.Env = os.Environ() + + // Create a carrier that injects trace context into the child + // process's environment rather than the current process's. + carrier := envcar.Carrier{ + SetEnvFunc: func(key, value string) { + cmd.Env = append(cmd.Env, key+"="+value) + }, + } + + // Inject trace context into the child's environment. + prop := propagation.TraceContext{} + prop.Inject(ctx, &carrier) + + // The child process now has trace context in its environment, + // independent of the parent process's environment variables. + out, err := cmd.Output() + if err != nil { + fmt.Println("error:", err) + return + } + fmt.Print(string(out)) + // Output: 00-0102030405060708090a0b0c0d0e0f10-0102030405060708-01 +} diff --git a/propagators/envcar/carrier_test.go b/propagators/envcar/carrier_test.go new file mode 100644 index 00000000000..367f697b8ff --- /dev/null +++ b/propagators/envcar/carrier_test.go @@ -0,0 +1,265 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package envcar_test + +import ( + "os" + "os/exec" + "slices" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + + "go.opentelemetry.io/contrib/propagators/envcar" +) + +var ( + traceID = trace.TraceID{0, 0, 0, 0, 0, 0, 0, 0x7b, 0, 0, 0, 0, 0, 0, 0x1, 0xc8} + spanID = trace.SpanID{0, 0, 0, 0, 0, 0, 0, 0x7b} + prop = propagation.TraceContext{} +) + +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-000000000000007b00000000000001c8-000000000000007b-01", + }, + want: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "valid tracestate", + envs: map[string]string{ + "TRACEPARENT": "00-000000000000007b00000000000001c8-000000000000007b-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 := t.Context() + for k, v := range tc.envs { + t.Setenv(k, v) + } + ctx = prop.Extract(ctx, &envcar.Carrier{}) + 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-000000000000007b00000000000001c8-000000000000007b-01", + }, + sc: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }), + }, + { + name: "with tracestate", + want: map[string]string{ + "TRACEPARENT": "00-000000000000007b00000000000001c8-000000000000007b-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 := t.Context() + ctx = trace.ContextWithRemoteSpanContext(ctx, tc.sc) + c := envcar.Carrier{ + SetEnvFunc: func(key, value string) { + t.Setenv(key, value) + }, + } + + 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) + } + } + }) + } +} + +func TestCarrierKeys(t *testing.T) { + t.Setenv("TRACEPARENT", "value") + + c := envcar.Carrier{} + keys := c.Keys() + + assert.Contains(t, keys, "TRACEPARENT") +} + +func TestCarrierSetNilFunc(_ *testing.T) { + c := envcar.Carrier{} // SetEnvFunc is nil + c.Set("key", "value") // should not panic, just no-op +} + +func TestCarrierGetCaseInsensitive(t *testing.T) { + t.Setenv("TRACEPARENT", "myvalue") + + c := envcar.Carrier{} + assert.Equal(t, "myvalue", c.Get("traceparent")) + assert.Equal(t, "myvalue", c.Get("TRACEPARENT")) +} + +func TestCarrierSetUppercasesUnderscoresKey(t *testing.T) { + var gotKey string + var gotValue string + c := envcar.Carrier{ + SetEnvFunc: func(key, value string) { + gotKey = key + gotValue = value + }, + } + + c.Set("traceparent", "value") + assert.Equal(t, "TRACEPARENT", gotKey) + assert.Equal(t, "value", gotValue) + + c.Set("key with spaces", "value with spaces") + assert.Equal(t, "KEY_WITH_SPACES", gotKey) + assert.Equal(t, "value with spaces", gotValue) + + c.Set("Mój Bagaż", "🧳") + assert.Equal(t, "M_J_BAGA_", gotKey) + assert.Equal(t, "🧳", gotValue) +} + +func TestConcurrentChildProcesses(t *testing.T) { + // Test that concurrent goroutines can each spawn child processes + // with their own unique trace context. + const numGoroutines = 10 + + type result struct { + index int + want string + got string + err error + } + + results := make(chan result, numGoroutines) + var wg sync.WaitGroup + baseCtx := t.Context() + + for i := range numGoroutines { + wg.Add(1) + go func() { + defer wg.Done() + + // Create a unique trace ID for this goroutine. + traceID := trace.TraceID{byte(i + 1)} + spanID := trace.SpanID{byte(i + 1)} + spanCtx := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }) + ctx := trace.ContextWithSpanContext(baseCtx, spanCtx) + + // Each goroutine gets its own cmd with its own Env slice. + cmd := exec.Command("printenv", "TRACEPARENT") + cmd.Env = os.Environ() + + // Each goroutine gets its own carrier that writes to its cmd.Env. + carrier := envcar.Carrier{ + SetEnvFunc: func(key, value string) { + cmd.Env = append(cmd.Env, key+"="+value) + }, + } + + // Inject this goroutine's trace context. + prop := propagation.TraceContext{} + prop.Inject(ctx, &carrier) + + // Run the child process and capture output. + out, err := cmd.Output() + + // Expected traceparent format for this goroutine's trace ID. + expected := "00-" + traceID.String() + "-" + spanID.String() + "-01" + + results <- result{ + index: i, + want: expected, + got: strings.TrimSpace(string(out)), + err: err, + } + }() + } + + wg.Wait() + close(results) + + // Verify each goroutine's child process received the correct trace context. + for r := range results { + require.NoError(t, r.err, "goroutine %d failed to run child process", r.index) + assert.Equal(t, r.want, r.got, + "goroutine %d: child process received wrong trace context", r.index) + } +} + +// Ensure Get and Keys to fetch from cache. +func TestCarrierGetFetchOnce(t *testing.T) { + t.Setenv("TRACEPARENT", "myvalue") + c := envcar.Carrier{} + require.Equal(t, "myvalue", c.Get("TRACEPARENT")) + require.NotEqual(t, -1, slices.Index(c.Keys(), "TRACEPARENT")) + t.Setenv("TRACEPARENT", "bad") + t.Setenv("SOMETHINGNEW", "bad") + // Assert a carrier instance reads the env vars only once: + assert.Equal(t, "myvalue", c.Get("TRACEPARENT")) + assert.NotEqual(t, -1, slices.Index(c.Keys(), "TRACEPARENT")) + assert.Equal(t, -1, slices.Index(c.Keys(), "SOMETHINGNEW")) +} diff --git a/propagators/envcar/doc.go b/propagators/envcar/doc.go new file mode 100644 index 00000000000..73d0c22f42c --- /dev/null +++ b/propagators/envcar/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package envcar implements the Environment Carrier specification as documented +// here https://opentelemetry.io/docs/specs/otel/context/env-carriers/ +package envcar // import "go.opentelemetry.io/contrib/propagators/envcar" diff --git a/propagators/envcar/go.mod b/propagators/envcar/go.mod new file mode 100644 index 00000000000..e0dab3f76e6 --- /dev/null +++ b/propagators/envcar/go.mod @@ -0,0 +1,19 @@ +module go.opentelemetry.io/contrib/propagators/envcar + +go 1.24.0 + +require ( + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/propagators/envcar/go.sum b/propagators/envcar/go.sum new file mode 100644 index 00000000000..e71ccbbca16 --- /dev/null +++ b/propagators/envcar/go.sum @@ -0,0 +1,39 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/propagators/envcar/upper_test.go b/propagators/envcar/upper_test.go new file mode 100644 index 00000000000..42a384bbcef --- /dev/null +++ b/propagators/envcar/upper_test.go @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package envcar + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUpperWithUnderscores(t *testing.T) { + tests := []struct { + in, want string + }{ + {"", ""}, + {"ABC", "ABC"}, + {"abc", "ABC"}, + {"01239", "01239"}, + {"a_b_c", "A_B_C"}, + {"hello-world", "HELLO_WORLD"}, + {"foo.bar", "FOO_BAR"}, + {"Content-Type", "CONTENT_TYPE"}, + {"traceparent", "TRACEPARENT"}, + {"key with spaces", "KEY_WITH_SPACES"}, + {"MiXeD_123!", "MIXED_123_"}, + {"🧳", "_"}, + {"Mój Bagaż", "M_J_BAGA_"}, + } + for _, tc := range tests { + t.Run(tc.in, func(t *testing.T) { + assert.Equal(t, tc.want, upperWithUnderscores(tc.in)) + }) + } +} diff --git a/propagators/envcar/version.go b/propagators/envcar/version.go new file mode 100644 index 00000000000..2e825da00f5 --- /dev/null +++ b/propagators/envcar/version.go @@ -0,0 +1,10 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package envcar // import "go.opentelemetry.io/contrib/propagators/envcar" + +// Version is the current release version of the envcar propagator. +func Version() string { + return "1.40.0" + // This string is updated by the pre_release.sh script during release +} diff --git a/propagators/envcar/version_test.go b/propagators/envcar/version_test.go new file mode 100644 index 00000000000..6ab702d55a0 --- /dev/null +++ b/propagators/envcar/version_test.go @@ -0,0 +1,23 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +package envcar_test + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/contrib/propagators/envcar" +) + +// regex taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +var versionRegex = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)` + + `(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)` + + `(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` + + `(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) + +func TestVersionSemver(t *testing.T) { + v := envcar.Version() + assert.NotNil(t, versionRegex.FindStringSubmatch(v), "version is not semver: %s", v) +} diff --git a/versions.yaml b/versions.yaml index 830236adb04..ae7de01b475 100644 --- a/versions.yaml +++ b/versions.yaml @@ -25,6 +25,7 @@ module-sets: - go.opentelemetry.io/contrib/detectors/aws/lambda - go.opentelemetry.io/contrib/exporters/autoexport - go.opentelemetry.io/contrib/propagators/autoprop + - go.opentelemetry.io/contrib/propagators/envcar - go.opentelemetry.io/contrib/propagators/opencensus - go.opentelemetry.io/contrib/propagators/opencensus/examples - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp