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
4 changes: 4 additions & 0 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]

### Added

- Add `AlwaysRecord` sampler in `go.opentelemetry.io/otel/sdk/trace`. (#7724)

### Changed

- `Exporter` in `go.opentelemetry.io/otel/exporter/prometheus` ignores metrics with the scope `go.opentelemetry.io/contrib/bridges/prometheus`.
Expand Down
28 changes: 28 additions & 0 deletions sdk/trace/sampling.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,31 @@ func (pb parentBased) Description() string {
pb.config.localParentNotSampled.Description(),
)
}

// AlwaysRecord returns a sampler decorator which ensures that every span
// is passed to the SpanProcessor, even those that would be normally dropped.
// It converts `Drop` decisions from the root sampler into `RecordOnly` decisions,
// allowing processors to see all spans without sending them to exporters. This is
// typically used to enable accurate span-to-metrics processing.
func AlwaysRecord(root Sampler) Sampler {
return alwaysRecord{root}
}

type alwaysRecord struct {
root Sampler
}

func (ar alwaysRecord) ShouldSample(p SamplingParameters) SamplingResult {
rootSamplerSamplingResult := ar.root.ShouldSample(p)
if rootSamplerSamplingResult.Decision == Drop {
return SamplingResult{
Decision: RecordOnly,
Tracestate: trace.SpanContextFromContext(p.ParentContext).TraceState(),
}
}
return rootSamplerSamplingResult
}

func (ar alwaysRecord) Description() string {
return "AlwaysRecord{root:" + ar.root.Description() + "}"
}
61 changes: 61 additions & 0 deletions sdk/trace/sampling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,64 @@ func TestTracestateIsPassed(t *testing.T) {
})
}
}

func TestAlwaysRecordSamplingDecision(t *testing.T) {
traceID, _ := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736")
spanID, _ := trace.SpanIDFromHex("00f067aa0ba902b7")

testCases := []struct {
name string
rootSampler Sampler
expectedDecision SamplingDecision
}{
{
name: "when root sampler decision is RecordAndSample, AlwaysRecord returns RecordAndSample",
rootSampler: AlwaysSample(),
expectedDecision: RecordAndSample,
},
{
name: "when root sampler decision is Drop, AlwaysRecord returns RecordOnly",
rootSampler: NeverSample(),
expectedDecision: RecordOnly,
},
{
name: "when root sampler decision is RecordOnly, AlwaysRecord returns RecordOnly",
rootSampler: RecordingOnly(),
expectedDecision: RecordOnly,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
sampler := AlwaysRecord(tc.rootSampler)
parentCtx := trace.ContextWithSpanContext(
t.Context(),
trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
}),
)
samplingResult := sampler.ShouldSample(SamplingParameters{ParentContext: parentCtx})
if samplingResult.Decision != tc.expectedDecision {
t.Errorf("Sampling decision should be %v, got %v instead",
tc.expectedDecision,
samplingResult.Decision,
)
}
})
}
}

func TestAlwaysRecordDefaultDescription(t *testing.T) {
sampler := AlwaysRecord(NeverSample())

expectedDescription := fmt.Sprintf("AlwaysRecord{root:%s}", NeverSample().Description())

if sampler.Description() != expectedDescription {
t.Errorf("Sampler description should be %s, got '%s' instead",
expectedDescription,
sampler.Description(),
)
}
}