Skip to content

Commit 77bf7df

Browse files
committed
feat: Add trace based sampling for go plugin.
1 parent 5a4ce75 commit 77bf7df

File tree

5 files changed

+755
-24
lines changed

5 files changed

+755
-24
lines changed

go/config.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ import (
44
"context"
55

66
"github.com/launchdarkly/observability-sdk/go/internal/defaults"
7+
"go.opentelemetry.io/otel/trace"
78
)
89

910
type observabilityConfig struct {
10-
serviceName string
11-
serviceVersion string
12-
environment string
13-
backendURL string
14-
otlpEndpoint string
15-
manualStart bool
16-
context context.Context
17-
debug bool
11+
serviceName string
12+
serviceVersion string
13+
environment string
14+
backendURL string
15+
otlpEndpoint string
16+
manualStart bool
17+
context context.Context
18+
debug bool
19+
samplingRateMap map[trace.SpanKind]float64
1820
}
1921

2022
func defaultConfig() observabilityConfig {
@@ -86,3 +88,12 @@ func WithDebug() Option {
8688
c.debug = true
8789
}
8890
}
91+
92+
// WithSamplingRateMap sets the sampling rate for each span kind.
93+
// This setting can influence the quality of metrics used for experiments and guarded
94+
// releases and should only be adjusted with consultation.
95+
func WithSamplingRateMap(rates map[trace.SpanKind]float64) Option {
96+
return Option(func(conf *observabilityConfig) {
97+
conf.samplingRateMap = rates
98+
})
99+
}

go/internal/otel/otel.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ func createTracerProvider(
210210
return nil, fmt.Errorf("creating OTLP trace exporter: %w", err)
211211
}
212212
opts = append([]sdktrace.TracerProviderOption{
213-
sdktrace.WithSampler(sampler),
214213
sdktrace.WithBatcher(newTraceExporter(exporter, customSampler),
215214
sdktrace.WithBatchTimeout(time.Second),
216215
sdktrace.WithExportTimeout(30*time.Second),
@@ -219,6 +218,10 @@ func createTracerProvider(
219218
),
220219
sdktrace.WithResource(resources),
221220
}, opts...)
221+
// Only configure a sampler when there is a sampling configuration.
222+
if sampler != nil {
223+
opts = append(opts, sdktrace.WithSampler(sampler))
224+
}
222225
return sdktrace.NewTracerProvider(opts...), nil
223226
}
224227

go/plugin.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,6 @@ func (p ObservabilityPlugin) Metadata() ldplugins.Metadata {
5656
return ldplugins.NewMetadata("launchdarkly-observability")
5757
}
5858

59-
type allSampler struct{}
60-
61-
// Description provide a description of the sampler.
62-
func (a *allSampler) Description() string {
63-
return "samples all traces"
64-
}
65-
66-
// ShouldSample determines if the trace should be sampled.
67-
func (a *allSampler) ShouldSample(parameters trace.SamplingParameters) trace.SamplingResult {
68-
return trace.SamplingResult{
69-
Decision: trace.RecordAndSample,
70-
}
71-
}
72-
7359
func (p ObservabilityPlugin) getSamplingConfig(projectId string) (*gql.GetSamplingConfigResponse, error) {
7460
var ctx context.Context
7561
if p.config.context != nil {
@@ -101,7 +87,12 @@ func (p ObservabilityPlugin) Register(client interfaces.LDClientInterface, ldmd
10187
logging.SetLogger(logging.ConsoleLogger{})
10288
}
10389

104-
var s trace.Sampler = &allSampler{}
90+
var s trace.Sampler
91+
if len(p.config.samplingRateMap) > 0 {
92+
s = getSampler(p.config.samplingRateMap)
93+
} else {
94+
s = nil
95+
}
10596
otel.SetConfig(otel.Config{
10697
OtlpEndpoint: p.config.otlpEndpoint,
10798
ResourceAttributes: attributes,

go/trace_sampler.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package ldobserve
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
7+
"github.com/samber/lo"
8+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
9+
"go.opentelemetry.io/otel/trace"
10+
)
11+
12+
type traceSampler struct {
13+
traceIDUpperBounds map[trace.SpanKind]uint64
14+
description string
15+
}
16+
17+
func (ts traceSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult {
18+
psc := trace.SpanContextFromContext(p.ParentContext)
19+
if psc.IsSampled() {
20+
return sdktrace.SamplingResult{
21+
Decision: sdktrace.RecordAndSample,
22+
Tracestate: psc.TraceState(),
23+
}
24+
}
25+
bound, ok := ts.traceIDUpperBounds[p.Kind]
26+
if !ok {
27+
bound, ok = ts.traceIDUpperBounds[trace.SpanKindUnspecified]
28+
// If there are no bounds specified, then we sample all
29+
// Avoiding doing work here versus having default bounds which would
30+
// would require additional work per span.
31+
if !ok {
32+
return sdktrace.SamplingResult{
33+
Decision: sdktrace.RecordAndSample,
34+
Tracestate: psc.TraceState(),
35+
}
36+
}
37+
}
38+
39+
x := binary.BigEndian.Uint64(p.TraceID[8:16]) >> 1
40+
if x < bound {
41+
return sdktrace.SamplingResult{
42+
Decision: sdktrace.RecordAndSample,
43+
Tracestate: psc.TraceState(),
44+
}
45+
}
46+
return sdktrace.SamplingResult{
47+
Decision: sdktrace.Drop,
48+
Tracestate: psc.TraceState(),
49+
}
50+
}
51+
52+
func (ts traceSampler) Description() string {
53+
return ts.description
54+
}
55+
56+
// creates a per-span-kind sampler that samples each kind at a provided fraction.
57+
func getSampler(rates map[trace.SpanKind]float64) traceSampler {
58+
return traceSampler{
59+
description: fmt.Sprintf("TraceIDRatioBased{%+v}", rates),
60+
traceIDUpperBounds: lo.MapEntries(rates, func(key trace.SpanKind, value float64) (trace.SpanKind, uint64) {
61+
return key, uint64(value * (1 << 63))
62+
}),
63+
}
64+
}

0 commit comments

Comments
 (0)