Skip to content
Open
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 @@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add support for per-series start time tracking for cumulative metrics in `go.opentelemetry.io/otel/sdk/metric`.
Set `OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true` to enable. (#8060)
- Add `WithCardinalityLimitSelector` for metric reader for configuring cardinality limits specific to the instrument kind. (#7855)
- Add experimental `TraceIDRatioBased` sampler in `go.opentelemetry.io/otel/sdk/trace/x` that conforms to the [OpenTelemetry specification's threshold-based sampling algorithm](https://opentelemetry.io/docs/specs/otel/trace/sdk/#traceidratiobased). (#8123)

### Changed

Expand Down
41 changes: 41 additions & 0 deletions sdk/trace/x/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Experimental Features

The Trace SDK contains features that have not yet stabilized in the OpenTelemetry specification.
These features are added to the OpenTelemetry Go Trace SDK prior to stabilization in the specification so that users can start experimenting with them and provide feedback.

These features may change in backwards incompatible ways as feedback is applied.
See the [Compatibility and Stability](#compatibility-and-stability) section for more information.

## Features

- [TraceIDRatioBased Sampler](#traceidratiobased-sampler)

### TraceIDRatioBased Sampler

`TraceIDRatioBased` is a threshold-based sampler that conforms to the [OpenTelemetry specification's TraceIdRatioBased sampler](https://opentelemetry.io/docs/specs/otel/trace/sdk/#traceidratiobased).

It uses the least significant 56 bits of the trace ID (per [W3C Trace Context Level 2 Random Trace ID Flag](https://www.w3.org/TR/trace-context-2/#random-trace-id-flag)) for deterministic sampling decisions and propagates the sampling threshold via the `th` sub-key in the W3C `ot` tracestate vendor key.

#### Usage

```go
import (
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/x"
)

tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(
sdktrace.ParentBased(x.TraceIDRatioBased(0.5)),
),
)
```

## Compatibility and Stability

Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../VERSIONING.md).
These features may be removed or modified in successive version releases, including patch versions.

When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release.
There is no guarantee that any environment variable feature flags that enabled the experimental feature will be supported by the stable version.
If they are supported, they may be accompanied with a deprecation notice stating a timeline for the removal of that support.
132 changes: 132 additions & 0 deletions sdk/trace/x/sampler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// Package x contains experimental trace features.
package x // import "go.opentelemetry.io/otel/sdk/trace/x"

import (
"encoding/binary"
"fmt"
"math"
"strconv"
"strings"

"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)

const (
// defaultSamplingPrecision is the default precision for threshold encoding.
defaultSamplingPrecision = 4
maxAdjustedCount = 1 << 56
// randomnessMask masks the least significant 56 bits of the trace ID per
// W3C Trace Context Level 2 Random Trace ID Flag.
// https://www.w3.org/TR/trace-context-2/#random-trace-id-flag
randomnessMask = maxAdjustedCount - 1

probabilityZeroThreshold = 1 / float64(maxAdjustedCount)
probabilityOneThreshold = 1 - 0x1p-52
)

// traceIDRatioSampler is the sdktrace.Sampler implementation used by
// TraceIDRatioBased.
type traceIDRatioSampler struct {
threshold uint64
thkv string
description string
}

// ShouldSample implements sdktrace.Sampler.
func (ts *traceIDRatioSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult {
psc := trace.SpanContextFromContext(p.ParentContext)
state := psc.TraceState()

existingOtts := state.Get("ot")

var randomness uint64
var hasRandomness bool
if existingOtts != "" {
randomness, hasRandomness = tracestateRandomness(existingOtts)
}

if !hasRandomness {
randomness = binary.BigEndian.Uint64(p.TraceID[8:16]) & randomnessMask
}

if ts.threshold > randomness {
return sdktrace.SamplingResult{
Decision: sdktrace.Drop,
Tracestate: state,
}
}

var newOtts string
// Only insert/update th when randomness is available (either from
// explicit rv value or trace ID with the random flag). Otherwise,
// erase any existing th to signal the span is not guaranteed to be
// statistically representative.
// See https://opentelemetry.io/docs/specs/otel/trace/tracestate-probability-sampling/#general-requirements
if hasRandomness || psc.TraceFlags().IsRandom() {
newOtts = InsertOrUpdateTraceStateThKeyValue(existingOtts, ts.thkv)
} else {
newOtts = eraseTraceStateThKeyValue(existingOtts)
}

if newOtts == "" {
state = state.Delete("ot")
return sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample, Tracestate: state}
}
combined, err := state.Insert("ot", newOtts)
if err != nil {
otel.Handle(fmt.Errorf("could not combine tracestate: %w", err))
return sdktrace.SamplingResult{Decision: sdktrace.Drop, Tracestate: state}
}
return sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample, Tracestate: combined}
}

// Description implements sdktrace.Sampler.
func (ts *traceIDRatioSampler) Description() string {
return ts.description
}

// TraceIDRatioBased samples a given fraction of traces. Fractions >= 1 will
// always sample. Fractions < 0 are treated as zero. To respect the parent
// trace's SampledFlag, the TraceIDRatioBased sampler should be used as a
// delegate of a ParentBased sampler.
//
//nolint:revive // revive complains about stutter of `x.TraceIDRatioBased`
func TraceIDRatioBased(fraction float64) sdktrace.Sampler {
const (
maxp = 14
defp = defaultSamplingPrecision
hbits = 4
)
if fraction > probabilityOneThreshold {
return sdktrace.AlwaysSample()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't propagate th properly, right? It also doesn't return the correct description.

Same comment for NeverSample below.

Copy link
Copy Markdown
Contributor

@jmacd jmacd Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I compared with https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/208b7c0565fe51033fa8d8b3d96a3c3dcba79a3f/pkg/sampling/probability.go#L33, which is an ancestor of this. It returns an error in these cases.

I realize this may have happened because in an earlier implementation of this, I had assumed that AlwaysSample() would return a sampler that sets ot=th:0. @yuanyuanzhao3 sadly note that https://opentelemetry.io/docs/specs/otel/trace/sdk/#alwayson does not dictate to set th:0, and I'm not sure myself whether this counts as oversight or just the path of least resistance. It suggests we should have ProbabilitySampler with fraction=1 fix this, later we can discuss modifying the OTel SDK (which is I think what we want).

}
if fraction < probabilityZeroThreshold {
return sdktrace.NeverSample()
}

_, expF := math.Frexp(fraction)
_, expR := math.Frexp(1 - fraction)
precision := min(maxp, max(defp+expF/-hbits, defp+expR/-hbits))

scaled := uint64(math.Round(fraction * float64(maxAdjustedCount)))
threshold := maxAdjustedCount - scaled

if shift := hbits * (maxp - precision); shift != 0 {
half := uint64(1) << (shift - 1)
threshold += half
threshold >>= shift
threshold <<= shift
}

tvalue := strings.TrimRight(strconv.FormatUint(maxAdjustedCount+threshold, 16)[1:], "0")
return &traceIDRatioSampler{
threshold: threshold,
thkv: "th:" + tvalue,
description: fmt.Sprintf("TraceIDRatioBased{%g}", fraction),
}
}
Loading
Loading