internal/sampler/random/random.go

// Package random implements a fixed-rate head sampler. It emits each
// record with probability Rate and drops the rest.
//
// See mercemay.top/src/lambdalog/internal/sampler/random/.
package random

import (
	"math/rand"
	"sync"
	"time"

	"mercemay.top/src/lambdalog/internal/sampler"
)

// Sampler is a fixed-rate head sampler. The zero value samples at rate 1.0.
type Sampler struct {
	// Rate is the probability of emission in [0.0, 1.0]. Values outside the
	// range are clamped on Sample.
	Rate float64
	// Tag, if true, returns sampler.Tag instead of sampler.Emit so kept
	// records carry a sampled marker.
	Tag bool

	mu  sync.Mutex
	rng *rand.Rand
}

// Name returns a stable identifier for diagnostics.
func (s *Sampler) Name() string { return "random" }

// Sample applies the head sampling decision.
func (s *Sampler) Sample(in sampler.Input) sampler.Decision {
	rate := s.Rate
	if rate <= 0 {
		return sampler.Drop
	}
	if rate >= 1 {
		if s.Tag {
			return sampler.Tag
		}
		return sampler.Emit
	}
	s.mu.Lock()
	if s.rng == nil {
		s.rng = rand.New(rand.NewSource(seedFrom(in.Now)))
	}
	v := s.rng.Float64()
	s.mu.Unlock()
	if v > rate {
		return sampler.Drop
	}
	if s.Tag {
		return sampler.Tag
	}
	return sampler.Emit
}

// SeedFixed sets the RNG to a deterministic seed. Intended for tests.
func (s *Sampler) SeedFixed(seed int64) {
	s.mu.Lock()
	s.rng = rand.New(rand.NewSource(seed))
	s.mu.Unlock()
}

func seedFrom(t time.Time) int64 {
	if t.IsZero() {
		return time.Now().UnixNano()
	}
	return t.UnixNano()
}

// NewPercent returns a Sampler with Rate equal to pct/100. Out-of-range
// inputs are clamped.
func NewPercent(pct int) *Sampler {
	switch {
	case pct <= 0:
		return &Sampler{Rate: 0}
	case pct >= 100:
		return &Sampler{Rate: 1}
	default:
		return &Sampler{Rate: float64(pct) / 100.0}
	}
}