diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab8f2cb40b..70811b259f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Configuration file can now be set via `OTEL_CONFIG_FILE` in `go.opentelemetry.io/contrib/otelconf`. (#8639) +### Changed + +- Prepend `_` to the normalized environment variable name when the key starts with a digit in `go.opentelemetry.io/contrib/propagators/envcar`, ensuring POSIX compliance. (#8678) + ### Fixed - Limit the request body size at 1MB in `go.opentelemetry.io/contrib/zpages`. (#8656) diff --git a/propagators/envcar/carrier.go b/propagators/envcar/carrier.go index 4bcdddabb83..eee90de8469 100644 --- a/propagators/envcar/carrier.go +++ b/propagators/envcar/carrier.go @@ -11,23 +11,6 @@ import ( "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)) //nolint:gosec // G115: overflow is already checked. - 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. @@ -71,7 +54,7 @@ func (c *Carrier) fetch() { // environment and all future reads will be from that store. func (c *Carrier) Get(key string) string { c.fetch() - return c.values[upperWithUnderscores(key)] + return c.values[normalize(key)] } // Set stores the key-value pair in the environment variable. @@ -82,7 +65,7 @@ func (c *Carrier) Set(key, value string) { if c.SetEnvFunc == nil { return } - k := upperWithUnderscores(key) + k := normalize(key) c.SetEnvFunc(k, value) } diff --git a/propagators/envcar/normalize.go b/propagators/envcar/normalize.go new file mode 100644 index 00000000000..cdc90445b6e --- /dev/null +++ b/propagators/envcar/normalize.go @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package envcar // import "go.opentelemetry.io/contrib/propagators/envcar" + +import ( + "unicode/utf8" +) + +// normalize converts s to a valid POSIX environment variable name. +// The conversion rules are: +// - A–Z, 0–9, and _ are kept as-is. +// - a–z are uppercased. +// - All other characters are replaced with _. +// - If the result would start with a digit, an underscore is prepended. +func normalize(s string) string { + if s == "" { + return "" + } + + // Pre-allocate the exact output length. If the first byte is a digit, + // the name must be prefixed with '_', so allocate one extra byte. + var b []byte + i := 0 + if s[0] >= '0' && s[0] <= '9' { + b = make([]byte, utf8.RuneCountInString(s)+1) + b[0] = '_' + i = 1 + } else { + b = make([]byte, utf8.RuneCountInString(s)) + } + + for _, r := range s { + switch { + case r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_': + // Uppercase letters, digits, and underscores are valid as-is. + b[i] = byte(r) //nolint:gosec // G115: overflow is not possible. + case r >= 'a' && r <= 'z': + // Lowercase letters are converted to uppercase. + b[i] = byte(r + 'A' - 'a') + default: + // All other characters (including non-ASCII runes) become underscores. + b[i] = '_' + } + i++ + } + return string(b) +} diff --git a/propagators/envcar/normalize_test.go b/propagators/envcar/normalize_test.go new file mode 100644 index 00000000000..20965c67944 --- /dev/null +++ b/propagators/envcar/normalize_test.go @@ -0,0 +1,49 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package envcar + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var normalizeCases = []struct { + in, want string +}{ + {"", ""}, + {"ABC", "ABC"}, + {"abc", "ABC"}, + {"01239", "_01239"}, + {"0abc", "_0ABC"}, + {"9", "_9"}, + {"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_"}, +} + +func TestNormalize(t *testing.T) { + for _, tc := range normalizeCases { + t.Run(tc.in, func(t *testing.T) { + assert.Equal(t, tc.want, normalize(tc.in)) + }) + } +} + +func BenchmarkNormalize(b *testing.B) { + for _, tc := range normalizeCases { + b.Run(tc.in, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + normalize(tc.in) + } + }) + } +} diff --git a/propagators/envcar/upper_test.go b/propagators/envcar/upper_test.go deleted file mode 100644 index 42a384bbcef..00000000000 --- a/propagators/envcar/upper_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// 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)) - }) - } -}