Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve tracestate performance #4722

Merged
merged 13 commits into from
Dec 2, 2023
20 changes: 12 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Changed

- Improve `go.opentelemetry.io/otel/trace.TraceState` 's performance (#4722)
xiehuc marked this conversation as resolved.
Show resolved Hide resolved

### Removed

- Remove the deprecated `go.opentelemetry.io/otel/bridge/opencensus.NewTracer`. (#4706)
Expand Down Expand Up @@ -1189,7 +1193,7 @@ We have updated the project minimum supported Go version to 1.16

### Fixed

- json stdout exporter no longer crashes due to concurrency bug. (#2265)
- JSON stdout exporter no longer crashes due to concurrency bug. (#2265)
xiehuc marked this conversation as resolved.
Show resolved Hide resolved

## [Metrics 0.24.0] - 2021-10-01

Expand Down Expand Up @@ -1587,7 +1591,7 @@ with major version 0.

### Added

- Added `Marshaler` config option to `otlphttp` to enable otlp over json or protobufs. (#1586)
- Added `Marshaler` config option to `otlphttp` to enable otlp over JSON or protobufs. (#1586)
- A `ForceFlush` method to the `"go.opentelemetry.io/otel/sdk/trace".TracerProvider` to flush all registered `SpanProcessor`s. (#1608)
- Added `WithSampler` and `WithSpanLimits` to tracer provider. (#1633, #1702)
- `"go.opentelemetry.io/otel/trace".SpanContext` now has a `remote` property, and `IsRemote()` predicate, that is true when the `SpanContext` has been extracted from remote context data. (#1701)
Expand Down Expand Up @@ -1890,7 +1894,7 @@ with major version 0.
### Changed

- Add reconnecting udp connection type to Jaeger exporter.
This change adds a new optional implementation of the udp conn interface used to detect changes to an agent's host dns record.
This change adds a new optional implementation of the udp conn interface used to detect changes to an agent's host DNS record.
It then adopts the new destination address to ensure the exporter doesn't get stuck. This change was ported from jaegertracing/jaeger-client-go#520. (#1063)
- Replace `StartOption` and `EndOption` in `go.opentelemetry.io/otel/api/trace` with `SpanOption`.
This change is matched by replacing the `StartConfig` and `EndConfig` with a unified `SpanConfig`. (#1108)
Expand Down Expand Up @@ -2066,7 +2070,7 @@ This release migrates the default OpenTelemetry SDK into its own Go module, deco

- A new Resource Detector interface is included to allow resources to be automatically detected and included. (#939)
- A Detector to automatically detect resources from an environment variable. (#939)
- Github action to generate protobuf Go bindings locally in `internal/opentelemetry-proto-gen`. (#938)
- GitHub action to generate protobuf Go bindings locally in `internal/opentelemetry-proto-gen`. (#938)
- OTLP .proto files from `open-telemetry/opentelemetry-proto` imported as a git submodule under `internal/opentelemetry-proto`.
References to `github.com/open-telemetry/opentelemetry-proto` changed to `go.opentelemetry.io/otel/internal/opentelemetry-proto-gen`. (#942)

Expand All @@ -2089,7 +2093,7 @@ This release migrates the default OpenTelemetry SDK into its own Go module, deco
- Add `peer.service` semantic attribute. (#898)
- Add database-specific semantic attributes. (#899)
- Add semantic convention for `faas.coldstart` and `container.id`. (#909)
- Add http content size semantic conventions. (#905)
- Add HTTP content size semantic conventions. (#905)
- Include `http.request_content_length` in HTTP request basic attributes. (#905)
- Add semantic conventions for operating system process resource attribute keys. (#919)
- The Jaeger exporter now has a `WithBatchMaxCount` option to specify the maximum number of spans sent in a batch. (#931)
Expand Down Expand Up @@ -2153,7 +2157,7 @@ This release implements the v0.5.0 version of the OpenTelemetry specification.
- The othttp instrumentation now includes default metrics. (#861)
- This CHANGELOG file to track all changes in the project going forward.
- Support for array type attributes. (#798)
- Apply transitive dependabot go.mod dependency updates as part of a new automatic Github workflow. (#844)
- Apply transitive dependabot go.mod dependency updates as part of a new automatic GitHub workflow. (#844)
- Timestamps are now passed to exporters for each export. (#835)
- Add new `Accumulation` type to metric SDK to transport telemetry from `Accumulator`s to `Processor`s.
This replaces the prior `Record` `struct` use for this purpose. (#835)
Expand Down Expand Up @@ -2284,7 +2288,7 @@ This release implements the v0.5.0 version of the OpenTelemetry specification.
- Alias `api` types to root package of project. (#696)
- Create basic `othttp.Transport` for simple client instrumentation. (#678)
- `SetAttribute(string, interface{})` to the trace API. (#674)
- Jaeger exporter option that allows user to specify custom http client. (#671)
- Jaeger exporter option that allows user to specify custom HTTP client. (#671)
- `Stringer` and `Infer` methods to `key`s. (#662)

### Changed
Expand Down Expand Up @@ -2711,7 +2715,7 @@ This release contains a Metrics SDK with stdout exporter and supports basic aggr
## [0.1.0] - 2019-11-04

This is the first release of open-telemetry go library.
It contains api and sdk for trace and meter.
It contains API and SDK for trace and meter.

### Added

Expand Down
160 changes: 117 additions & 43 deletions trace/tracestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,14 @@ package trace // import "go.opentelemetry.io/otel/trace"
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)

const (
maxListMembers = 32

listDelimiter = ","

// based on the W3C Trace Context specification, see
// https://www.w3.org/TR/trace-context-1/#tracestate-header
noTenantKeyFormat = `[a-z][_0-9a-z\-\*\/]*`
withTenantKeyFormat = `[a-z0-9][_0-9a-z\-\*\/]*@[a-z][_0-9a-z\-\*\/]*`
valueFormat = `[\x20-\x2b\x2d-\x3c\x3e-\x7e]*[\x21-\x2b\x2d-\x3c\x3e-\x7e]`
listDelimiters = ","
memberDelimiter = "="

errInvalidKey errorConst = "invalid tracestate key"
errInvalidValue errorConst = "invalid tracestate value"
Expand All @@ -39,43 +33,102 @@ const (
errDuplicate errorConst = "duplicate list-member in tracestate"
)

var (
noTenantKeyRe = regexp.MustCompile(`^` + noTenantKeyFormat + `$`)
withTenantKeyRe = regexp.MustCompile(`^` + withTenantKeyFormat + `$`)
valueRe = regexp.MustCompile(`^` + valueFormat + `$`)
memberRe = regexp.MustCompile(`^\s*((?:` + noTenantKeyFormat + `)|(?:` + withTenantKeyFormat + `))=(` + valueFormat + `)\s*$`)
)

type member struct {
Key string
Value string
}

func newMember(key, value string) (member, error) {
if len(key) > 256 {
return member{}, fmt.Errorf("%w: %s", errInvalidKey, key)
func checkValueChar(v byte) bool {
// [\x20-\x2b\x2d-\x3c\x3e-\x7e]*
return v >= '\x20' && v <= '\x7e' && v != '\x2c' && v != '\x3d'
}

func checkValueLast(v byte) bool {
// [\x21-\x2b\x2d-\x3c\x3e-\x7e]
return v >= '\x21' && v <= '\x7e' && v != '\x2c' && v != '\x3d'
}

func checkValue(val string) bool {
n := len(val)
if n == 0 || n > 256 {
return false
}
if !noTenantKeyRe.MatchString(key) {
if !withTenantKeyRe.MatchString(key) {
return member{}, fmt.Errorf("%w: %s", errInvalidKey, key)
// valueFormat = `[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]`
for i := 0; i < n-1; i++ {
if !checkValueChar(val[i]) {
return false
}
atIndex := strings.LastIndex(key, "@")
if atIndex > 241 || len(key)-1-atIndex > 14 {
return member{}, fmt.Errorf("%w: %s", errInvalidKey, key)
}
return checkValueLast(val[n-1])
}

func checkKeyRemain(key string) bool {
// [_0-9a-z\-\*\/]*
for _, v := range key {
if (v >= '0' && v <= '9') || (v >= 'a' && v <= 'z') {
continue
}
switch v {
case '_', '-', '*', '/':
continue
}
return false
}
if len(value) > 256 || !valueRe.MatchString(value) {
return true
}

func checkKeyPart(key string, n int) bool {
xiehuc marked this conversation as resolved.
Show resolved Hide resolved
if len(key) == 0 {
return false
}
first := key[0] // key first char
ret := len(key[1:]) <= n
// [a-z]
ret = ret && first >= 'a' && first <= 'z'
return ret && checkKeyRemain(key[1:])
}

func checkKeyTenant(key string, n int) bool {
xiehuc marked this conversation as resolved.
Show resolved Hide resolved
if len(key) == 0 {
return false
}
first := key[0] // key first char
ret := len(key[1:]) <= n
// [a-z0-9]
ret = ret && ((first >= 'a' && first <= 'z') || (first >= '0' && first <= '9'))
return ret && checkKeyRemain(key[1:])
}

func checkKey(key string) bool {
// noTenantKeyFormat = `[a-z][_0-9a-z\-\*\/]{0,255}`
// withTenantKeyFormat = `[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}`
tenant, system, ok := strings.Cut(key, "@")
if !ok {
return checkKeyPart(key, 255)
}
return checkKeyTenant(tenant, 240) && checkKeyPart(system, 13)
}

// based on the W3C Trace Context specification, see
// https://www.w3.org/TR/trace-context-1/#tracestate-header
func newMember(key, value string) (member, error) {
if !checkKey(key) {
return member{}, fmt.Errorf("%w: %s", errInvalidKey, key)
}
if !checkValue(value) {
return member{}, fmt.Errorf("%w: %s", errInvalidValue, value)
xiehuc marked this conversation as resolved.
Show resolved Hide resolved
}
return member{Key: key, Value: value}, nil
}

func parseMember(m string) (member, error) {
matches := memberRe.FindStringSubmatch(m)
if len(matches) != 3 {
key, val, ok := strings.Cut(m, memberDelimiter)
if !ok {
return member{}, fmt.Errorf("%w: %s", errInvalidMember, m)
}
result, e := newMember(matches[1], matches[2])
key = strings.TrimLeft(key, " \t")
val = strings.TrimRight(val, " \t")
result, e := newMember(key, val)
if e != nil {
return member{}, fmt.Errorf("%w: %s", errInvalidMember, m)
}
Expand All @@ -85,7 +138,7 @@ func parseMember(m string) (member, error) {
// String encodes member into a string compliant with the W3C Trace Context
// specification.
func (m member) String() string {
return fmt.Sprintf("%s=%s", m.Key, m.Value)
return m.Key + "=" + m.Value
}

// TraceState provides additional vendor-specific trace identification
Expand All @@ -109,8 +162,8 @@ var _ json.Marshaler = TraceState{}
// ParseTraceState attempts to decode a TraceState from the passed
// string. It returns an error if the input is invalid according to the W3C
// Trace Context specification.
func ParseTraceState(tracestate string) (TraceState, error) {
if tracestate == "" {
func ParseTraceState(ts string) (TraceState, error) {
if ts == "" {
return TraceState{}, nil
}

Expand All @@ -120,7 +173,9 @@ func ParseTraceState(tracestate string) (TraceState, error) {

var members []member
found := make(map[string]struct{})
for _, memberStr := range strings.Split(tracestate, listDelimiter) {
for ts != "" {
var memberStr string
memberStr, ts, _ = strings.Cut(ts, listDelimiters)
if len(memberStr) == 0 {
continue
}
Expand Down Expand Up @@ -153,11 +208,20 @@ func (ts TraceState) MarshalJSON() ([]byte, error) {
// Trace Context specification. The returned string will be invalid if the
// TraceState contains any invalid members.
func (ts TraceState) String() string {
members := make([]string, len(ts.list))
for i, m := range ts.list {
members[i] = m.String()
if len(ts.list) == 0 {
return ""
}
var sb strings.Builder
xiehuc marked this conversation as resolved.
Show resolved Hide resolved
_, _ = sb.WriteString(ts.list[0].Key)
_ = sb.WriteByte('=')
_, _ = sb.WriteString(ts.list[0].Value)
for i := 1; i < len(ts.list); i++ {
_ = sb.WriteByte(listDelimiters[0])
_, _ = sb.WriteString(ts.list[i].Key)
_ = sb.WriteByte('=')
_, _ = sb.WriteString(ts.list[i].Value)
}
return strings.Join(members, listDelimiter)
return sb.String()
}

// Get returns the value paired with key from the corresponding TraceState
Expand Down Expand Up @@ -189,15 +253,25 @@ func (ts TraceState) Insert(key, value string) (TraceState, error) {
if err != nil {
return ts, err
}

cTS := ts.Delete(key)
if cTS.Len()+1 <= maxListMembers {
cTS.list = append(cTS.list, member{})
n := len(ts.list)
found := n
for i := range ts.list {
if ts.list[i].Key == key {
found = i
}
}
cTS := TraceState{}
if found == n && n < maxListMembers {
cTS.list = make([]member, n+1)
} else {
cTS.list = make([]member, n)
}
// When the number of members exceeds capacity, drop the "right-most".
copy(cTS.list[1:], cTS.list)
cTS.list[0] = m

// When the number of members exceeds capacity, drop the "right-most".
copy(cTS.list[1:], ts.list[0:found])
if found < n {
copy(cTS.list[1+found:], ts.list[found+1:])
}
return cTS, nil
}

Expand Down
46 changes: 46 additions & 0 deletions trace/tracestate_benchkmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package trace

import (
"testing"
)

func BenchmarkTraceStateParse(b *testing.B) {
for _, test := range testcases {
b.Run(test.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = ParseTraceState(test.in)
}
})
}
}

func BenchmarkTraceStateString(b *testing.B) {
for _, test := range testcases {
if len(test.tracestate.list) == 0 {
continue
}
b.Run(test.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = test.tracestate.String()
}
})
}
}