Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ The next release will require at least [Go 1.25].

- Support testing of [Go 1.26]. (#7902)

### Fixed

- Update `Baggage` in `go.opentelemetry.io/otel/propagation` and `Parse` and `New` in `go.opentelemetry.io/otel/baggage` to comply with W3C Baggage specification limits.
`New` and `Parse` now return partial baggage along with an error when limits are exceeded.
Errors from baggage extraction are reported to the global error handler. (#7880)

<!-- Released section -->
<!-- Don't change this section unless doing release -->

Expand Down
77 changes: 56 additions & 21 deletions baggage/baggage.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import (
)

const (
maxMembers = 180
maxBytesPerMembers = 4096
maxMembers = 64
maxBytesPerBaggageString = 8192

listDelimiter = ","
Expand All @@ -29,7 +28,6 @@ var (
errInvalidProperty = errors.New("invalid baggage list-member property")
errInvalidMember = errors.New("invalid baggage list-member")
errMemberNumber = errors.New("too many list-members in baggage-string")
errMemberBytes = errors.New("list-member too large")
errBaggageBytes = errors.New("baggage-string too large")
)

Expand Down Expand Up @@ -309,10 +307,6 @@ func newInvalidMember() Member {
// an error if the input is invalid according to the W3C Baggage
// specification.
func parseMember(member string) (Member, error) {
if n := len(member); n > maxBytesPerMembers {
return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n)
}

var props properties
keyValue, properties, found := strings.Cut(member, propertyDelimiter)
if found {
Expand Down Expand Up @@ -430,36 +424,60 @@ type Baggage struct { //nolint:golint
// New returns a new valid Baggage. It returns an error if it results in a
// Baggage exceeding limits set in that specification.
//
// If the resulting Baggage exceeds the maximum allowed members or bytes, the
// first N members that fit within the limits are kept and an error is returned
// along with the partial result.
//
// It expects all the provided members to have already been validated.
func New(members ...Member) (Baggage, error) {
if len(members) == 0 {
return Baggage{}, nil
}

b := make(baggage.List)
// Track insertion order for deterministic truncation.
var keys []string
for _, m := range members {
if !m.hasData {
return Baggage{}, errInvalidMember
}

// OpenTelemetry resolves duplicates by last-one-wins.
if _, exists := b[m.key]; !exists {
keys = append(keys, m.key)
}
b[m.key] = baggage.Item{
Value: m.value,
Properties: m.properties.asInternal(),
}
}
Comment thread
XSAM marked this conversation as resolved.

// Check member numbers after deduplication.
var truncateErr error

// Check member count after deduplication.
if len(b) > maxMembers {
return Baggage{}, errMemberNumber
truncateErr = errors.Join(truncateErr, errMemberNumber)
// Keep the first maxMembers keys.
for _, k := range keys[maxMembers:] {
delete(b, k)
}
keys = keys[:maxMembers]
}

// Check byte size.
bag := Baggage{b}
if n := len(bag.String()); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
truncateErr = errors.Join(truncateErr, fmt.Errorf("%w: %d", errBaggageBytes, n))
// Remove members from the end until the baggage fits.
for len(keys) > 0 && len(bag.String()) > maxBytesPerBaggageString {
last := keys[len(keys)-1]
delete(b, last)
keys = keys[:len(keys)-1]
bag = Baggage{b}
}
}

return bag, nil
return Baggage{b}, truncateErr
}

// Parse attempts to decode a baggage-string from the passed string. It
Expand All @@ -470,35 +488,52 @@ func New(members ...Member) (Baggage, error) {
// defined (reading left-to-right) will be the only one kept. This diverges
// 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), the first N members that fit within the limits are kept and an
// error is returned along with the partial result.
func Parse(bStr string) (Baggage, error) {
if bStr == "" {
return Baggage{}, nil
}

if n := len(bStr); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}

b := make(baggage.List)
var totalBytes int
var truncateErr error
for memberStr := range strings.SplitSeq(bStr, listDelimiter) {
// Check member count limit.
if len(b) >= maxMembers {
truncateErr = errors.Join(truncateErr, errMemberNumber)
break
Comment thread
XSAM marked this conversation as resolved.
}
Comment thread
XSAM marked this conversation as resolved.

m, err := parseMember(memberStr)
if err != nil {
return Baggage{}, err
}

// Check byte size limit.
// Account for comma separator between members.
memberBytes := len(m.String())
if len(b) > 0 {
memberBytes++ // comma separator
}
if totalBytes+memberBytes > maxBytesPerBaggageString {
truncateErr = errors.Join(truncateErr, errBaggageBytes)
break
Comment thread
XSAM marked this conversation as resolved.
}
Comment thread
XSAM marked this conversation as resolved.

// OpenTelemetry resolves duplicates by last-one-wins.
b[m.key] = baggage.Item{
Value: m.value,
Properties: m.properties.asInternal(),
}
totalBytes += memberBytes
Comment thread
XSAM marked this conversation as resolved.
Outdated
}

// OpenTelemetry does not allow for duplicate list-members, but the W3C
// specification does. Now that we have deduplicated, ensure the baggage
// does not exceed list-member limits.
if len(b) > maxMembers {
return Baggage{}, errMemberNumber
if truncateErr != nil {
return Baggage{b}, truncateErr
}

return Baggage{b}, nil
}

Expand Down
37 changes: 23 additions & 14 deletions baggage/baggage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,28 +257,34 @@ func key(n int) string {
}

func TestNewBaggageErrorTooManyBytes(t *testing.T) {
m := make([]Member, (maxBytesPerBaggageString/maxBytesPerMembers)+1)
// Create members that together exceed maxBytesPerBaggageString.
// Each member needs key + "=" so use keys that sum to > 8192 bytes.
keySize := maxBytesPerBaggageString / maxMembers
m := make([]Member, maxMembers)
for i := range m {
m[i] = Member{key: key(maxBytesPerMembers), hasData: true}
m[i] = Member{key: key(keySize), hasData: true}
}
_, err := New(m...)
b, err := New(m...)
assert.ErrorIs(t, err, errBaggageBytes)
// Partial result should contain members that fit within the byte limit.
assert.Positive(t, b.Len(), "should return partial baggage")
assert.LessOrEqual(t, len(b.String()), maxBytesPerBaggageString, "partial baggage should be within byte limit")
}

func TestNewBaggageErrorTooManyMembers(t *testing.T) {
m := make([]Member, maxMembers+1)
for i := range m {
m[i] = Member{key: fmt.Sprintf("%d", i), hasData: true}
}
_, err := New(m...)
b, err := New(m...)
assert.ErrorIs(t, err, errMemberNumber)
// Partial result should contain exactly maxMembers.
assert.Equal(t, maxMembers, b.Len(), "should return first %d members", maxMembers)
}

func TestBaggageParse(t *testing.T) {
tooLarge := key(maxBytesPerBaggageString + 1)

tooLargeMember := key(maxBytesPerMembers + 1)

m := make([]string, maxMembers+1)
for i := range m {
m[i] = fmt.Sprintf("a%d=", i)
Expand Down Expand Up @@ -518,17 +524,20 @@ func TestBaggageParse(t *testing.T) {
{
name: "invalid baggage string: too large",
in: tooLarge,
err: errBaggageBytes,
},
{
name: "invalid baggage string: member too large",
in: tooLargeMember,
err: errMemberBytes,
// tooLarge is a single key without "=", so parseMember fails
err: errInvalidMember,
},
{
name: "invalid baggage string: too many members",
name: "baggage string with too many members keeps first 64",
in: tooManyMembers,
err: errMemberNumber,
want: func() baggage.List {
b := make(baggage.List)
for i := range maxMembers {
b[fmt.Sprintf("a%d", i)] = baggage.Item{Value: ""}
}
return b
}(),
err: errMemberNumber,
},
{
name: "percent-encoded octet sequences do not match the UTF-8 encoding scheme",
Expand Down
96 changes: 96 additions & 0 deletions internal/errorhandler/errorhandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// Package errorhandler provides the global error handler for OpenTelemetry.
//
// This package has no OTel dependencies, allowing it to be imported by any
// package in the module without creating import cycles.
package errorhandler // import "go.opentelemetry.io/otel/internal/errorhandler"

import (
"errors"
"log"
"sync"
"sync/atomic"
)

// ErrorHandler handles irremediable events.
type ErrorHandler interface {
// Handle handles any error deemed irremediable by an OpenTelemetry
// component.
Handle(error)
}

type ErrDelegator struct {
delegate atomic.Pointer[ErrorHandler]
}

// Compile-time check that delegator implements ErrorHandler.
var _ ErrorHandler = (*ErrDelegator)(nil)

func (d *ErrDelegator) Handle(err error) {
if eh := d.delegate.Load(); eh != nil {
(*eh).Handle(err)
return
}
log.Print(err)
}

// setDelegate sets the ErrorHandler delegate.
func (d *ErrDelegator) setDelegate(eh ErrorHandler) {
d.delegate.Store(&eh)
}

type errorHandlerHolder struct {
eh ErrorHandler
}

var (
globalErrorHandler = defaultErrorHandler()
delegateErrorHandlerOnce sync.Once
)

// GetErrorHandler returns the global ErrorHandler instance.
//
// The default ErrorHandler instance returned will log all errors to STDERR
// until an override ErrorHandler is set with SetErrorHandler. All
// ErrorHandler returned prior to this will automatically forward errors to
// the set instance instead of logging.
//
// Subsequent calls to SetErrorHandler after the first will not forward errors
// to the new ErrorHandler for prior returned instances.
func GetErrorHandler() ErrorHandler {
return globalErrorHandler.Load().(errorHandlerHolder).eh
}

// SetErrorHandler sets the global ErrorHandler to h.
//
// The first time this is called all ErrorHandler previously returned from
// GetErrorHandler will send errors to h instead of the default logging
// ErrorHandler. Subsequent calls will set the global ErrorHandler, but not
// delegate errors to h.
func SetErrorHandler(h ErrorHandler) {
current := GetErrorHandler()

if _, cOk := current.(*ErrDelegator); cOk {
if _, ehOk := h.(*ErrDelegator); ehOk && current == h {
// Do not assign to the delegate of the default ErrDelegator to be
// itself.
log.Print(errors.New("no ErrorHandler delegate configured"), " ErrorHandler remains its current value.")
return
}
}

delegateErrorHandlerOnce.Do(func() {
if def, ok := current.(*ErrDelegator); ok {
def.setDelegate(h)
}
})
globalErrorHandler.Store(errorHandlerHolder{eh: h})
}

func defaultErrorHandler() *atomic.Value {
v := &atomic.Value{}
v.Store(errorHandlerHolder{eh: &ErrDelegator{}})
return v
}
Loading