Skip to content

Commit

Permalink
Validate members once, in NewMember (#2522)
Browse files Browse the repository at this point in the history
* use NewMember, or specify if the member is not validated when creating new ones

* expect members to already be validated when creating a new package

* add changelog entry

* add an isEmpty field to member and property for quick validation

* rename isEmpty to hasData

So by default, an empty struct really is marked as having no data

* Update baggage/baggage_test.go

Co-authored-by: Aaron Clawson <Aaron.Clawson@gmail.com>

* don't validate the member in parseMember, we alredy ran that validation

We also don't want to use NewMember, as that runs the property
validation again, making the benchmark quite slower

* move changelog entry to the fixed section

* provide the member/property data when returning an invalid error

Co-authored-by: Aaron Clawson <Aaron.Clawson@gmail.com>
  • Loading branch information
dmathieu and MadVikingGod committed Feb 4, 2022
1 parent c5776cc commit cb76cf1
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 51 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -30,6 +30,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Change the `otlpmetric.Client` interface's `UploadMetrics` method to accept a single `ResourceMetrics` instead of a slice of them. (#2491)
- Specify explicit buckets in Prometheus example. (#2493)
- W3C baggage will now decode urlescaped values. (#2529)
- Baggage members are now only validated once, when calling `NewMember` and not also when adding it to the baggage itself. (#2522)

### Removed

Expand Down
100 changes: 71 additions & 29 deletions baggage/baggage.go
Expand Up @@ -61,52 +61,65 @@ type Property struct {
// hasValue indicates if a zero-value value means the property does not
// have a value or if it was the zero-value.
hasValue bool

// hasData indicates whether the created property contains data or not.
// Properties that do not contain data are invalid with no other check
// required.
hasData bool
}

func NewKeyProperty(key string) (Property, error) {
p := Property{}
if !keyRe.MatchString(key) {
return p, fmt.Errorf("%w: %q", errInvalidKey, key)
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
p.key = key

p := Property{key: key, hasData: true}
return p, nil
}

func NewKeyValueProperty(key, value string) (Property, error) {
p := Property{}
if !keyRe.MatchString(key) {
return p, fmt.Errorf("%w: %q", errInvalidKey, key)
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
if !valueRe.MatchString(value) {
return p, fmt.Errorf("%w: %q", errInvalidValue, value)
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value)
}

p := Property{
key: key,
value: value,
hasValue: true,
hasData: true,
}
p.key = key
p.value = value
p.hasValue = true
return p, nil
}

func newInvalidProperty() Property {
return Property{}
}

// parseProperty attempts to decode a Property from the passed string. It
// returns an error if the input is invalid according to the W3C Baggage
// specification.
func parseProperty(property string) (Property, error) {
p := Property{}
if property == "" {
return p, nil
return newInvalidProperty(), nil
}

match := propertyRe.FindStringSubmatch(property)
if len(match) != 4 {
return p, fmt.Errorf("%w: %q", errInvalidProperty, property)
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property)
}

p := Property{hasData: true}
if match[1] != "" {
p.key = match[1]
} else {
p.key = match[2]
p.value = match[3]
p.hasValue = true
}

return p, nil
}

Expand All @@ -117,6 +130,10 @@ func (p Property) validate() error {
return fmt.Errorf("invalid property: %w", err)
}

if !p.hasData {
return errFunc(fmt.Errorf("%w: %q", errInvalidProperty, p))
}

if !keyRe.MatchString(p.key) {
return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key))
}
Expand Down Expand Up @@ -220,26 +237,40 @@ func (p properties) String() string {
type Member struct {
key, value string
properties properties

// hasData indicates whether the created property contains data or not.
// Properties that do not contain data are invalid with no other check
// required.
hasData bool
}

// NewMember returns a new Member from the passed arguments. An error is
// returned if the created Member would be invalid according to the W3C
// Baggage specification.
func NewMember(key, value string, props ...Property) (Member, error) {
m := Member{key: key, value: value, properties: properties(props).Copy()}
m := Member{
key: key,
value: value,
properties: properties(props).Copy(),
hasData: true,
}
if err := m.validate(); err != nil {
return Member{}, err
return newInvalidMember(), err
}

return m, nil
}

func newInvalidMember() Member {
return Member{}
}

// parseMember attempts to decode a Member from the passed string. It returns
// 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 Member{}, fmt.Errorf("%w: %d", errMemberBytes, n)
return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n)
}

var (
Expand All @@ -254,7 +285,7 @@ func parseMember(member string) (Member, error) {
for _, pStr := range strings.Split(parts[1], propertyDelimiter) {
p, err := parseProperty(pStr)
if err != nil {
return Member{}, err
return newInvalidMember(), err
}
props = append(props, p)
}
Expand All @@ -265,21 +296,21 @@ func parseMember(member string) (Member, error) {
// Take into account a value can contain equal signs (=).
kv := strings.SplitN(parts[0], keyValueDelimiter, 2)
if len(kv) != 2 {
return Member{}, fmt.Errorf("%w: %q", errInvalidMember, member)
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member)
}
// "Leading and trailing whitespaces are allowed but MUST be trimmed
// when converting the header into a data structure."
key = strings.TrimSpace(kv[0])
var err error
value, err = url.QueryUnescape(strings.TrimSpace(kv[1]))
if err != nil {
return Member{}, fmt.Errorf("%w: %q", err, value)
return newInvalidMember(), fmt.Errorf("%w: %q", err, value)
}
if !keyRe.MatchString(key) {
return Member{}, fmt.Errorf("%w: %q", errInvalidKey, key)
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
if !valueRe.MatchString(value) {
return Member{}, fmt.Errorf("%w: %q", errInvalidValue, value)
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
}
default:
// This should never happen unless a developer has changed the string
Expand All @@ -288,12 +319,16 @@ func parseMember(member string) (Member, error) {
panic("failed to parse baggage member")
}

return Member{key: key, value: value, properties: props}, nil
return Member{key: key, value: value, properties: props, hasData: true}, nil
}

// validate ensures m conforms to the W3C Baggage specification, returning an
// error otherwise.
func (m Member) validate() error {
if !m.hasData {
return fmt.Errorf("%w: %q", errInvalidMember, m)
}

if !keyRe.MatchString(m.key) {
return fmt.Errorf("%w: %q", errInvalidKey, m.key)
}
Expand Down Expand Up @@ -329,19 +364,21 @@ type Baggage struct { //nolint:golint
list baggage.List
}

// New returns a new valid Baggage. It returns an error if the passed members
// are invalid according to the W3C Baggage specification or if it results in
// a Baggage exceeding limits set in that specification.
// New returns a new valid Baggage. It returns an error if it results in a
// Baggage exceeding limits set in that specification.
//
// 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)
for _, m := range members {
if err := m.validate(); err != nil {
return Baggage{}, err
if !m.hasData {
return Baggage{}, errInvalidMember
}

// OpenTelemetry resolves duplicates by last-one-wins.
b[m.key] = baggage.Item{
Value: m.value,
Expand Down Expand Up @@ -406,14 +443,16 @@ func Parse(bStr string) (Baggage, error) {
//
// If there is no list-member matching the passed key the returned Member will
// be a zero-value Member.
// The returned member is not validated, as we assume the validation happened
// when it was added to the Baggage.
func (b Baggage) Member(key string) Member {
v, ok := b.list[key]
if !ok {
// We do not need to worry about distiguising between the situation
// where a zero-valued Member is included in the Baggage because a
// zero-valued Member is invalid according to the W3C Baggage
// specification (it has an empty key).
return Member{}
return newInvalidMember()
}

return Member{
Expand All @@ -425,6 +464,9 @@ func (b Baggage) Member(key string) Member {

// Members returns all the baggage list-members.
// The order of the returned list-members does not have significance.
//
// The returned members are not validated, as we assume the validation happened
// when they were added to the Baggage.
func (b Baggage) Members() []Member {
if len(b.list) == 0 {
return nil
Expand All @@ -448,8 +490,8 @@ func (b Baggage) Members() []Member {
// If member is invalid according to the W3C Baggage specification, an error
// is returned with the original Baggage.
func (b Baggage) SetMember(member Member) (Baggage, error) {
if err := member.validate(); err != nil {
return b, fmt.Errorf("%w: %s", errInvalidMember, err)
if !member.hasData {
return b, errInvalidMember
}

n := len(b.list)
Expand Down

0 comments on commit cb76cf1

Please sign in to comment.