Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
478ba68
feat: add environment carrier
Joibel Jan 21, 2026
aa72967
test: add examples
Joibel Jan 30, 2026
f58fdd1
Merge branch 'main' into envcar
Joibel Jan 30, 2026
02deedc
Update CODEOWNERS
Joibel Jan 30, 2026
edad481
Update CHANGELOG.md
Joibel Jan 30, 2026
166eab6
chore: remove error return and add test
Joibel Jan 30, 2026
1b54363
Update propagators/envcar/carrier.go
Joibel Feb 2, 2026
7533bf3
Merge branch 'main' into envcar
Joibel Feb 3, 2026
9ea9d63
feat: persistent values from carrier
Joibel Feb 3, 2026
f819347
chore: various code review feedback items
Joibel Feb 25, 2026
35a4228
Merge branch 'main' into envcar
Joibel Feb 25, 2026
bd65603
chore: move changelog entry
Joibel Feb 25, 2026
c29d61e
docs: improve godocs
Joibel Feb 25, 2026
a27cbe6
chore: go.sum
Joibel Feb 25, 2026
feb0dc2
Update propagators/envcar/carrier.go
Joibel Feb 26, 2026
311ae22
Update propagators/envcar/carrier.go
Joibel Feb 26, 2026
ba6e72b
Update propagators/envcar/carrier.go
Joibel Feb 26, 2026
2025db5
Update propagators/envcar/carrier.go
Joibel Feb 26, 2026
4a8eead
Update propagators/envcar/carrier.go
Joibel Feb 26, 2026
c667885
chore: fix test
Joibel Mar 3, 2026
e4a9322
Merge remote-tracking branch 'upstream/main' into envcar
Joibel Mar 3, 2026
1e24f82
feat: use underscores for non-A-Z
Joibel Mar 4, 2026
ab56ec0
chore: linter fix
Joibel Mar 4, 2026
4556ab9
chore: fix up the comments and linter problems
Joibel Mar 4, 2026
1dca629
Merge branch 'main' into envcar
pellared Mar 4, 2026
d776978
Fix CHANGELOG
pellared Mar 4, 2026
1f25da6
Merge branch 'main' into envcar
pellared Mar 4, 2026
2e78d6b
docs: small updates from code review
Joibel Mar 4, 2026
f3bac0d
test: new keys and get test
Joibel Mar 5, 2026
bb38d84
Merge branch 'main' into envcar
Joibel Mar 5, 2026
517f352
Update propagators/envcar/carrier_test.go
Joibel Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions propagators/envcar/carrier.go
Original file line number Diff line number Diff line change
@@ -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() {
Comment thread
dmathieu marked this conversation as resolved.
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
Comment thread
pellared marked this conversation as resolved.
// 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 {
Comment thread
pellared marked this conversation as resolved.
c.fetch()
keys := make([]string, 0, len(c.values))
for key := range c.values {
keys = append(keys, key)
}
return keys
}
81 changes: 81 additions & 0 deletions propagators/envcar/carrier_example_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading