Skip to content

Commit

Permalink
Add sampling config for instrumentation (open-telemetry#982)
Browse files Browse the repository at this point in the history
* Add sampling config for instrumetnation

* changelog

* Apply suggestions from code review

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

* Update sampling.go

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

* Update internal/pkg/instrumentation/probe/sampling/sampling.go

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>

* fix typo in function name

* remove unnecessery function

* code review changes

* Add test for invalid sampler config

* Adjust probe load test

* update samplingRateDenominator to math.MaxUint32

* Update config/sampling.go

Co-authored-by: Mike Dame <mikedame@google.com>

* code review fix

---------

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Co-authored-by: Mike Dame <mikedame@google.com>
  • Loading branch information
3 people committed Sep 18, 2024
1 parent 308ca22 commit a8908cc
Show file tree
Hide file tree
Showing 12 changed files with 665 additions and 20 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http
- Introduce `config.Provider` as an option to set the initial configuration and update it in runtime. ([#1010](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1010))
- Support `go.opentelemetry.io/otel@v1.29.0`. ([#1032](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1032))
- Support `google.golang.org/grpc` `1.66.0`. ([#1046](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1046))
- `Sampler` interface that can be passed to `Instrumentation` via the new `WithSampler` option.
This configuration allows customization of what sampler is used by the `Instrumentation`. ([#982](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/982))
- The `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` environment variables are now supported when the `WithEnv` option is used. ([#982](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/982))
- Support `golang.org/x/net` `v0.29.0`. ([#1051](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1051))
- Support Go `1.22.7`. ([#1051](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1051))
- Support Go `1.23.1`. ([#1051](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1051))

### Changed

- The `WithSampler` option function now accepts the new `Sampler` interface instead of `trace.Sampler`. ([#982](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/982))

### Fixed

- Fix dirty shutdown caused by panic. ([#980](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/980))
Expand Down
15 changes: 11 additions & 4 deletions config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ type InstrumentationConfig struct {
// If set to false, traces are enabled by default for all libraries, unless the library is explicitly disabled.
// default is false - traces are enabled by default.
DefaultTracesDisabled bool

// Sampler is used to determine whether a trace should be sampled and exported.
Sampler Sampler
}

// Provider provides the initial configuration and updates to the instrumentation configuration.
Expand All @@ -52,15 +55,19 @@ type Provider interface {
Shutdown(ctx context.Context) error
}

type noopProvider struct{}
type noopProvider struct {
Sampler Sampler
}

// NewNoopProvider returns a provider that does not provide any updates and provide the default configuration as the initial one.
func NewNoopProvider() Provider {
return &noopProvider{}
func NewNoopProvider(s Sampler) Provider {
return &noopProvider{Sampler: s}
}

func (p *noopProvider) InitialConfig(_ context.Context) InstrumentationConfig {
return InstrumentationConfig{}
return InstrumentationConfig{
Sampler: p.Sampler,
}
}

func (p *noopProvider) Watch() <-chan InstrumentationConfig {
Expand Down
298 changes: 298 additions & 0 deletions config/sampling.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package config

import (
"errors"
"strconv"
"strings"

"go.opentelemetry.io/auto/internal/pkg/instrumentation/probe/sampling"
)

// Sampler decides whether a trace should be sampled and exported.
type Sampler interface {
validate() error
convert() (*sampling.Config, error)
}

// OpenTelemetry spec-defined sampler names and environment variables for configuration.
const (
TracesSamplerKey = "OTEL_TRACES_SAMPLER"
TracesSamplerArgKey = "OTEL_TRACES_SAMPLER_ARG"

SamplerNameAlwaysOn = "always_on"
SamplerNameAlwaysOff = "always_off"
SamplerNameTraceIDRatio = "traceidratio"
SamplerNameParentBasedAlwaysOn = "parentbased_always_on"
SamplerNameParsedBasedAlwaysOff = "parentbased_always_off"
SamplerNameParentBasedTraceIDRatio = "parentbased_traceidratio"
)

// AlwaysOn is 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.
type AlwaysOn struct{}

var _ Sampler = AlwaysOn{}

func (AlwaysOn) validate() error {
return nil
}

func (AlwaysOn) convert() (*sampling.Config, error) {
return &sampling.Config{
Samplers: map[sampling.SamplerID]sampling.SamplerConfig{
sampling.AlwaysOnID: {
SamplerType: sampling.SamplerAlwaysOn,
},
},
ActiveSampler: sampling.AlwaysOnID,
}, nil
}

// AlwaysOff returns a Sampler that samples no traces.
type AlwaysOff struct{}

var _ Sampler = AlwaysOff{}

func (AlwaysOff) validate() error {
return nil
}

func (AlwaysOff) convert() (*sampling.Config, error) {
return &sampling.Config{
Samplers: map[sampling.SamplerID]sampling.SamplerConfig{
sampling.AlwaysOffID: {
SamplerType: sampling.SamplerAlwaysOff,
},
},
ActiveSampler: sampling.AlwaysOffID,
}, nil
}

// TraceIDRatio samples a given fraction of traces. Fraction should be in the closed interval [0, 1].
// To respect the parent trace's SampledFlag, the TraceIDRatio sampler should be used
// as a delegate of a [ParentBased] sampler.
type TraceIDRatio struct {
// Fraction is the fraction of traces to sample. This value needs to be in the interval [0, 1].
Fraction float64
}

var _ Sampler = TraceIDRatio{}

func (t TraceIDRatio) validate() error {
if t.Fraction < 0 || t.Fraction > 1 {
return errors.New("fraction in TraceIDRatio must be in the range [0, 1]")
}

return nil
}

func (t TraceIDRatio) convert() (*sampling.Config, error) {
tidConfig, err := sampling.NewTraceIDRatioConfig(t.Fraction)
if err != nil {
return nil, err
}
return &sampling.Config{
Samplers: map[sampling.SamplerID]sampling.SamplerConfig{
sampling.TraceIDRatioID: {
SamplerType: sampling.SamplerTraceIDRatio,
Config: tidConfig,
},
},
ActiveSampler: sampling.TraceIDRatioID,
}, nil
}

// ParentBased is a [Sampler] which behaves differently,
// based on the parent of the span. If the span has no parent,
// the Root sampler is used to make sampling decision. If the span has
// a parent, depending on whether the parent is remote and whether it
// is sampled, one of the following samplers will apply:
// - RemoteSampled (default: [AlwaysOn])
// - RemoteNotSampled (default: [AlwaysOff])
// - LocalSampled (default: [AlwaysOn])
// - LocalNotSampled (default: [AlwaysOff])
type ParentBased struct {
// Root is the Sampler used when a span is created without a parent.
Root Sampler
// RemoteSampled is the Sampler used when the span parent is remote and sampled.
RemoteSampled Sampler
// RemoteNotSampled is the Sampler used when the span parent is remote and not sampled.
RemoteNotSampled Sampler
// LocalSampled is the Sampler used when the span parent is local and sampled.
LocalSampled Sampler
// LocalNotSampled is the Sampler used when the span parent is local and not sampled.
LocalNotSampled Sampler
}

var _ Sampler = ParentBased{}

func validateParentBasedComponent(s Sampler) error {
if s == nil {
return nil
}
if _, ok := s.(ParentBased); ok {
return errors.New("parent-based sampler cannot wrap parent-based sampler")
}
return s.validate()
}

func (p ParentBased) validate() error {
var err error
return errors.Join(err,
validateParentBasedComponent(p.LocalNotSampled),
validateParentBasedComponent(p.LocalSampled),
validateParentBasedComponent(p.RemoteNotSampled),
validateParentBasedComponent(p.RemoteSampled),
validateParentBasedComponent(p.Root))
}

func (p ParentBased) convert() (*sampling.Config, error) {
pbc := sampling.DefaultParentBasedSampler()
samplers := make(map[sampling.SamplerID]sampling.SamplerConfig)
rootSampler, err := ConvertSamplerToConfig(p.Root)
if err != nil {
return nil, err
}
if rootSampler != nil {
pbc.Root = rootSampler.ActiveSampler
for id, config := range rootSampler.Samplers {
samplers[id] = config
}
}

remoteSampledSampler, err := ConvertSamplerToConfig(p.RemoteSampled)
if err != nil {
return nil, err
}
if remoteSampledSampler != nil {
pbc.RemoteSampled = remoteSampledSampler.ActiveSampler
for id, config := range remoteSampledSampler.Samplers {
samplers[id] = config
}
}

remoteNotSampledSampler, err := ConvertSamplerToConfig(p.RemoteNotSampled)
if err != nil {
return nil, err
}
if remoteNotSampledSampler != nil {
pbc.RemoteNotSampled = remoteNotSampledSampler.ActiveSampler
for id, config := range remoteNotSampledSampler.Samplers {
samplers[id] = config
}
}

localSampledSamplers, err := ConvertSamplerToConfig(p.LocalSampled)
if err != nil {
return nil, err
}
if localSampledSamplers != nil {
pbc.LocalSampled = localSampledSamplers.ActiveSampler
for id, config := range localSampledSamplers.Samplers {
samplers[id] = config
}
}

localNotSampledSampler, err := ConvertSamplerToConfig(p.LocalNotSampled)
if err != nil {
return nil, err
}
if localNotSampledSampler != nil {
pbc.LocalNotSampled = localNotSampledSampler.ActiveSampler
for id, config := range localNotSampledSampler.Samplers {
samplers[id] = config
}
}

samplers[sampling.ParentBasedID] = sampling.SamplerConfig{
SamplerType: sampling.SamplerParentBased,
Config: pbc,
}

return &sampling.Config{
Samplers: samplers,
ActiveSampler: sampling.ParentBasedID,
}, nil
}

// DefaultSampler returns a ParentBased sampler with the following defaults:
// - Root: AlwaysOn
// - RemoteSampled: AlwaysOn
// - RemoteNotSampled: AlwaysOff
// - LocalSampled: AlwaysOn
// - LocalNotSampled: AlwaysOff
func DefaultSampler() Sampler {
return ParentBased{
Root: AlwaysOn{},
RemoteSampled: AlwaysOn{},
RemoteNotSampled: AlwaysOff{},
LocalSampled: AlwaysOn{},
LocalNotSampled: AlwaysOff{},
}
}

// NewSamplerFromEnv creates a Sampler based on the environment variables.
// If the environment variables are not set, it returns a nil Sampler.
func NewSamplerFromEnv(lookupEnv func(string) (string, bool)) (Sampler, error) {
samplerName, ok := lookupEnv(TracesSamplerKey)
if !ok {
return nil, nil
}

defaultSampler := DefaultSampler().(ParentBased)

samplerName = strings.ToLower(strings.TrimSpace(samplerName))
samplerArg, hasSamplerArg := lookupEnv(TracesSamplerArgKey)
samplerArg = strings.TrimSpace(samplerArg)

switch samplerName {
case SamplerNameAlwaysOn:
return AlwaysOn{}, nil
case SamplerNameAlwaysOff:
return AlwaysOff{}, nil
case SamplerNameTraceIDRatio:
if hasSamplerArg {
ratio, err := strconv.ParseFloat(samplerArg, 64)
if err != nil {
return nil, err
}
return TraceIDRatio{Fraction: ratio}, nil
}
return TraceIDRatio{Fraction: 1}, nil
case SamplerNameParentBasedAlwaysOn:
defaultSampler.Root = AlwaysOn{}
return defaultSampler, nil
case SamplerNameParsedBasedAlwaysOff:
defaultSampler.Root = AlwaysOff{}
return defaultSampler, nil
case SamplerNameParentBasedTraceIDRatio:
if !hasSamplerArg {
defaultSampler.Root = TraceIDRatio{Fraction: 1}
return defaultSampler, nil
}
ratio, err := strconv.ParseFloat(samplerArg, 64)
if err != nil {
return nil, err
}
defaultSampler.Root = TraceIDRatio{Fraction: ratio}
return defaultSampler, nil
default:
return nil, errors.New("unknown sampler name")
}
}

// ConvertSamplerToConfig converts a Sampler its internal representation.
func ConvertSamplerToConfig(s Sampler) (*sampling.Config, error) {
if s == nil {
return nil, nil
}
if err := s.validate(); err != nil {
return nil, err
}
return s.convert()
}
Loading

0 comments on commit a8908cc

Please sign in to comment.