Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ The next release will require at least [Go 1.24].
- Change `AssertEqual` in `go.opentelemetry.io/otel/log/logtest` to accept `TestingT` in order to support benchmarks and fuzz tests. (#6908)
- Change `SDKProcessorLogQueueCapacity`, `SDKProcessorLogQueueSize`, `SDKProcessorSpanQueueSize`, and `SDKProcessorSpanQueueCapacity` in `go.opentelemetry.io/otel/semconv/v1.36.0/otelconv` to use a `Int64ObservableUpDownCounter`. (#7041)
- Change `DefaultExemplarReservoirProviderSelector` in `go.opentelemetry.io/otel/sdk/metric` to use `runtime.GOMAXPROCS(0)` instead of `runtime.NumCPU()` for the `FixedSizeReservoirProvider` default size. (#7094)
- Optimize `TraceIDFromHex` and `SpanIDFromHex` in `go.opentelemetry.io/otel/sdk/trace`. (#6791)

### Deprecated

Expand Down
39 changes: 39 additions & 0 deletions sdk/trace/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,45 @@ func BenchmarkSpanWithEvents_WithTimestamp(b *testing.B) {
})
}

func BenchmarkTraceIDFromHex(b *testing.B) {
want := trace.TraceID{
0xde,
0xad,
0xbe,
0xef,
0x01,
0x23,
0x45,
0x67,
0x89,
0xab,
0xcd,
0xef,
0x01,
0x23,
0x45,
0x67,
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
got, _ := trace.TraceIDFromHex("deadbeef0123456789abcdef01234567")
if got != want {
b.Fatalf("got = %q want = %q", got.String(), want)
}
}
}

func BenchmarkSpanIDFromHex(b *testing.B) {
want := trace.SpanID{0xde, 0xad, 0xbe, 0xef, 0x01, 0x23, 0x45, 0x67}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
got, _ := trace.SpanIDFromHex("deadbeef01234567")
if got != want {
b.Fatalf("got = %q want = %q", got.String(), want)
}
}
}

func BenchmarkTraceID_DotString(b *testing.B) {
t, _ := trace.TraceIDFromHex("0000000000000001000000000000002a")
sc := trace.NewSpanContext(trace.SpanContextConfig{TraceID: t})
Expand Down
38 changes: 38 additions & 0 deletions trace/hex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package trace // import "go.opentelemetry.io/otel/trace"

const (
// hexLU is a hex lookup table of the 16 lowercase hex digits.
// The character values of the string are indexed at the equivalent
// hexadecimal value they represent. This table efficiently encodes byte data
// into a string representation of hexadecimal.
hexLU = "0123456789abcdef"

// hexRev is a reverse hex lookup table for lowercase hex digits.
Comment thread
MrAlias marked this conversation as resolved.
// The table is efficiently decodes a hexadecimal string into bytes.
// Valid hexadecimal characters are indexed at their respective values. All
// other invalid ASCII characters are represented with '\xff'.
//
// The '\xff' character is used as invalid because no valid character has
// the upper 4 bits set. Meaning, an efficient validation can be performed
// over multiple character parsing by checking these bits remain zero.
hexRev = "" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
)
138 changes: 90 additions & 48 deletions trace/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
package trace // import "go.opentelemetry.io/otel/trace"

import (
"bytes"
"encoding/hex"
"encoding/json"
)

Expand Down Expand Up @@ -41,18 +39,44 @@ var (
// IsValid reports whether the trace TraceID is valid. A valid trace ID does
// not consist of zeros only.
func (t TraceID) IsValid() bool {
return !bytes.Equal(t[:], nilTraceID[:])
return t != nilTraceID
}

// MarshalJSON implements a custom marshal function to encode TraceID
// as a hex string.
func (t TraceID) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
b := [32 + 2]byte{0: '"', 33: '"'}
h := t.hexBytes()
copy(b[1:], h[:])
return b[:], nil
}

// String returns the hex string representation form of a TraceID.
func (t TraceID) String() string {
return hex.EncodeToString(t[:])
h := t.hexBytes()
return string(h[:])
}

// hexBytes returns the hex string representation form of a TraceID.
func (t TraceID) hexBytes() [32]byte {
return [32]byte{
hexLU[t[0x0]>>4], hexLU[t[0x0]&0xf],
hexLU[t[0x1]>>4], hexLU[t[0x1]&0xf],
hexLU[t[0x2]>>4], hexLU[t[0x2]&0xf],
hexLU[t[0x3]>>4], hexLU[t[0x3]&0xf],
hexLU[t[0x4]>>4], hexLU[t[0x4]&0xf],
hexLU[t[0x5]>>4], hexLU[t[0x5]&0xf],
hexLU[t[0x6]>>4], hexLU[t[0x6]&0xf],
hexLU[t[0x7]>>4], hexLU[t[0x7]&0xf],
hexLU[t[0x8]>>4], hexLU[t[0x8]&0xf],
hexLU[t[0x9]>>4], hexLU[t[0x9]&0xf],
hexLU[t[0xa]>>4], hexLU[t[0xa]&0xf],
hexLU[t[0xb]>>4], hexLU[t[0xb]&0xf],
hexLU[t[0xc]>>4], hexLU[t[0xc]&0xf],
hexLU[t[0xd]>>4], hexLU[t[0xd]&0xf],
hexLU[t[0xe]>>4], hexLU[t[0xe]&0xf],
hexLU[t[0xf]>>4], hexLU[t[0xf]&0xf],
}
}

// SpanID is a unique identity of a span in a trace.
Expand All @@ -66,78 +90,88 @@ var (
// IsValid reports whether the SpanID is valid. A valid SpanID does not consist
// of zeros only.
func (s SpanID) IsValid() bool {
return !bytes.Equal(s[:], nilSpanID[:])
return s != nilSpanID
}

// MarshalJSON implements a custom marshal function to encode SpanID
// as a hex string.
func (s SpanID) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
b := [16 + 2]byte{0: '"', 17: '"'}
h := s.hexBytes()
copy(b[1:], h[:])
return b[:], nil
}

// String returns the hex string representation form of a SpanID.
func (s SpanID) String() string {
return hex.EncodeToString(s[:])
b := s.hexBytes()
return string(b[:])
}

func (s SpanID) hexBytes() [16]byte {
return [16]byte{
hexLU[s[0]>>4], hexLU[s[0]&0xf],
hexLU[s[1]>>4], hexLU[s[1]&0xf],
hexLU[s[2]>>4], hexLU[s[2]&0xf],
hexLU[s[3]>>4], hexLU[s[3]&0xf],
hexLU[s[4]>>4], hexLU[s[4]&0xf],
hexLU[s[5]>>4], hexLU[s[5]&0xf],
hexLU[s[6]>>4], hexLU[s[6]&0xf],
hexLU[s[7]>>4], hexLU[s[7]&0xf],
}
}

// TraceIDFromHex returns a TraceID from a hex string if it is compliant with
// the W3C trace-context specification. See more at
// https://www.w3.org/TR/trace-context/#trace-id
// nolint:revive // revive complains about stutter of `trace.TraceIDFromHex`.
func TraceIDFromHex(h string) (TraceID, error) {
t := TraceID{}
if len(h) != 32 {
return t, errInvalidTraceIDLength
return [16]byte{}, errInvalidTraceIDLength
}

if err := decodeHex(h, t[:]); err != nil {
return t, err
var b [16]byte
invalidMark := byte(0)
for i := 0; i < len(h); i += 4 {
b[i/2] = (hexRev[h[i]] << 4) | hexRev[h[i+1]]
b[i/2+1] = (hexRev[h[i+2]] << 4) | hexRev[h[i+3]]
invalidMark |= hexRev[h[i]] | hexRev[h[i+1]] | hexRev[h[i+2]] | hexRev[h[i+3]]
}

if !t.IsValid() {
return t, errNilTraceID
// If the upper 4 bits of any byte are not zero, there was an invalid hex
// character since invalid hex characters are 0xff in hexRev.
if invalidMark&0xf0 != 0 {
return [16]byte{}, errInvalidHexID
}
// If we didn't set any bits, then h was all zeros.
if invalidMark == 0 {
return [16]byte{}, errNilTraceID
}
return t, nil
return b, nil
}

// SpanIDFromHex returns a SpanID from a hex string if it is compliant
// with the w3c trace-context specification.
// See more at https://www.w3.org/TR/trace-context/#parent-id
func SpanIDFromHex(h string) (SpanID, error) {
s := SpanID{}
if len(h) != 16 {
return s, errInvalidSpanIDLength
}

if err := decodeHex(h, s[:]); err != nil {
return s, err
return [8]byte{}, errInvalidSpanIDLength
}

if !s.IsValid() {
return s, errNilSpanID
var b [8]byte
invalidMark := byte(0)
for i := 0; i < len(h); i += 4 {
b[i/2] = (hexRev[h[i]] << 4) | hexRev[h[i+1]]
b[i/2+1] = (hexRev[h[i+2]] << 4) | hexRev[h[i+3]]
invalidMark |= hexRev[h[i]] | hexRev[h[i+1]] | hexRev[h[i+2]] | hexRev[h[i+3]]
}
return s, nil
}

func decodeHex(h string, b []byte) error {
for _, r := range h {
switch {
case 'a' <= r && r <= 'f':
continue
case '0' <= r && r <= '9':
continue
default:
return errInvalidHexID
}
// If the upper 4 bits of any byte are not zero, there was an invalid hex
// character since invalid hex characters are 0xff in hexRev.
if invalidMark&0xf0 != 0 {
return [8]byte{}, errInvalidHexID
}

decoded, err := hex.DecodeString(h)
if err != nil {
return err
// If we didn't set any bits, then h was all zeros.
if invalidMark == 0 {
return [8]byte{}, errNilSpanID
}

copy(b, decoded)
return nil
return b, nil
}

// TraceFlags contains flags that can be set on a SpanContext.
Expand All @@ -160,12 +194,20 @@ func (tf TraceFlags) WithSampled(sampled bool) TraceFlags { // nolint:revive //
// MarshalJSON implements a custom marshal function to encode TraceFlags
// as a hex string.
func (tf TraceFlags) MarshalJSON() ([]byte, error) {
return json.Marshal(tf.String())
b := [2 + 2]byte{0: '"', 3: '"'}
h := tf.hexBytes()
copy(b[1:], h[:])
return b[:], nil
}

// String returns the hex string representation form of TraceFlags.
func (tf TraceFlags) String() string {
return hex.EncodeToString([]byte{byte(tf)})
h := tf.hexBytes()
return string(h[:])
}

func (tf TraceFlags) hexBytes() [2]byte {
return [2]byte{hexLU[tf>>4], hexLU[tf&0xf]}
}

// SpanContextConfig contains mutable fields usable for constructing
Expand Down
Loading