Skip to content

propagation: extract of multiple header values #5973

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
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4b08c01
Prototypes propagation with multiple values. Adds MultiTextMapCarrier…
jamesmoessis Nov 14, 2024
4cffc3c
implement composability suggestion with regards to interface design
jamesmoessis Apr 28, 2025
5970fd4
rename GetAll to Values as suggested
jamesmoessis Apr 28, 2025
a63c63a
ensure backwards compatibility message is in all places it should be
jamesmoessis Apr 28, 2025
9e4e348
optimise when bVals is empty as suggested -- also add test case for e…
jamesmoessis Apr 28, 2025
c72ade6
add changelog entry
jamesmoessis Apr 28, 2025
9de8b16
appease linter
jamesmoessis Apr 28, 2025
cf66b68
Update propagation/baggage.go
jamesmoessis Apr 29, 2025
52f3e06
rename MultiGetter to ValuesGetter, implementing review suggestion
jamesmoessis Apr 29, 2025
8bbb867
add more information into the go doc of Extract, as per review
jamesmoessis Apr 29, 2025
1bceb79
update HeaderCarrier go doc to explicitly state that it satisfies Mul…
jamesmoessis Apr 29, 2025
c7bf432
update TextMapPropagator.Extract go doc to mention checking if carrie…
jamesmoessis Apr 29, 2025
324cfcf
Merge branch 'main' into multi-baggage-propagation-prototype
pellared May 6, 2025
5c239a5
remove MultiTextMapCarrier and just reference ValuesGetter directly, …
jamesmoessis May 8, 2025
2381840
Merge branch 'main' into multi-baggage-propagation-prototype
jamesmoessis May 8, 2025
47204d0
Update propagation/baggage.go
jamesmoessis May 8, 2025
a239650
Update CHANGELOG.md
jamesmoessis May 8, 2025
41191a9
add compile time checks for implementing interfaces
jamesmoessis May 8, 2025
9698552
update test to additionally test baggage with map carrier, as map car…
jamesmoessis May 8, 2025
f7254e2
Merge branch 'main' into multi-baggage-propagation-prototype
pellared May 9, 2025
d116229
Update propagation/baggage_test.go
jamesmoessis May 14, 2025
0d124fe
Update propagation/baggage_test.go
jamesmoessis May 14, 2025
f9c5a64
Merge branch 'main' into multi-baggage-propagation-prototype
pellared May 14, 2025
91a4ffc
fix applied suggestion
jamesmoessis May 15, 2025
1307ad4
Merge branch 'main' into multi-baggage-propagation-prototype
pellared May 15, 2025
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
See the [migration documentation](./semconv/v1.31.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.30.0`(#6479)
- Add `Recording`, `Scope`, and `Record` types in `go.opentelemetry.io/otel/log/logtest`. (#6507)
- Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#6688)
- Add `ValuesGetter` in `go.opentelemetry.io/otel/propagation`, a `TextMapCarrier` that supports retrieving multiple values for a single key. (#5973)
- Add `Values` method to `HeaderCarrier` to implement the new `ValuesGetter` interface in `go.opentelemetry.io/otel/propagation`. (#5973)
- Update `Baggage` in `go.opentelemetry.io/otel/propagation` to retrieve multiple values for a key when the carrier implements `ValuesGetter`. (#5973)

### Removed

Expand Down
36 changes: 33 additions & 3 deletions propagation/baggage.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,21 @@ func (b Baggage) Inject(ctx context.Context, carrier TextMapCarrier) {
}

// Extract returns a copy of parent with the baggage from the carrier added.
// If carrier implements [ValuesGetter] (e.g. [HeaderCarrier]), Values is invoked
// for multiple values extraction. Otherwise, Get is called.
func (b Baggage) Extract(parent context.Context, carrier TextMapCarrier) context.Context {
if multiCarrier, ok := carrier.(ValuesGetter); ok {
return extractMultiBaggage(parent, multiCarrier)
}
return extractSingleBaggage(parent, carrier)
}

// Fields returns the keys who's values are set with Inject.
func (b Baggage) Fields() []string {
return []string{baggageHeader}
}

func extractSingleBaggage(parent context.Context, carrier TextMapCarrier) context.Context {
bStr := carrier.Get(baggageHeader)
if bStr == "" {
return parent
Expand All @@ -41,7 +55,23 @@ func (b Baggage) Extract(parent context.Context, carrier TextMapCarrier) context
return baggage.ContextWithBaggage(parent, bag)
}

// Fields returns the keys who's values are set with Inject.
func (b Baggage) Fields() []string {
return []string{baggageHeader}
func extractMultiBaggage(parent context.Context, carrier ValuesGetter) context.Context {
bVals := carrier.Values(baggageHeader)
if len(bVals) == 0 {
return parent
}
var members []baggage.Member
for _, bStr := range bVals {
currBag, err := baggage.Parse(bStr)
if err != nil {
continue
}
members = append(members, currBag.Members()...)
}

b, err := baggage.New(members...)
if err != nil || b.Len() == 0 {
return parent
}
return baggage.ContextWithBaggage(parent, b)
}
68 changes: 67 additions & 1 deletion propagation/baggage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (m members) Baggage(t *testing.T) baggage.Baggage {
return bag
}

func TestExtractValidBaggageFromHTTPReq(t *testing.T) {
func TestExtractValidBaggage(t *testing.T) {
prop := propagation.TextMapPropagator(propagation.Baggage{})
tests := []struct {
name string
Expand Down Expand Up @@ -113,13 +113,79 @@ func TestExtractValidBaggageFromHTTPReq(t *testing.T) {
{Key: "key1", Value: "val%2"},
},
},
{
name: "empty header",
header: "",
want: members{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mapCarr := propagation.MapCarrier{}
mapCarr["baggage"] = tt.header
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req.Header.Set("baggage", tt.header)

// test with http header carrier (which implements ValuesGetter)
ctx := prop.Extract(context.Background(), propagation.HeaderCarrier(req.Header))
expected := tt.want.Baggage(t)
assert.Equal(t, expected, baggage.FromContext(ctx))

// test with map carrier (which does not implement ValuesGetter)
ctx = prop.Extract(context.Background(), mapCarr)
expected = tt.want.Baggage(t)
assert.Equal(t, expected, baggage.FromContext(ctx))
})
}
}

func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
prop := propagation.TextMapPropagator(propagation.Baggage{})
tests := []struct {
name string
headers []string
want members
}{
{
name: "non conflicting headers",
headers: []string{"key1=val1", "key2=val2"},
want: members{
{Key: "key1", Value: "val1"},
{Key: "key2", Value: "val2"},
},
},
{
name: "conflicting keys, uses last val",
headers: []string{"key1=val1", "key1=val2"},
want: members{
{Key: "key1", Value: "val2"},
},
},
{
name: "single empty",
headers: []string{"", "key1=val1"},
want: members{
{Key: "key1", Value: "val1"},
},
},
{
name: "all empty",
headers: []string{"", ""},
want: members{},
},
{
name: "none",
headers: []string{},
want: members{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req.Header["Baggage"] = tt.headers

ctx := context.Background()
ctx = prop.Extract(ctx, propagation.HeaderCarrier(req.Header))
expected := tt.want.Baggage(t)
Expand Down
30 changes: 28 additions & 2 deletions propagation/propagation.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
)

// TextMapCarrier is the storage medium used by a TextMapPropagator.
// See ValuesGetter for how a TextMapCarrier can get multiple values for a key.
type TextMapCarrier interface {
// DO NOT CHANGE: any modification will not be backwards compatible and
// must never be done outside of a new major release.
Expand All @@ -29,6 +30,18 @@ type TextMapCarrier interface {
// must never be done outside of a new major release.
}

// ValuesGetter can return multiple values for a single key,
// with contrast to TextMapCarrier.Get which returns a single value.
type ValuesGetter interface {
// DO NOT CHANGE: any modification will not be backwards compatible and
// must never be done outside of a new major release.

// Values returns all values associated with the passed key.
Values(key string) []string
// DO NOT CHANGE: any modification will not be backwards compatible and
// must never be done outside of a new major release.
}

// MapCarrier is a TextMapCarrier that uses a map held in memory as a storage
// medium for propagated key-value pairs.
type MapCarrier map[string]string
Expand All @@ -55,14 +68,25 @@ func (c MapCarrier) Keys() []string {
return keys
}

// HeaderCarrier adapts http.Header to satisfy the TextMapCarrier interface.
// HeaderCarrier adapts http.Header to satisfy the TextMapCarrier and ValuesGetter interfaces.
type HeaderCarrier http.Header

// Get returns the value associated with the passed key.
// Compile time check that HeaderCarrier implements ValuesGetter.
var _ TextMapCarrier = HeaderCarrier{}

// Compile time check that HeaderCarrier implements TextMapCarrier.
var _ ValuesGetter = HeaderCarrier{}

// Get returns the first value associated with the passed key.
func (hc HeaderCarrier) Get(key string) string {
return http.Header(hc).Get(key)
}

// Values returns all values associated with the passed key.
func (hc HeaderCarrier) Values(key string) []string {
return http.Header(hc).Values(key)
}

// Set stores the key-value pair.
func (hc HeaderCarrier) Set(key string, value string) {
http.Header(hc).Set(key, value)
Expand All @@ -89,6 +113,8 @@ type TextMapPropagator interface {
// must never be done outside of a new major release.

// Extract reads cross-cutting concerns from the carrier into a Context.
// Implementations may check if the carrier implements ValuesGetter,
// to support extraction of multiple values per key.
Extract(ctx context.Context, carrier TextMapCarrier) context.Context
// DO NOT CHANGE: any modification will not be backwards compatible and
// must never be done outside of a new major release.
Expand Down
Loading