1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-05-22 09:35:21 +02:00

Add max baggage length as limitation (#8222)

```
goos: darwin
goarch: arm64
pkg: go.opentelemetry.io/otel/baggage
cpu: Apple M1 Max
                                 │   /tmp/old.txt    │             /tmp/new.txt             │
                                 │      sec/op       │   sec/op     vs base                 │
New-10                                  413.5n ±  1%   410.1n ± 1%         ~ (p=0.184 n=10)
NewMemberRaw-10                         12.65n ±  1%   12.62n ± 1%         ~ (p=0.270 n=10)
Parse-10                                1.252µ ±  2%   1.254µ ± 1%         ~ (p=0.778 n=10)
String-10                               594.9n ±  1%   593.4n ± 1%         ~ (p=0.279 n=10)
ValueEscape/nothing_to_escape-10        4.890n ±  1%   4.885n ± 0%         ~ (p=0.579 n=10)
ValueEscape/requires_escaping-10        22.02n ±  1%   21.47n ± 1%    -2.50% (p=0.000 n=10)
ValueEscape/long_value-10               507.4n ±  1%   506.6n ± 2%         ~ (p=0.481 n=10)
MemberString-10                         486.7n ± 15%   514.0n ± 5%         ~ (p=0.190 n=10)
ParseOversized-10                  22544795.0n ±  1%   130.8n ± 4%  -100.00% (p=0.000 n=10)
geomean                                 510.0n         133.8n        -73.76%

                                 │   /tmp/old.txt   │              /tmp/new.txt              │
                                 │       B/op       │     B/op      vs base                  │
New-10                                 592.0 ± 0%       592.0 ± 0%        ~ (p=1.000 n=10) ¹
NewMemberRaw-10                        0.000 ± 0%       0.000 ± 0%        ~ (p=1.000 n=10) ¹
Parse-10                             1.039Ki ± 0%     1.039Ki ± 0%        ~ (p=1.000 n=10) ¹
String-10                              840.0 ± 0%       840.0 ± 0%        ~ (p=1.000 n=10) ¹
ValueEscape/nothing_to_escape-10       0.000 ± 0%       0.000 ± 0%        ~ (p=1.000 n=10) ¹
ValueEscape/requires_escaping-10       16.00 ± 0%       16.00 ± 0%        ~ (p=1.000 n=10) ¹
ValueEscape/long_value-10              576.0 ± 0%       576.0 ± 0%        ~ (p=1.000 n=10) ¹
MemberString-10                        656.0 ± 0%       656.0 ± 0%        ~ (p=1.000 n=10) ¹
ParseOversized-10                  801126.50 ± 0%       88.00 ± 0%  -99.99% (p=0.000 n=10)
geomean                                           ²                 -63.68%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │   /tmp/old.txt    │             /tmp/new.txt              │
                                 │     allocs/op     │ allocs/op   vs base                   │
New-10                                  6.000 ± 0%     6.000 ± 0%         ~ (p=1.000 n=10) ¹
NewMemberRaw-10                         0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=10) ¹
Parse-10                                18.00 ± 0%     18.00 ± 0%         ~ (p=1.000 n=10) ¹
String-10                               8.000 ± 0%     8.000 ± 0%         ~ (p=1.000 n=10) ¹
ValueEscape/nothing_to_escape-10        0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=10) ¹
ValueEscape/requires_escaping-10        1.000 ± 0%     1.000 ± 0%         ~ (p=1.000 n=10) ¹
ValueEscape/long_value-10               2.000 ± 0%     2.000 ± 0%         ~ (p=1.000 n=10) ¹
MemberString-10                         4.000 ± 0%     4.000 ± 0%         ~ (p=1.000 n=10) ¹
ParseOversized-10                  250007.000 ± 0%     3.000 ± 0%  -100.00% (p=0.000 n=10)
geomean                                            ²                -71.61%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean
```

---------

Co-authored-by: Robert Pająk <pellared@hotmail.com>
This commit is contained in:
Sam Xie
2026-05-20 07:25:54 -07:00
committed by GitHub
parent 552680a415
commit 97447f5c54
5 changed files with 320 additions and 69 deletions
+1
View File
@@ -83,6 +83,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Fix counting of spans and logs in self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`, `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`, `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`, and `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8254)
- Drop conflicting scope attributes named `name`, `version`, or `schema_url` from metric labels in `go.opentelemetry.io/otel/exporters/prometheus`, preserving the dedicated `otel_scope_name`, `otel_scope_version`, and `otel_scope_schema_url` labels. (#8264)
- Close schema files opened by `ParseFile` in `go.opentelemetry.io/otel/schema/v1.0` and `go.opentelemetry.io/otel/schema/v1.1`. ([GHSA-995v-fvrw-c78m](https://github.com/open-telemetry/opentelemetry-go/security/advisories/GHSA-995v-fvrw-c78m))
- Enforce the 8192-byte baggage size limit during extraction/parsing, changing behavior when the limit is exceeded in `go.opentelemetry.io/otel/baggage` and `go.opentelemetry.io/otel/propagation`. (#8222)
<!-- Released section -->
<!-- Don't change this section unless doing release -->
+26 -4
View File
@@ -14,6 +14,10 @@ import (
)
const (
maxParseErrors = 5
// W3C Baggage specification limits.
// https://www.w3.org/TR/baggage/#limits
maxMembers = 64
maxBytesPerBaggageString = 8192
@@ -493,9 +497,15 @@ func New(members ...Member) (Baggage, error) {
// from the W3C Baggage specification which allows duplicate list-members, but
// conforms to the OpenTelemetry Baggage specification.
//
// If the baggage-string exceeds the maximum allowed members (64) or bytes
// (8192), members are dropped until the limits are satisfied and an error is
// returned along with the partial result.
// If the raw baggage-string exceeds the maximum allowed bytes (8192), an
// empty Baggage and an error are returned.
//
// Otherwise, members are parsed left-to-right and accumulated until one of
// the following conditions is reached, at which point parsing stops and an
// error is returned alongside the partial result:
// - accepting the next member would cause the encoded baggage to exceed
// 8192 bytes, or
// - the baggage already contains 64 distinct keys.
//
// Invalid members are skipped and the error is returned along with the
// partial result containing the valid members.
@@ -504,9 +514,14 @@ func Parse(bStr string) (Baggage, error) {
return Baggage{}, nil
}
if n := len(bStr); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}
b := make(baggage.List)
sizes := make(map[string]int) // Track per-key byte sizes
var totalBytes int
var parseErrors int
var truncateErr error
for memberStr := range strings.SplitSeq(bStr, listDelimiter) {
// Check member count limit.
@@ -517,7 +532,10 @@ func Parse(bStr string) (Baggage, error) {
m, err := parseMember(memberStr)
if err != nil {
truncateErr = errors.Join(truncateErr, err)
parseErrors++
if parseErrors <= maxParseErrors {
truncateErr = errors.Join(truncateErr, err)
}
continue // skip invalid member, keep processing
}
@@ -553,6 +571,10 @@ func Parse(bStr string) (Baggage, error) {
totalBytes = newTotalBytes
}
if dropped := parseErrors - maxParseErrors; dropped > 0 {
truncateErr = errors.Join(truncateErr, fmt.Errorf("and %d more invalid member(s)", dropped))
}
if len(b) == 0 {
return Baggage{}, truncateErr
}
+63 -15
View File
@@ -528,8 +528,7 @@ func TestBaggageParse(t *testing.T) {
{
name: "invalid baggage string: too large",
in: tooLarge,
// tooLarge is a single key without "=", so parseMember fails
err: errInvalidMember,
err: errBaggageBytes,
},
{
name: "baggage string with too many members keeps first 64",
@@ -544,31 +543,26 @@ func TestBaggageParse(t *testing.T) {
err: errMemberNumber,
},
{
name: "baggage string exceeds byte limit returns partial result",
name: "baggage string at max size is accepted",
in: func() string {
// Create members that collectively exceed maxBytesPerBaggageString.
// Each member: "kN=" + value. We use values large enough that
// a few members fit but the total exceeds 8192 bytes.
// Create a baggage string of exactly maxBytesPerBaggageString.
// 3 members: "k0=" + 2727v + "," + "k1=" + 2727v + "," + "k2=" + 2727v
// = 3*(3+2727) + 2 = 8192
val := strings.Repeat("v", 2727)
var parts []string
val := strings.Repeat("v", 2000)
for i := range 10 {
for i := range 3 {
parts = append(parts, fmt.Sprintf("k%d=%s", i, val))
}
return strings.Join(parts, ",")
}(),
want: func() baggage.List {
// Only members that fit within 8192 bytes should be kept.
// Each member is ~2003 bytes ("kN=" + 2000 "v"s), plus comma.
// 4 members = 4*2003 + 3 commas = 8015 bytes (fits).
// 5 members = 5*2003 + 4 commas = 10019 bytes (exceeds).
b := make(baggage.List)
val := strings.Repeat("v", 2000)
for i := range 4 {
val := strings.Repeat("v", 2727)
for i := range 3 {
b[fmt.Sprintf("k%d", i)] = baggage.Item{Value: val}
}
return b
}(),
err: errBaggageBytes,
},
{
name: "percent-encoded octet sequences do not match the UTF-8 encoding scheme",
@@ -1272,3 +1266,57 @@ func BenchmarkMemberString(b *testing.B) {
_ = member.String()
}
}
func BenchmarkParseOversized(b *testing.B) {
// 1MB oversized baggage string.
oversized := strings.Repeat("k=v,", 250000)
b.ReportAllocs()
for b.Loop() {
_, _ = Parse(oversized)
}
}
func TestParseErrorCap(t *testing.T) {
// Build a baggage string with many invalid members (no '=' delimiter).
// All within the 8192 byte limit.
var parts []string
for i := range 20 {
parts = append(parts, fmt.Sprintf("bad%d", i))
}
// Add one valid member so the baggage is not empty.
parts = append(parts, "good=val")
bStr := strings.Join(parts, ",")
b, err := Parse(bStr)
assert.ErrorIs(t, err, errInvalidMember)
assert.Equal(t, 1, b.Len(), "should return the valid member")
// Count the number of joined errors.
errs := err.Error()
invalidCount := strings.Count(errs, "invalid baggage list-member")
assert.Equal(t, maxParseErrors, invalidCount,
"should cap individual parse errors at maxParseErrors")
assert.Contains(t, errs, "and 15 more invalid member(s)")
}
func TestParseErrorCapAllInvalid(t *testing.T) {
// All members invalid, no valid members. Exercises the len(b)==0
// return path with a capped error message.
var parts []string
for i := range 20 {
parts = append(parts, fmt.Sprintf("bad%d", i))
}
bStr := strings.Join(parts, ",")
b, err := Parse(bStr)
assert.ErrorIs(t, err, errInvalidMember)
assert.Equal(t, 0, b.Len(), "should return empty baggage")
errs := err.Error()
invalidCount := strings.Count(errs, "invalid baggage list-member")
assert.Equal(t, maxParseErrors, invalidCount,
"should cap individual parse errors at maxParseErrors")
assert.Contains(t, errs, "and 15 more invalid member(s)")
}
+48 -11
View File
@@ -5,6 +5,8 @@ package propagation // import "go.opentelemetry.io/otel/propagation"
import (
"context"
"errors"
"fmt"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/internal/errorhandler"
@@ -13,9 +15,12 @@ import (
const (
baggageHeader = "baggage"
maxParseErrors = 5
// W3C Baggage specification limits.
// https://www.w3.org/TR/baggage/#limits
maxMembers = 64
maxMembers = 64
maxBytesPerBaggageString = 8192
)
// Baggage is a propagator that supports the W3C Baggage format.
@@ -72,24 +77,56 @@ func extractMultiBaggage(parent context.Context, carrier ValuesGetter) context.C
}
var members []baggage.Member
for _, bStr := range bVals {
currBag, err := baggage.Parse(bStr)
if err != nil {
errorhandler.GetErrorHandler().Handle(err)
var totalBytes int
var parseErrors int
var truncateErr error
for i, bStr := range bVals {
if i > 0 {
totalBytes++ // comma separator between combined header values
}
if currBag.Len() == 0 {
continue
totalBytes += len(bStr)
if totalBytes > maxBytesPerBaggageString {
// Per the W3C Baggage spec, the byte limit applies to the
// combination of all baggage headers, not each header
// individually. Mirror the single-header behavior of
// reporting the error and returning the parent context
// with no baggage attached.
errorhandler.GetErrorHandler().Handle(fmt.Errorf(
"baggage: aggregate header size %d exceeds %d byte limit",
totalBytes,
maxBytesPerBaggageString,
))
return parent
}
members = append(members, currBag.Members()...)
if len(members) >= maxMembers {
break
// If members exceed the limit, stop parsing baggage.
if len(members) <= maxMembers {
currBag, err := baggage.Parse(bStr)
if err != nil {
parseErrors++
if parseErrors <= maxParseErrors {
truncateErr = errors.Join(truncateErr, err)
}
}
if currBag.Len() == 0 {
continue
}
members = append(members, currBag.Members()...)
}
}
if dropped := parseErrors - maxParseErrors; dropped > 0 {
truncateErr = errors.Join(truncateErr, fmt.Errorf("and %d more error(s)", dropped))
}
b, err := baggage.New(members...)
if err != nil {
errorhandler.GetErrorHandler().Handle(err)
truncateErr = errors.Join(truncateErr, err)
}
if truncateErr != nil {
errorhandler.GetErrorHandler().Handle(truncateErr)
}
if b.Len() == 0 {
return parent
}
+182 -39
View File
@@ -12,10 +12,13 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/propagation"
)
const maxBytesPerBaggageString = 8192
type property struct {
Key, Value string
}
@@ -161,18 +164,29 @@ func generateMembers(n int, prefix string) members {
return m
}
// generateFixedWidthBaggageHeader creates a baggage header where every member
// has the same byte length, ensuring deterministic String() size regardless of
// which members baggage.New() keeps after map-order truncation.
// Each member is "prefixNN=vNN" with zero-padded indices.
func generateFixedWidthBaggageHeader(n int, prefix string) string {
parts := make([]string, n)
for i := range parts {
parts[i] = fmt.Sprintf("%s%02d=v%02d", prefix, i, i)
}
return strings.Join(parts, ",")
}
func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
// W3C Baggage spec limits: https://www.w3.org/TR/baggage/#limits
const maxMembers = 64
const maxBytesPerBaggageString = 8192
prop := propagation.TextMapPropagator(propagation.Baggage{})
tests := []struct {
name string
headers []string
want members
wantCount int // Used when want is nil and we only care about count.
wantMaxBytes int // Used to check that baggage size doesn't exceed limit.
name string
headers []string
want members
wantCount int // Used when want is nil and we only care about count.
wantBytes int // Used to check exact baggage size when want is nil.
}{
{
name: "non conflicting headers",
@@ -239,13 +253,13 @@ func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
{
name: "multiple headers exceeds total max members limit keeps 64",
headers: []string{
generateBaggageHeader(maxMembers/2, "a"),
generateBaggageHeader(maxMembers/2, "b"),
generateBaggageHeader(1, "c"),
generateFixedWidthBaggageHeader(maxMembers/2, "a"),
generateFixedWidthBaggageHeader(maxMembers/2, "b"),
generateFixedWidthBaggageHeader(1, "c"),
},
want: nil, // Non-deterministic truncation by baggage.New()
wantCount: maxMembers,
wantMaxBytes: maxBytesPerBaggageString,
want: nil, // Non-deterministic truncation by baggage.New()
wantCount: maxMembers,
wantBytes: maxMembers*7 + maxMembers - 1, // 64 uniform "xNN=vNN" members + 63 commas
},
{
name: "single header at max bytes limit",
@@ -255,19 +269,17 @@ func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
},
},
{
name: "single header exceeds max bytes limit drops oversized member",
name: "single header exceeds max bytes limit will be dropped entirely",
headers: []string{"k=" + strings.Repeat("v", maxBytesPerBaggageString-1)},
want: members{},
},
{
name: "multiple headers exceed total max bytes keeps one that fits",
name: "multiple headers exceed total max bytes drops everything",
headers: []string{
"k=" + strings.Repeat("v", maxBytesPerBaggageString-2),
"y=" + strings.Repeat("v", maxBytesPerBaggageString-2),
},
want: nil, // Non-deterministic: either k or y will be kept
wantCount: 1, // Only one member fits
wantMaxBytes: maxBytesPerBaggageString,
want: members{},
},
{
name: "multiple headers within total max bytes",
@@ -282,32 +294,49 @@ func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
},
},
{
name: "many headers exceeding member limit caps collection early",
name: "empty headers between non-empty do count comma separators",
headers: []string{
"k=" + strings.Repeat("v", maxBytesPerBaggageString/2-2),
"",
"",
// The comma as the separator of member would take 1 byte.
"y=" + strings.Repeat("v", maxBytesPerBaggageString/2-2-1),
},
want: members{},
},
{
name: "empty headers before non-empty do count comma separators",
headers: []string{
// The comma as the separator of member would take 1 byte.
"",
"k=" + strings.Repeat("v", maxBytesPerBaggageString/2-2),
// The comma as the separator of member would take 1 byte.
"y=" + strings.Repeat("v", maxBytesPerBaggageString/2-2-1),
},
want: members{},
},
{
name: "multiple headers exceed total max bytes due to comma separator",
headers: []string{
// Two equal halves: each is exactly maxBytesPerBaggageString/2 bytes.
// Without the comma separator: 4096 + 4096 = 8192 (at limit).
// With the comma separator: 4096 + 1 + 4096 = 8193 (over limit).
"k=" + strings.Repeat("v", maxBytesPerBaggageString/2-2),
"y=" + strings.Repeat("v", maxBytesPerBaggageString/2-2),
},
want: members{},
},
{
name: "many headers total max members and max bytes",
headers: func() []string {
// 100 headers with 10 members each = 1000 total members.
// The cap should stop collecting after ~maxMembers and
// New() truncates to exactly maxMembers.
h := make([]string, 100)
for i := range h {
h[i] = generateBaggageHeader(10, fmt.Sprintf("h%d_k", i))
h[i] = generateFixedWidthBaggageHeader(10, fmt.Sprintf("h%02d_k", i))
}
return h
}(),
wantCount: maxMembers,
wantMaxBytes: maxBytesPerBaggageString,
},
{
name: "skips large member that exceeds byte limit and continues",
headers: []string{
"small1=v1,small2=v2",
"large=" + strings.Repeat("x", maxBytesPerBaggageString),
"small3=v3",
},
want: members{
{Key: "small1", Value: "v1"},
{Key: "small2", Value: "v2"},
{Key: "small3", Value: "v3"},
},
want: members{},
},
}
@@ -324,10 +353,9 @@ func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
if tt.want != nil {
expected := tt.want.Baggage(t)
assert.Equal(t, expected, got)
} else if tt.wantCount > 0 {
// If only count is specified, verify count and byte limit
} else {
assert.Equal(t, tt.wantCount, got.Len(), "expected member count")
assert.LessOrEqual(t, len(got.String()), tt.wantMaxBytes, "baggage size exceeds limit")
assert.Len(t, got.String(), tt.wantBytes, "expected baggage size")
}
})
}
@@ -498,3 +526,118 @@ func TestBaggagePropagatorGetAllKeys(t *testing.T) {
t.Errorf("GetAllKeys: -got +want %s", diff)
}
}
func TestExtractOversizedSingleBaggageHeader(t *testing.T) {
prop := propagation.Baggage{}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.com", http.NoBody)
// Set a single baggage header exceeding 8192 bytes.
req.Header.Set("baggage", "key="+strings.Repeat("v", maxBytesPerBaggageString))
ctx := prop.Extract(t.Context(), propagation.HeaderCarrier(req.Header))
got := baggage.FromContext(ctx)
assert.Equal(t, 0, got.Len(), "oversized header should result in empty baggage")
}
type errHandler struct {
err error
}
func (e *errHandler) Handle(err error) { e.err = err }
func TestExtractManyBaggageHeader(t *testing.T) {
tests := []struct {
name string
headers func() []string
want func() members
wantErrStr []string
}{
{
name: "aggregate byte budget exceeded drops everything",
headers: func() []string {
// 100 headers, each ~195 bytes. Total: ~19.5KB, well over 8192.
h := make([]string, 100)
for i := range 100 {
h[i] = fmt.Sprintf("k%d=%s", i, strings.Repeat("v", 190))
}
return h
},
want: func() members {
return members{}
},
wantErrStr: []string{"aggregate header size 8374 exceeds 8192 byte limit"},
},
{
name: "too many invalid headers triggers error cap",
headers: func() []string {
// 10 invalid headers (no '=' delimiter) followed by 1 valid.
// Each invalid header produces 1 parse error from baggage.Parse.
// maxParseErrors=5, so 5 errors are kept and 5 are summarized.
h := make([]string, 11)
for i := range 10 {
h[i] = fmt.Sprintf("bad%d", i)
}
h[10] = "good=val"
return h
},
want: func() members {
return members{{Key: "good", Value: "val"}}
},
wantErrStr: []string{"invalid baggage list-member", "and 5 more error(s)"},
},
{
name: "single header with many invalid members preserves inner error",
headers: func() []string {
// One header with 10 invalid members. baggage.Parse caps its own
// joined error at maxParseErrors=5 and appends its own summary.
// extractMultiBaggage must count this as a single failing header
// so the inner error is still attached to the outer error chain.
invalid := make([]string, 10)
for i := range invalid {
invalid[i] = fmt.Sprintf("bad%d", i)
}
return []string{strings.Join(invalid, ",") + ",good=val"}
},
want: func() members {
return members{{Key: "good", Value: "val"}}
},
wantErrStr: []string{"invalid baggage list-member", "and 5 more invalid member(s)"},
},
{
name: "member cap does not stop aggregate byte accounting",
headers: func() []string {
return []string{
generateFixedWidthBaggageHeader(64, "a"),
"oversized=" + strings.Repeat("v", maxBytesPerBaggageString),
}
},
want: func() members {
return members{}
},
wantErrStr: []string{"exceeds 8192 byte limit"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originalErrorHandler := otel.GetErrorHandler()
eh := &errHandler{}
otel.SetErrorHandler(eh)
t.Cleanup(func() { otel.SetErrorHandler(originalErrorHandler) })
prop := propagation.Baggage{}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.com", http.NoBody)
for _, h := range tt.headers() {
req.Header.Add("baggage", h)
}
ctx := prop.Extract(t.Context(), propagation.HeaderCarrier(req.Header))
got := baggage.FromContext(ctx)
assert.Equal(t, tt.want().Baggage(t), got)
assert.Error(t, eh.err)
for _, s := range tt.wantErrStr {
assert.Contains(t, eh.err.Error(), s)
}
})
}
}