internal/sampler/sampler.go

// Package sampler defines a pluggable interface for deciding whether a
// given log record should be emitted. Implementations live in sibling
// packages: random, adaptive, threshold.
//
// See mercemay.top/src/lambdalog/.
package sampler

import "time"

// Decision is the result of a call to Sampler.Sample.
type Decision int

const (
	// Drop discards the record.
	Drop Decision = iota
	// Emit writes the record normally.
	Emit
	// Tag writes the record but adds a {"sampled": true} field so
	// aggregators can distinguish head-sampled records.
	Tag
)

// Input is the subset of record metadata samplers need. Keeping it separate
// from encoder.Record allows the sampler tree to compile without a
// dependency on the encoder package.
type Input struct {
	Level    string
	Message  string
	Now      time.Time
	QPSHint  float64
	Attempt  int
}

// Sampler is the contract every implementation satisfies.
type Sampler interface {
	Sample(Input) Decision
	Name() string
}

// Chain composes multiple samplers with short-circuit semantics: the first
// one that returns Drop wins. Tag propagates through but does not
// short-circuit; Emit yields to the next sampler.
type Chain []Sampler

// Name returns a deterministic name derived from the chain members.
func (c Chain) Name() string {
	if len(c) == 0 {
		return "chain(empty)"
	}
	name := "chain("
	for i, s := range c {
		if i > 0 {
			name += ","
		}
		name += s.Name()
	}
	return name + ")"
}

// Sample applies each child in order. See Chain for semantics.
func (c Chain) Sample(in Input) Decision {
	result := Emit
	for _, s := range c {
		d := s.Sample(in)
		switch d {
		case Drop:
			return Drop
		case Tag:
			result = Tag
		}
	}
	return result
}

// Always is a trivial sampler that returns a fixed Decision. It is useful
// as a fallback or in tests.
type Always Decision

// Sample returns the underlying Decision.
func (a Always) Sample(Input) Decision { return Decision(a) }

// Name reports "always-emit" / "always-drop" / "always-tag".
func (a Always) Name() string {
	switch Decision(a) {
	case Drop:
		return "always-drop"
	case Tag:
		return "always-tag"
	default:
		return "always-emit"
	}
}