sampling

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 11, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Overview

Package sampling provides load-adaptive sampling utilities.

Overview

This package prevents the "Observer Effect" where 100% logging/tracing causes performance degradation. The AdaptiveSampler dynamically adjusts sampling rate based on system load (measured by latency).

Components

  • AdaptiveSampler: Interface for load-adaptive sampling
  • DefaultAdaptiveSampler: Production implementation
  • HeadSampler: Samples only first N items
  • RateLimitedSampler: Samples at most N items per second

Example Usage

sampler := sampling.NewAdaptiveSampler(sampling.DefaultSamplingConfig())
defer sampler.Stop()

// In request handler:
if sampler.ShouldSample() {
    trace.Start()
    defer trace.End()
}

// Record latency for adaptive behavior:
sampler.RecordLatency(time.Since(start))

Thread Safety

All types in this package are safe for concurrent use.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AlwaysSampleError

func AlwaysSampleError(sampler AdaptiveSampler, isError bool) bool

AlwaysSampleError is a helper that always samples errors.

Description

Wraps ShouldSample to always return true for errors. Useful when you want to ensure all errors are captured.

Inputs

  • sampler: The sampler to use
  • isError: Whether this is an error case

Outputs

  • bool: True if should sample (always true for errors)

Example

if AlwaysSampleError(sampler, err != nil) {
    recordTrace()
}

Limitations

  • May increase sampling volume if errors are frequent

Assumptions

  • sampler is not nil

func HeadSampler

func HeadSampler(n int) func() bool

HeadSampler creates a sampler that only samples the first N items.

Description

Useful for sampling the start of a batch operation. Thread-safe via atomic counter.

Inputs

  • n: Number of items to sample (must be > 0)

Outputs

  • func(): Function that returns true for first N calls

Example

sampler := HeadSampler(10)
for _, item := range items {
    if sampler() {
        log.Printf("Processing item: %v", item)
    }
}

Limitations

  • Once N items are sampled, never samples again

Assumptions

  • N is positive; if N <= 0, returns a sampler that never samples

func RateLimitedSampler

func RateLimitedSampler(perSecond int) func() bool

RateLimitedSampler creates a sampler with per-second rate limiting.

Description

Samples at most N items per second, regardless of base rate. Returns a function for consistency with HeadSampler API.

Inputs

  • perSecond: Maximum samples per second (must be > 0)

Outputs

  • func() bool: Function that returns true if under rate limit

Example

sampler := RateLimitedSampler(100)  // Max 100 per second
for event := range events {
    if sampler() {
        processEvent(event)
    }
}

Limitations

  • Window resets every second (not sliding window)

Assumptions

  • perSecond is positive; if <= 0, returns a sampler that never samples

Types

type AdaptiveSampler

type AdaptiveSampler interface {
	// ShouldSample returns true if this item should be sampled.
	ShouldSample() bool

	// ShouldSampleContext returns true and records the decision in context.
	ShouldSampleContext(ctx context.Context) (context.Context, bool)

	// RecordLatency records a latency measurement for adaptive adjustment.
	RecordLatency(latency time.Duration)

	// GetSamplingRate returns the current sampling rate (0.0-1.0).
	GetSamplingRate() float64

	// SetBaseSamplingRate sets the base sampling rate.
	SetBaseSamplingRate(rate float64)

	// Stats returns current sampler statistics.
	Stats() SamplerStats

	// ForceEnable temporarily enables 100% sampling (for debugging).
	ForceEnable(duration time.Duration)
}

AdaptiveSampler defines the interface for load-adaptive sampling.

Description

AdaptiveSampler prevents the "Observer Effect" where 100% logging/tracing causes performance degradation. It dynamically adjusts sampling rate based on system load.

Thread Safety

Implementations must be safe for concurrent use.

Example

var sampler AdaptiveSampler = NewAdaptiveSampler(DefaultSamplingConfig())
if sampler.ShouldSample() {
    trace.Start()
    defer trace.End()
}

Limitations

  • Sampling is probabilistic; important events may be missed
  • Rate adjustment has some latency

Assumptions

  • Caller records latencies for adaptive behavior
  • Missing some samples is acceptable

type Config

type Config struct {
	// BaseSamplingRate is the default sampling rate (0.0-1.0).
	// Default: 0.1 (10%)
	BaseSamplingRate float64

	// MinSamplingRate is the minimum rate even under high load.
	// Default: 0.01 (1%)
	MinSamplingRate float64

	// MaxSamplingRate is the maximum rate under low load.
	// Default: 1.0 (100%)
	MaxSamplingRate float64

	// LatencyThreshold triggers throttling when exceeded.
	// Default: 100ms
	LatencyThreshold time.Duration

	// LatencyWindow is the window for averaging latency.
	// Default: 1 minute
	LatencyWindow time.Duration

	// AdjustmentInterval is how often to recalculate rate.
	// Default: 10 seconds
	AdjustmentInterval time.Duration

	// LatencyBufferSize is the number of latency samples to store in the ring buffer.
	// A larger buffer provides more stable averages but uses more memory.
	// Default: 1024
	LatencyBufferSize int
}

Config configures the adaptive sampler.

Description

Defines the parameters for adaptive sampling behavior.

Example

config := Config{
    BaseSamplingRate: 0.1,      // 10% base rate
    MinSamplingRate:  0.01,     // Never go below 1%
    MaxSamplingRate:  1.0,      // Can go up to 100%
    LatencyThreshold: 100 * time.Millisecond,
}

Limitations

  • All rate values must be in range [0.0, 1.0]

Assumptions

  • MinSamplingRate <= BaseSamplingRate <= MaxSamplingRate

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns sensible defaults.

Description

Returns configuration with 10% base sampling rate that adapts based on system latency.

Inputs

  • None

Outputs

  • Config: Default configuration

Example

config := DefaultConfig()
config.BaseSamplingRate = 0.2  // Override to 20%
sampler := NewAdaptiveSampler(config)

Limitations

  • Defaults may not be optimal for all workloads

Assumptions

  • Caller will tune for their specific use case

type DefaultAdaptiveSampler

type DefaultAdaptiveSampler struct {
	// contains filtered or unexported fields
}

DefaultAdaptiveSampler implements AdaptiveSampler.

Description

Dynamically adjusts sampling rate based on system latency to prevent observability overhead from causing performance issues. When latency increases, sampling rate decreases automatically.

Use Cases

  • Trace sampling during high load
  • Log sampling for high-volume paths
  • Metric collection rate adjustment

Thread Safety

DefaultAdaptiveSampler is safe for concurrent use.

Algorithm

The sampler uses a simple proportional adjustment:

  • If avg latency > threshold: rate = rate * (threshold / latency)
  • If avg latency < threshold/2: rate = min(rate * 1.1, max)

Example

sampler := NewAdaptiveSampler(DefaultConfig())
defer sampler.Stop()

// In request handler:
if sampler.ShouldSample() {
    trace.Start()
    defer trace.End()
}

// Record latency after request:
sampler.RecordLatency(time.Since(start))

Limitations

  • Uses simple random sampling (not reservoir)
  • Latency window uses ring buffer (fixed size)

Assumptions

  • Caller calls Stop() on shutdown
  • Latency recordings represent actual system load

func NewAdaptiveSampler

func NewAdaptiveSampler(config Config) *DefaultAdaptiveSampler

NewAdaptiveSampler creates a new adaptive sampler.

Description

Creates a sampler that automatically adjusts rate based on latency. Starts a background goroutine for periodic adjustment. Caller must call Stop() to clean up the background goroutine.

Inputs

  • config: Configuration for sampling behavior

Outputs

  • *DefaultAdaptiveSampler: New sampler

Example

sampler := NewAdaptiveSampler(Config{
    BaseSamplingRate: 0.2,
    LatencyThreshold: 50 * time.Millisecond,
})
defer sampler.Stop()

Limitations

  • Starts a background goroutine that must be stopped

Assumptions

  • Caller will call Stop() on shutdown

func (*DefaultAdaptiveSampler) ForceEnable

func (s *DefaultAdaptiveSampler) ForceEnable(duration time.Duration)

ForceEnable temporarily enables 100% sampling.

Description

Forces 100% sampling for a specified duration. Useful for debugging specific issues. Thread-safe: uses mutex to ensure both forceEnabled and forceUntil are updated atomically.

Inputs

  • duration: How long to force 100% sampling

Outputs

  • None

Example

sampler.ForceEnable(5 * time.Minute)  // 100% sampling for 5 minutes

Limitations

  • May cause performance degradation if duration is long

Assumptions

  • Duration is reasonable for the system load

func (*DefaultAdaptiveSampler) GetSamplingRate

func (s *DefaultAdaptiveSampler) GetSamplingRate() float64

GetSamplingRate returns the current sampling rate.

Description

Returns the current effective sampling rate (0.0-1.0).

Inputs

  • None (receiver only)

Outputs

  • float64: Current sampling rate

Example

rate := sampler.GetSamplingRate()
log.Printf("Current sampling rate: %.1f%%", rate*100)

Limitations

  • Value may change immediately after return

Assumptions

  • Receiver is not nil

func (*DefaultAdaptiveSampler) RecordLatency

func (s *DefaultAdaptiveSampler) RecordLatency(latency time.Duration)

RecordLatency records a latency measurement.

Description

Records latency for use in adaptive rate adjustment. Uses a ring buffer to maintain a rolling window of measurements.

Performance Note

The latencyMu mutex serializes all latency recordings. On systems with very high request rates and many cores, this could become a contention point. If profiling reveals this as a bottleneck, consider using a buffered channel with a single consumer goroutine, or sharded mutexes. For most use cases (< 100k req/s), the current implementation is adequate.

Inputs

  • latency: The measured latency

Outputs

  • None

Example

start := time.Now()
doWork()
sampler.RecordLatency(time.Since(start))

Limitations

  • High-frequency recording may cause mutex contention

Assumptions

  • Latency values are representative of system load

func (*DefaultAdaptiveSampler) SetBaseSamplingRate

func (s *DefaultAdaptiveSampler) SetBaseSamplingRate(rate float64)

SetBaseSamplingRate sets the base sampling rate.

Description

Updates the base rate. The actual rate may still be adjusted based on load. Thread-safe: uses mutex to protect config access.

Note: This does NOT immediately update currentRate. The background adjustLoop is the single source of truth for currentRate and will pick up the new BaseSamplingRate on its next tick. This prevents a race condition where adjustRate could overwrite the user's setting with a value calculated from stale data.

Inputs

  • rate: New base rate (0.0-1.0)

Outputs

  • None

Example

sampler.SetBaseSamplingRate(0.5)  // Set to 50%

Limitations

  • Rate change takes effect on next adjustment cycle

Assumptions

  • Rate is in valid range [0.0, 1.0]

func (*DefaultAdaptiveSampler) ShouldSample

func (s *DefaultAdaptiveSampler) ShouldSample() bool

ShouldSample returns true if this item should be sampled.

Description

Uses the current sampling rate to make a probabilistic decision. Thread-safe and fast (lock-free for the common path).

Inputs

  • None (receiver only)

Outputs

  • bool: True if item should be sampled

Example

if sampler.ShouldSample() {
    // Record trace, log, etc.
}

Limitations

  • Decision is probabilistic; results vary between calls

Assumptions

  • Receiver is not nil

func (*DefaultAdaptiveSampler) ShouldSampleContext

func (s *DefaultAdaptiveSampler) ShouldSampleContext(ctx context.Context) (context.Context, bool)

ShouldSampleContext returns sampling decision and annotated context.

Description

Same as ShouldSample but also stores the decision in context for consistent sampling of related operations. If a decision was already made (stored in context), it returns that decision without double-counting.

Inputs

  • ctx: Request context

Outputs

  • context.Context: Context with sampling decision
  • bool: True if item should be sampled

Example

ctx, sampled := sampler.ShouldSampleContext(ctx)
if sampled {
    // All operations with this ctx will consistently sample
}

Limitations

  • Context must be propagated for consistent sampling

Assumptions

  • ctx is not nil

func (*DefaultAdaptiveSampler) Stats

Stats returns current sampler statistics.

Description

Returns a snapshot of current sampling statistics.

Inputs

  • None (receiver only)

Outputs

  • SamplerStats: Current statistics

Example

stats := sampler.Stats()
log.Printf("Sampled: %d, Dropped: %d, Rate: %.1f%%",
    stats.TotalSampled, stats.TotalDropped, stats.CurrentRate*100)

Limitations

  • Values are point-in-time snapshot

Assumptions

  • Receiver is not nil

func (*DefaultAdaptiveSampler) Stop

func (s *DefaultAdaptiveSampler) Stop()

Stop stops the sampler's background goroutine.

Description

Stops the adjustment loop. Should be called on shutdown. Idempotent: safe to call multiple times without panic.

Inputs

  • None (receiver only)

Outputs

  • None

Example

sampler := NewAdaptiveSampler(config)
defer sampler.Stop()

Limitations

  • Sampler should not be used after Stop()

Assumptions

  • Receiver is not nil

type SamplerStats

type SamplerStats struct {
	// TotalSampled is the count of items that were sampled.
	TotalSampled int64

	// TotalDropped is the count of items that were not sampled.
	TotalDropped int64

	// CurrentRate is the current sampling rate.
	CurrentRate float64

	// AverageLatency is the rolling average latency.
	AverageLatency time.Duration

	// IsThrottled indicates if sampling is reduced due to load.
	IsThrottled bool

	// ThrottleReason explains why sampling is reduced.
	ThrottleReason string

	// ForceEnabled indicates if force-enable is active.
	ForceEnabled bool
}

SamplerStats contains sampling statistics.

Description

Provides visibility into sampling behavior for monitoring.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL