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

Update Samplers to conform to Spec #531

Merged
merged 9 commits into from
Mar 10, 2020
5 changes: 4 additions & 1 deletion api/trace/always_sampler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ func (as alwaysSampleSampler) ShouldSample(
_ core.SpanContext,
_ bool,
_ core.TraceID,
_ uint64,
_ core.SpanID,
_ string,
_ SpanKind,
_ []core.KeyValue,
_ []Link,
) Decision {
return alwaysSampleDecision
}
Expand Down
2 changes: 1 addition & 1 deletion api/trace/always_sampler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

func TestShouldSample(t *testing.T) {
gotD := AlwaysSampleSampler().ShouldSample(
core.SpanContext{}, false, core.TraceID{}, 0, "span")
core.SpanContext{}, false, core.TraceID{}, core.SpanID{}, "span", SpanKindClient, []core.KeyValue{}, []Link{})
wantD := Decision{Sampled: true}
if diff := cmp.Diff(wantD, gotD); diff != "" {
t.Errorf("Decision: +got, -want%v", diff)
Expand Down
5 changes: 4 additions & 1 deletion api/trace/never_sampler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ func (ns neverSampleSampler) ShouldSample(
_ core.SpanContext,
_ bool,
_ core.TraceID,
_ uint64,
_ core.SpanID,
_ string,
_ SpanKind,
_ []core.KeyValue,
_ []Link,
) Decision {
return neverSampledecision
}
Expand Down
2 changes: 1 addition & 1 deletion api/trace/never_sampler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

func TestNeverSamperShouldSample(t *testing.T) {
gotD := NeverSampleSampler().ShouldSample(
core.SpanContext{}, false, core.TraceID{}, 0, "span")
core.SpanContext{}, false, core.TraceID{}, core.SpanID{}, "span", SpanKindClient, []core.KeyValue{}, []Link{})
wantD := Decision{Sampled: false}
if diff := cmp.Diff(wantD, gotD); diff != "" {
t.Errorf("Decision: +got, -want%v", diff)
Expand Down
5 changes: 4 additions & 1 deletion api/trace/sampler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ type Sampler interface {
sc core.SpanContext,
remote bool,
traceID core.TraceID,
spanID uint64,
spanID core.SpanID,
spanName string,
spanKind SpanKind,
attributes []core.KeyValue,
links []Link,
) Decision

// Description returns of the sampler. It contains its name or short description
Expand Down
92 changes: 74 additions & 18 deletions sdk/trace/sampling.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ package trace

import (
"encoding/binary"
"fmt"

"go.opentelemetry.io/otel/api/core"
api "go.opentelemetry.io/otel/api/trace"
)

// Sampler decides whether a trace should be sampled and exported.
type Sampler func(SamplingParameters) SamplingDecision
type Sampler interface {
ShouldSample(SamplingParameters) SamplingResult
Description() string
}

// SamplingParameters contains the values passed to a Sampler.
type SamplingParameters struct {
Expand All @@ -30,11 +35,46 @@ type SamplingParameters struct {
SpanID core.SpanID
Name string
HasRemoteParent bool
Kind api.SpanKind
Attributes []core.KeyValue
Links []api.Link
}

// SamplingDecision indicates whether a span is recorded and sampled.
type SamplingDecision uint8

// Valid sampling decisions
const (
NotRecord SamplingDecision = iota
Record
RecordAndSampled
krnowak marked this conversation as resolved.
Show resolved Hide resolved
)

// SamplingResult conveys a SamplingDecision and a set of Attributes.
type SamplingResult struct {
Decision SamplingDecision
Attributes []core.KeyValue
}

type probabilitySampler struct {
traceIDUpperBound uint64
description string
}

// SamplingDecision is the value returned by a Sampler.
type SamplingDecision struct {
Sample bool
func (ps probabilitySampler) ShouldSample(p SamplingParameters) SamplingResult {
if p.ParentContext.IsSampled() {
return SamplingResult{Decision: RecordAndSampled}
}

x := binary.BigEndian.Uint64(p.TraceID[0:8]) >> 1
if x < ps.traceIDUpperBound {
return SamplingResult{Decision: RecordAndSampled}
}
return SamplingResult{Decision: NotRecord}
}

func (ps probabilitySampler) Description() string {
return ps.description
}

// ProbabilitySampler samples a given fraction of traces. Fractions >= 1 will
Expand All @@ -49,37 +89,53 @@ func ProbabilitySampler(fraction float64) Sampler {
if fraction <= 0 {
fraction = 0
}
traceIDUpperBound := uint64(fraction * (1 << 63))
return func(p SamplingParameters) SamplingDecision {
if p.ParentContext.IsSampled() {
return SamplingDecision{Sample: true}
}
x := binary.BigEndian.Uint64(p.TraceID[0:8]) >> 1
return SamplingDecision{Sample: x < traceIDUpperBound}

return &probabilitySampler{
traceIDUpperBound: uint64(fraction * (1 << 63)),
description: fmt.Sprintf("ProbabilitySampler{%g}", fraction),
}
}

type alwaysOnSampler struct{}

func (as alwaysOnSampler) ShouldSample(p SamplingParameters) SamplingResult {
return SamplingResult{Decision: RecordAndSampled}
}

func (as alwaysOnSampler) Description() string {
return "AlwaysOnSampler"
}

// AlwaysSample returns a Sampler that samples every trace.
// Be careful about using this sampler in a production application with
// significant traffic: a new trace will be started and exported for every
// request.
func AlwaysSample() Sampler {
return func(p SamplingParameters) SamplingDecision {
return SamplingDecision{Sample: true}
}
return alwaysOnSampler{}
}

type alwaysOffSampler struct{}

func (as alwaysOffSampler) ShouldSample(p SamplingParameters) SamplingResult {
return SamplingResult{Decision: NotRecord}
}

func (as alwaysOffSampler) Description() string {
return "AlwaysOffSampler"
}

// NeverSample returns a Sampler that samples no traces.
func NeverSample() Sampler {
return func(p SamplingParameters) SamplingDecision {
return SamplingDecision{Sample: false}
}
return alwaysOffSampler{}
}

// AlwaysParentSample returns a Sampler that samples a trace only
// if the parent span is sampled.
// This Sampler is a passthrough to the ProbabilitySampler with
// a fraction of value 0.
func AlwaysParentSample() Sampler {
return ProbabilitySampler(0)
return &probabilitySampler{
traceIDUpperBound: 0,
description: "AlwaysParentSampler",
}
}
8 changes: 4 additions & 4 deletions sdk/trace/sampling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func TestAlwaysParentSampleWithParentSampled(t *testing.T) {
SpanID: spanID,
TraceFlags: core.TraceFlagsSampled,
}
if !sampler(sdktrace.SamplingParameters{ParentContext: parentCtx}).Sample {
t.Error("Sampling decision should be true")
if sampler.ShouldSample(sdktrace.SamplingParameters{ParentContext: parentCtx}).Decision != sdktrace.RecordAndSampled {
t.Error("Sampling decision should be RecordAndSampled")
}
}

Expand All @@ -43,7 +43,7 @@ func TestAlwaysParentSampleWithParentNotSampled(t *testing.T) {
TraceID: traceID,
SpanID: spanID,
}
if sampler(sdktrace.SamplingParameters{ParentContext: parentCtx}).Sample {
t.Error("Sampling decision should be false")
if sampler.ShouldSample(sdktrace.SamplingParameters{ParentContext: parentCtx}).Decision != sdktrace.NotRecord {
t.Error("Sampling decision should be NotRecord")
}
}
38 changes: 32 additions & 6 deletions sdk/trace/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,17 @@ func (s *span) SetName(name string) {
name: name,
cfg: s.tracer.provider.config.Load().(*Config),
span: s,
attributes: s.data.Attributes,
links: s.data.Links,
kind: s.data.SpanKind,
}
sampled := makeSamplingDecision(data)

// Adding attributes directly rather than using s.SetAttributes()
// as s.mu is already locked and attempting to do so would deadlock.
for _, a := range sampled.Attributes {
s.attributes.add(a)
}
makeSamplingDecision(data)
}

func (s *span) addLink(link apitrace.Link) {
Expand Down Expand Up @@ -308,8 +317,11 @@ func startSpanInternal(tr *tracer, name string, parent core.SpanContext, remoteP
name: name,
cfg: cfg,
span: span,
attributes: o.Attributes,
links: o.Links,
kind: o.SpanKind,
}
makeSamplingDecision(data)
sampled := makeSamplingDecision(data)

// TODO: [rghetia] restore when spanstore is added.
// if !internal.LocalSpanStoreEnabled && !span.spanContext.IsSampled() && !o.Record {
Expand All @@ -332,6 +344,8 @@ func startSpanInternal(tr *tracer, name string, parent core.SpanContext, remoteP
span.messageEvents = newEvictedQueue(cfg.MaxEventsPerSpan)
span.links = newEvictedQueue(cfg.MaxLinksPerSpan)

span.SetAttributes(sampled.Attributes...)

if !noParent {
span.data.ParentSpanID = parent.SpanID
}
Expand All @@ -354,9 +368,12 @@ type samplingData struct {
name string
cfg *Config
span *span
attributes []core.KeyValue
links []apitrace.Link
kind apitrace.SpanKind
}

func makeSamplingDecision(data samplingData) {
func makeSamplingDecision(data samplingData) SamplingResult {
if data.noParent || data.remoteParent {
// If this span is the child of a local span and no
// Sampler is set in the options, keep the parent's
Expand All @@ -369,16 +386,25 @@ func makeSamplingDecision(data samplingData) {
// sampler = o.Sampler
//}
spanContext := &data.span.spanContext
sampled := sampler(SamplingParameters{
sampled := sampler.ShouldSample(SamplingParameters{
ParentContext: data.parent,
TraceID: spanContext.TraceID,
SpanID: spanContext.SpanID,
Name: data.name,
HasRemoteParent: data.remoteParent}).Sample
if sampled {
HasRemoteParent: data.remoteParent,
Kind: data.kind,
Attributes: data.attributes,
Links: data.links,
})
if sampled.Decision == RecordAndSampled {
spanContext.TraceFlags |= core.TraceFlagsSampled
} else {
spanContext.TraceFlags &^= core.TraceFlagsSampled
}
return sampled
}
if data.parent.TraceFlags&core.TraceFlagsSampled != 0 {
return SamplingResult{Decision: RecordAndSampled}
}
return SamplingResult{Decision: NotRecord}
}
35 changes: 25 additions & 10 deletions sdk/trace/trace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,28 @@ func (t *testExporter) ExportSpan(ctx context.Context, d *export.SpanData) {
t.spans = append(t.spans, d)
}

type testSampler struct {
callCount int
prefix string
t *testing.T
}

func (ts *testSampler) ShouldSample(p SamplingParameters) SamplingResult {
ts.callCount++
ts.t.Logf("called sampler for name %q", p.Name)
decision := NotRecord
if strings.HasPrefix(p.Name, ts.prefix) {
decision = RecordAndSampled
}
return SamplingResult{Decision: decision, Attributes: []core.KeyValue{core.Key("callCount").Int(ts.callCount)}}
}

func (ts testSampler) Description() string {
return "testSampler"
}

func TestSetName(t *testing.T) {
samplerIsCalled := false
fooSampler := Sampler(func(p SamplingParameters) SamplingDecision {
samplerIsCalled = true
t.Logf("called sampler for name %q", p.Name)
return SamplingDecision{Sample: strings.HasPrefix(p.Name, "foo")}
})
fooSampler := &testSampler{prefix: "foo", t: t}
tp, _ := NewProvider(WithConfig(Config{DefaultSampler: fooSampler}))

type testCase struct {
Expand Down Expand Up @@ -109,18 +124,18 @@ func TestSetName(t *testing.T) {
},
} {
span := startNamedSpan(tp, "SetName", tt.name)
if !samplerIsCalled {
if fooSampler.callCount == 0 {
t.Errorf("%d: the sampler was not even called during span creation", idx)
}
samplerIsCalled = false
fooSampler.callCount = 0
if gotSampledBefore := span.SpanContext().IsSampled(); tt.sampledBefore != gotSampledBefore {
t.Errorf("%d: invalid sampling decision before rename, expected %v, got %v", idx, tt.sampledBefore, gotSampledBefore)
}
span.SetName(tt.newName)
if !samplerIsCalled {
if fooSampler.callCount == 0 {
t.Errorf("%d: the sampler was not even called during span rename", idx)
}
samplerIsCalled = false
fooSampler.callCount = 0
if gotSampledAfter := span.SpanContext().IsSampled(); tt.sampledAfter != gotSampledAfter {
t.Errorf("%d: invalid sampling decision after rename, expected %v, got %v", idx, tt.sampledAfter, gotSampledAfter)
}
Expand Down