...

Source file src/go.opentelemetry.io/otel/baggage/baggage.go

Documentation: go.opentelemetry.io/otel/baggage

     1  // Copyright The OpenTelemetry Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package baggage // import "go.opentelemetry.io/otel/baggage"
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"net/url"
    21  	"regexp"
    22  	"strings"
    23  
    24  	"go.opentelemetry.io/otel/internal/baggage"
    25  )
    26  
    27  const (
    28  	maxMembers               = 180
    29  	maxBytesPerMembers       = 4096
    30  	maxBytesPerBaggageString = 8192
    31  
    32  	listDelimiter     = ","
    33  	keyValueDelimiter = "="
    34  	propertyDelimiter = ";"
    35  
    36  	keyDef      = `([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+)`
    37  	valueDef    = `([\x21\x23-\x2b\x2d-\x3a\x3c-\x5B\x5D-\x7e]*)`
    38  	keyValueDef = `\s*` + keyDef + `\s*` + keyValueDelimiter + `\s*` + valueDef + `\s*`
    39  )
    40  
    41  var (
    42  	keyRe      = regexp.MustCompile(`^` + keyDef + `$`)
    43  	valueRe    = regexp.MustCompile(`^` + valueDef + `$`)
    44  	propertyRe = regexp.MustCompile(`^(?:\s*` + keyDef + `\s*|` + keyValueDef + `)$`)
    45  )
    46  
    47  var (
    48  	errInvalidKey      = errors.New("invalid key")
    49  	errInvalidValue    = errors.New("invalid value")
    50  	errInvalidProperty = errors.New("invalid baggage list-member property")
    51  	errInvalidMember   = errors.New("invalid baggage list-member")
    52  	errMemberNumber    = errors.New("too many list-members in baggage-string")
    53  	errMemberBytes     = errors.New("list-member too large")
    54  	errBaggageBytes    = errors.New("baggage-string too large")
    55  )
    56  
    57  // Property is an additional metadata entry for a baggage list-member.
    58  type Property struct {
    59  	key, value string
    60  
    61  	// hasValue indicates if a zero-value value means the property does not
    62  	// have a value or if it was the zero-value.
    63  	hasValue bool
    64  }
    65  
    66  // NewKeyProperty returns a new Property for key.
    67  //
    68  // If key is invalid, an error will be returned.
    69  func NewKeyProperty(key string) (Property, error) {
    70  	if !keyRe.MatchString(key) {
    71  		return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
    72  	}
    73  
    74  	p := Property{key: key}
    75  	return p, nil
    76  }
    77  
    78  // NewKeyValueProperty returns a new Property for key with value.
    79  //
    80  // If key or value are invalid, an error will be returned.
    81  func NewKeyValueProperty(key, value string) (Property, error) {
    82  	if !keyRe.MatchString(key) {
    83  		return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
    84  	}
    85  	if !valueRe.MatchString(value) {
    86  		return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value)
    87  	}
    88  
    89  	p := Property{
    90  		key:      key,
    91  		value:    value,
    92  		hasValue: true,
    93  	}
    94  	return p, nil
    95  }
    96  
    97  func newInvalidProperty() Property {
    98  	return Property{}
    99  }
   100  
   101  // parseProperty attempts to decode a Property from the passed string. It
   102  // returns an error if the input is invalid according to the W3C Baggage
   103  // specification.
   104  func parseProperty(property string) (Property, error) {
   105  	if property == "" {
   106  		return newInvalidProperty(), nil
   107  	}
   108  
   109  	match := propertyRe.FindStringSubmatch(property)
   110  	if len(match) != 4 {
   111  		return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property)
   112  	}
   113  
   114  	var p Property
   115  	if match[1] != "" {
   116  		p.key = match[1]
   117  	} else {
   118  		p.key = match[2]
   119  		p.value = match[3]
   120  		p.hasValue = true
   121  	}
   122  
   123  	return p, nil
   124  }
   125  
   126  // validate ensures p conforms to the W3C Baggage specification, returning an
   127  // error otherwise.
   128  func (p Property) validate() error {
   129  	errFunc := func(err error) error {
   130  		return fmt.Errorf("invalid property: %w", err)
   131  	}
   132  
   133  	if !keyRe.MatchString(p.key) {
   134  		return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key))
   135  	}
   136  	if p.hasValue && !valueRe.MatchString(p.value) {
   137  		return errFunc(fmt.Errorf("%w: %q", errInvalidValue, p.value))
   138  	}
   139  	if !p.hasValue && p.value != "" {
   140  		return errFunc(errors.New("inconsistent value"))
   141  	}
   142  	return nil
   143  }
   144  
   145  // Key returns the Property key.
   146  func (p Property) Key() string {
   147  	return p.key
   148  }
   149  
   150  // Value returns the Property value. Additionally, a boolean value is returned
   151  // indicating if the returned value is the empty if the Property has a value
   152  // that is empty or if the value is not set.
   153  func (p Property) Value() (string, bool) {
   154  	return p.value, p.hasValue
   155  }
   156  
   157  // String encodes Property into a string compliant with the W3C Baggage
   158  // specification.
   159  func (p Property) String() string {
   160  	if p.hasValue {
   161  		return fmt.Sprintf("%s%s%v", p.key, keyValueDelimiter, p.value)
   162  	}
   163  	return p.key
   164  }
   165  
   166  type properties []Property
   167  
   168  func fromInternalProperties(iProps []baggage.Property) properties {
   169  	if len(iProps) == 0 {
   170  		return nil
   171  	}
   172  
   173  	props := make(properties, len(iProps))
   174  	for i, p := range iProps {
   175  		props[i] = Property{
   176  			key:      p.Key,
   177  			value:    p.Value,
   178  			hasValue: p.HasValue,
   179  		}
   180  	}
   181  	return props
   182  }
   183  
   184  func (p properties) asInternal() []baggage.Property {
   185  	if len(p) == 0 {
   186  		return nil
   187  	}
   188  
   189  	iProps := make([]baggage.Property, len(p))
   190  	for i, prop := range p {
   191  		iProps[i] = baggage.Property{
   192  			Key:      prop.key,
   193  			Value:    prop.value,
   194  			HasValue: prop.hasValue,
   195  		}
   196  	}
   197  	return iProps
   198  }
   199  
   200  func (p properties) Copy() properties {
   201  	if len(p) == 0 {
   202  		return nil
   203  	}
   204  
   205  	props := make(properties, len(p))
   206  	copy(props, p)
   207  	return props
   208  }
   209  
   210  // validate ensures each Property in p conforms to the W3C Baggage
   211  // specification, returning an error otherwise.
   212  func (p properties) validate() error {
   213  	for _, prop := range p {
   214  		if err := prop.validate(); err != nil {
   215  			return err
   216  		}
   217  	}
   218  	return nil
   219  }
   220  
   221  // String encodes properties into a string compliant with the W3C Baggage
   222  // specification.
   223  func (p properties) String() string {
   224  	props := make([]string, len(p))
   225  	for i, prop := range p {
   226  		props[i] = prop.String()
   227  	}
   228  	return strings.Join(props, propertyDelimiter)
   229  }
   230  
   231  // Member is a list-member of a baggage-string as defined by the W3C Baggage
   232  // specification.
   233  type Member struct {
   234  	key, value string
   235  	properties properties
   236  
   237  	// hasData indicates whether the created property contains data or not.
   238  	// Properties that do not contain data are invalid with no other check
   239  	// required.
   240  	hasData bool
   241  }
   242  
   243  // NewMember returns a new Member from the passed arguments. The key will be
   244  // used directly while the value will be url decoded after validation. An error
   245  // is returned if the created Member would be invalid according to the W3C
   246  // Baggage specification.
   247  func NewMember(key, value string, props ...Property) (Member, error) {
   248  	m := Member{
   249  		key:        key,
   250  		value:      value,
   251  		properties: properties(props).Copy(),
   252  		hasData:    true,
   253  	}
   254  	if err := m.validate(); err != nil {
   255  		return newInvalidMember(), err
   256  	}
   257  	decodedValue, err := url.PathUnescape(value)
   258  	if err != nil {
   259  		return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
   260  	}
   261  	m.value = decodedValue
   262  	return m, nil
   263  }
   264  
   265  func newInvalidMember() Member {
   266  	return Member{}
   267  }
   268  
   269  // parseMember attempts to decode a Member from the passed string. It returns
   270  // an error if the input is invalid according to the W3C Baggage
   271  // specification.
   272  func parseMember(member string) (Member, error) {
   273  	if n := len(member); n > maxBytesPerMembers {
   274  		return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n)
   275  	}
   276  
   277  	var (
   278  		key, value string
   279  		props      properties
   280  	)
   281  
   282  	keyValue, properties, found := strings.Cut(member, propertyDelimiter)
   283  	if found {
   284  		// Parse the member properties.
   285  		for _, pStr := range strings.Split(properties, propertyDelimiter) {
   286  			p, err := parseProperty(pStr)
   287  			if err != nil {
   288  				return newInvalidMember(), err
   289  			}
   290  			props = append(props, p)
   291  		}
   292  	}
   293  	// Parse the member key/value pair.
   294  
   295  	// Take into account a value can contain equal signs (=).
   296  	k, v, found := strings.Cut(keyValue, keyValueDelimiter)
   297  	if !found {
   298  		return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member)
   299  	}
   300  	// "Leading and trailing whitespaces are allowed but MUST be trimmed
   301  	// when converting the header into a data structure."
   302  	key = strings.TrimSpace(k)
   303  	var err error
   304  	value, err = url.PathUnescape(strings.TrimSpace(v))
   305  	if err != nil {
   306  		return newInvalidMember(), fmt.Errorf("%w: %q", err, value)
   307  	}
   308  	if !keyRe.MatchString(key) {
   309  		return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key)
   310  	}
   311  	if !valueRe.MatchString(value) {
   312  		return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
   313  	}
   314  
   315  	return Member{key: key, value: value, properties: props, hasData: true}, nil
   316  }
   317  
   318  // validate ensures m conforms to the W3C Baggage specification.
   319  // A key is just an ASCII string, but a value must be URL encoded UTF-8,
   320  // returning an error otherwise.
   321  func (m Member) validate() error {
   322  	if !m.hasData {
   323  		return fmt.Errorf("%w: %q", errInvalidMember, m)
   324  	}
   325  
   326  	if !keyRe.MatchString(m.key) {
   327  		return fmt.Errorf("%w: %q", errInvalidKey, m.key)
   328  	}
   329  	if !valueRe.MatchString(m.value) {
   330  		return fmt.Errorf("%w: %q", errInvalidValue, m.value)
   331  	}
   332  	return m.properties.validate()
   333  }
   334  
   335  // Key returns the Member key.
   336  func (m Member) Key() string { return m.key }
   337  
   338  // Value returns the Member value.
   339  func (m Member) Value() string { return m.value }
   340  
   341  // Properties returns a copy of the Member properties.
   342  func (m Member) Properties() []Property { return m.properties.Copy() }
   343  
   344  // String encodes Member into a string compliant with the W3C Baggage
   345  // specification.
   346  func (m Member) String() string {
   347  	// A key is just an ASCII string, but a value is URL encoded UTF-8.
   348  	s := fmt.Sprintf("%s%s%s", m.key, keyValueDelimiter, url.QueryEscape(m.value))
   349  	if len(m.properties) > 0 {
   350  		s = fmt.Sprintf("%s%s%s", s, propertyDelimiter, m.properties.String())
   351  	}
   352  	return s
   353  }
   354  
   355  // Baggage is a list of baggage members representing the baggage-string as
   356  // defined by the W3C Baggage specification.
   357  type Baggage struct { //nolint:golint
   358  	list baggage.List
   359  }
   360  
   361  // New returns a new valid Baggage. It returns an error if it results in a
   362  // Baggage exceeding limits set in that specification.
   363  //
   364  // It expects all the provided members to have already been validated.
   365  func New(members ...Member) (Baggage, error) {
   366  	if len(members) == 0 {
   367  		return Baggage{}, nil
   368  	}
   369  
   370  	b := make(baggage.List)
   371  	for _, m := range members {
   372  		if !m.hasData {
   373  			return Baggage{}, errInvalidMember
   374  		}
   375  
   376  		// OpenTelemetry resolves duplicates by last-one-wins.
   377  		b[m.key] = baggage.Item{
   378  			Value:      m.value,
   379  			Properties: m.properties.asInternal(),
   380  		}
   381  	}
   382  
   383  	// Check member numbers after deduplication.
   384  	if len(b) > maxMembers {
   385  		return Baggage{}, errMemberNumber
   386  	}
   387  
   388  	bag := Baggage{b}
   389  	if n := len(bag.String()); n > maxBytesPerBaggageString {
   390  		return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
   391  	}
   392  
   393  	return bag, nil
   394  }
   395  
   396  // Parse attempts to decode a baggage-string from the passed string. It
   397  // returns an error if the input is invalid according to the W3C Baggage
   398  // specification.
   399  //
   400  // If there are duplicate list-members contained in baggage, the last one
   401  // defined (reading left-to-right) will be the only one kept. This diverges
   402  // from the W3C Baggage specification which allows duplicate list-members, but
   403  // conforms to the OpenTelemetry Baggage specification.
   404  func Parse(bStr string) (Baggage, error) {
   405  	if bStr == "" {
   406  		return Baggage{}, nil
   407  	}
   408  
   409  	if n := len(bStr); n > maxBytesPerBaggageString {
   410  		return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
   411  	}
   412  
   413  	b := make(baggage.List)
   414  	for _, memberStr := range strings.Split(bStr, listDelimiter) {
   415  		m, err := parseMember(memberStr)
   416  		if err != nil {
   417  			return Baggage{}, err
   418  		}
   419  		// OpenTelemetry resolves duplicates by last-one-wins.
   420  		b[m.key] = baggage.Item{
   421  			Value:      m.value,
   422  			Properties: m.properties.asInternal(),
   423  		}
   424  	}
   425  
   426  	// OpenTelemetry does not allow for duplicate list-members, but the W3C
   427  	// specification does. Now that we have deduplicated, ensure the baggage
   428  	// does not exceed list-member limits.
   429  	if len(b) > maxMembers {
   430  		return Baggage{}, errMemberNumber
   431  	}
   432  
   433  	return Baggage{b}, nil
   434  }
   435  
   436  // Member returns the baggage list-member identified by key.
   437  //
   438  // If there is no list-member matching the passed key the returned Member will
   439  // be a zero-value Member.
   440  // The returned member is not validated, as we assume the validation happened
   441  // when it was added to the Baggage.
   442  func (b Baggage) Member(key string) Member {
   443  	v, ok := b.list[key]
   444  	if !ok {
   445  		// We do not need to worry about distinguishing between the situation
   446  		// where a zero-valued Member is included in the Baggage because a
   447  		// zero-valued Member is invalid according to the W3C Baggage
   448  		// specification (it has an empty key).
   449  		return newInvalidMember()
   450  	}
   451  
   452  	return Member{
   453  		key:        key,
   454  		value:      v.Value,
   455  		properties: fromInternalProperties(v.Properties),
   456  		hasData:    true,
   457  	}
   458  }
   459  
   460  // Members returns all the baggage list-members.
   461  // The order of the returned list-members does not have significance.
   462  //
   463  // The returned members are not validated, as we assume the validation happened
   464  // when they were added to the Baggage.
   465  func (b Baggage) Members() []Member {
   466  	if len(b.list) == 0 {
   467  		return nil
   468  	}
   469  
   470  	members := make([]Member, 0, len(b.list))
   471  	for k, v := range b.list {
   472  		members = append(members, Member{
   473  			key:        k,
   474  			value:      v.Value,
   475  			properties: fromInternalProperties(v.Properties),
   476  			hasData:    true,
   477  		})
   478  	}
   479  	return members
   480  }
   481  
   482  // SetMember returns a copy the Baggage with the member included. If the
   483  // baggage contains a Member with the same key the existing Member is
   484  // replaced.
   485  //
   486  // If member is invalid according to the W3C Baggage specification, an error
   487  // is returned with the original Baggage.
   488  func (b Baggage) SetMember(member Member) (Baggage, error) {
   489  	if !member.hasData {
   490  		return b, errInvalidMember
   491  	}
   492  
   493  	n := len(b.list)
   494  	if _, ok := b.list[member.key]; !ok {
   495  		n++
   496  	}
   497  	list := make(baggage.List, n)
   498  
   499  	for k, v := range b.list {
   500  		// Do not copy if we are just going to overwrite.
   501  		if k == member.key {
   502  			continue
   503  		}
   504  		list[k] = v
   505  	}
   506  
   507  	list[member.key] = baggage.Item{
   508  		Value:      member.value,
   509  		Properties: member.properties.asInternal(),
   510  	}
   511  
   512  	return Baggage{list: list}, nil
   513  }
   514  
   515  // DeleteMember returns a copy of the Baggage with the list-member identified
   516  // by key removed.
   517  func (b Baggage) DeleteMember(key string) Baggage {
   518  	n := len(b.list)
   519  	if _, ok := b.list[key]; ok {
   520  		n--
   521  	}
   522  	list := make(baggage.List, n)
   523  
   524  	for k, v := range b.list {
   525  		if k == key {
   526  			continue
   527  		}
   528  		list[k] = v
   529  	}
   530  
   531  	return Baggage{list: list}
   532  }
   533  
   534  // Len returns the number of list-members in the Baggage.
   535  func (b Baggage) Len() int {
   536  	return len(b.list)
   537  }
   538  
   539  // String encodes Baggage into a string compliant with the W3C Baggage
   540  // specification. The returned string will be invalid if the Baggage contains
   541  // any invalid list-members.
   542  func (b Baggage) String() string {
   543  	members := make([]string, 0, len(b.list))
   544  	for k, v := range b.list {
   545  		members = append(members, Member{
   546  			key:        k,
   547  			value:      v.Value,
   548  			properties: fromInternalProperties(v.Properties),
   549  		}.String())
   550  	}
   551  	return strings.Join(members, listDelimiter)
   552  }
   553  

View as plain text