Skip to content

Commit f410084

Browse files
jamesmoessispellareddmathieu
authored
propagation: extract of multiple header values (#5973)
* Add `ValuesGetter` interface, an optional `TextMapCarrier` feature that adds `Values(string) []string`. * `HeaderCarrier` implements `ValuesGetter`. * Change `Baggage` to use the `Values()` method if it's implemented. Notable comment: #5973 (comment) Adds tests extracting requests with multiple 'baggage' headers set. Does not introduce any breaking changes or alter any existing tests. Spec issue: open-telemetry/opentelemetry-specification#433 Corresponding Java prototype: open-telemetry/opentelemetry-java#6852 --------- Co-authored-by: Robert Pająk <[email protected]> Co-authored-by: dmathieu <[email protected]>
1 parent 0385f83 commit f410084

File tree

4 files changed

+131
-6
lines changed

4 files changed

+131
-6
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1818
- Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#6751)
1919
- Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#6752)
2020
- Add `WithHTTPClient` option to configure the `http.Client` used by `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#6688)
21+
- Add `ValuesGetter` in `go.opentelemetry.io/otel/propagation`, a `TextMapCarrier` that supports retrieving multiple values for a single key. (#5973)
22+
- Add `Values` method to `HeaderCarrier` to implement the new `ValuesGetter` interface in `go.opentelemetry.io/otel/propagation`. (#5973)
23+
- Update `Baggage` in `go.opentelemetry.io/otel/propagation` to retrieve multiple values for a key when the carrier implements `ValuesGetter`. (#5973)
2124
- Add `AssertEqual` function in `go.opentelemetry.io/otel/log/logtest`. (#6662)
2225

2326
### Removed

propagation/baggage.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,21 @@ func (b Baggage) Inject(ctx context.Context, carrier TextMapCarrier) {
2828
}
2929

3030
// Extract returns a copy of parent with the baggage from the carrier added.
31+
// If carrier implements [ValuesGetter] (e.g. [HeaderCarrier]), Values is invoked
32+
// for multiple values extraction. Otherwise, Get is called.
3133
func (b Baggage) Extract(parent context.Context, carrier TextMapCarrier) context.Context {
34+
if multiCarrier, ok := carrier.(ValuesGetter); ok {
35+
return extractMultiBaggage(parent, multiCarrier)
36+
}
37+
return extractSingleBaggage(parent, carrier)
38+
}
39+
40+
// Fields returns the keys who's values are set with Inject.
41+
func (b Baggage) Fields() []string {
42+
return []string{baggageHeader}
43+
}
44+
45+
func extractSingleBaggage(parent context.Context, carrier TextMapCarrier) context.Context {
3246
bStr := carrier.Get(baggageHeader)
3347
if bStr == "" {
3448
return parent
@@ -41,7 +55,23 @@ func (b Baggage) Extract(parent context.Context, carrier TextMapCarrier) context
4155
return baggage.ContextWithBaggage(parent, bag)
4256
}
4357

44-
// Fields returns the keys who's values are set with Inject.
45-
func (b Baggage) Fields() []string {
46-
return []string{baggageHeader}
58+
func extractMultiBaggage(parent context.Context, carrier ValuesGetter) context.Context {
59+
bVals := carrier.Values(baggageHeader)
60+
if len(bVals) == 0 {
61+
return parent
62+
}
63+
var members []baggage.Member
64+
for _, bStr := range bVals {
65+
currBag, err := baggage.Parse(bStr)
66+
if err != nil {
67+
continue
68+
}
69+
members = append(members, currBag.Members()...)
70+
}
71+
72+
b, err := baggage.New(members...)
73+
if err != nil || b.Len() == 0 {
74+
return parent
75+
}
76+
return baggage.ContextWithBaggage(parent, b)
4777
}

propagation/baggage_test.go

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (m members) Baggage(t *testing.T) baggage.Baggage {
5656
return bag
5757
}
5858

59-
func TestExtractValidBaggageFromHTTPReq(t *testing.T) {
59+
func TestExtractValidBaggage(t *testing.T) {
6060
prop := propagation.TextMapPropagator(propagation.Baggage{})
6161
tests := []struct {
6262
name string
@@ -113,13 +113,79 @@ func TestExtractValidBaggageFromHTTPReq(t *testing.T) {
113113
{Key: "key1", Value: "val%2"},
114114
},
115115
},
116+
{
117+
name: "empty header",
118+
header: "",
119+
want: members{},
120+
},
116121
}
117122

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

130+
// test with http header carrier (which implements ValuesGetter)
131+
ctx := prop.Extract(context.Background(), propagation.HeaderCarrier(req.Header))
132+
expected := tt.want.Baggage(t)
133+
assert.Equal(t, expected, baggage.FromContext(ctx), "should extract baggage for HeaderCarrier")
134+
135+
// test with map carrier (which does not implement ValuesGetter)
136+
ctx = prop.Extract(context.Background(), mapCarr)
137+
expected = tt.want.Baggage(t)
138+
assert.Equal(t, expected, baggage.FromContext(ctx), "should extract baggage for MapCarrier")
139+
})
140+
}
141+
}
142+
143+
func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
144+
prop := propagation.TextMapPropagator(propagation.Baggage{})
145+
tests := []struct {
146+
name string
147+
headers []string
148+
want members
149+
}{
150+
{
151+
name: "non conflicting headers",
152+
headers: []string{"key1=val1", "key2=val2"},
153+
want: members{
154+
{Key: "key1", Value: "val1"},
155+
{Key: "key2", Value: "val2"},
156+
},
157+
},
158+
{
159+
name: "conflicting keys, uses last val",
160+
headers: []string{"key1=val1", "key1=val2"},
161+
want: members{
162+
{Key: "key1", Value: "val2"},
163+
},
164+
},
165+
{
166+
name: "single empty",
167+
headers: []string{"", "key1=val1"},
168+
want: members{
169+
{Key: "key1", Value: "val1"},
170+
},
171+
},
172+
{
173+
name: "all empty",
174+
headers: []string{"", ""},
175+
want: members{},
176+
},
177+
{
178+
name: "none",
179+
headers: []string{},
180+
want: members{},
181+
},
182+
}
183+
184+
for _, tt := range tests {
185+
t.Run(tt.name, func(t *testing.T) {
186+
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
187+
req.Header["Baggage"] = tt.headers
188+
123189
ctx := context.Background()
124190
ctx = prop.Extract(ctx, propagation.HeaderCarrier(req.Header))
125191
expected := tt.want.Baggage(t)

propagation/propagation.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
)
1010

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

33+
// ValuesGetter can return multiple values for a single key,
34+
// with contrast to TextMapCarrier.Get which returns a single value.
35+
type ValuesGetter interface {
36+
// DO NOT CHANGE: any modification will not be backwards compatible and
37+
// must never be done outside of a new major release.
38+
39+
// Values returns all values associated with the passed key.
40+
Values(key string) []string
41+
// DO NOT CHANGE: any modification will not be backwards compatible and
42+
// must never be done outside of a new major release.
43+
}
44+
3245
// MapCarrier is a TextMapCarrier that uses a map held in memory as a storage
3346
// medium for propagated key-value pairs.
3447
type MapCarrier map[string]string
@@ -55,14 +68,25 @@ func (c MapCarrier) Keys() []string {
5568
return keys
5669
}
5770

58-
// HeaderCarrier adapts http.Header to satisfy the TextMapCarrier interface.
71+
// HeaderCarrier adapts http.Header to satisfy the TextMapCarrier and ValuesGetter interfaces.
5972
type HeaderCarrier http.Header
6073

61-
// Get returns the value associated with the passed key.
74+
// Compile time check that HeaderCarrier implements ValuesGetter.
75+
var _ TextMapCarrier = HeaderCarrier{}
76+
77+
// Compile time check that HeaderCarrier implements TextMapCarrier.
78+
var _ ValuesGetter = HeaderCarrier{}
79+
80+
// Get returns the first value associated with the passed key.
6281
func (hc HeaderCarrier) Get(key string) string {
6382
return http.Header(hc).Get(key)
6483
}
6584

85+
// Values returns all values associated with the passed key.
86+
func (hc HeaderCarrier) Values(key string) []string {
87+
return http.Header(hc).Values(key)
88+
}
89+
6690
// Set stores the key-value pair.
6791
func (hc HeaderCarrier) Set(key string, value string) {
6892
http.Header(hc).Set(key, value)
@@ -89,6 +113,8 @@ type TextMapPropagator interface {
89113
// must never be done outside of a new major release.
90114

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

0 commit comments

Comments
 (0)