Use sampling
The smpl package helps you make a consistent sampling decision (sampled vs. not sampled) and store it in a context.Context. Other gosoline packages can then use that decision to change behavior (e.g. reduce log volume).
This guide focuses only on the sampling packages. If you want to see how sampling interacts with buffering logs on errors, read: Sampling & fingers-crossed.
Concepts
- A strategy is a small function that can make a sampling decision based on the current context.
- A decider applies strategies in order and stores the final decision in the context.
- The decision lives on the context and can be read from anywhere down the call chain.
Default behavior
If there is no sampling decision on the context, gosoline treats it as sampled.
That means smplctx.IsSampled(ctx) returns true by default.
Configuration
The sampling decider reads its settings from the sampling config key.
sampling:
enabled: true
strategies:
- tracing
sampling.enabled: if set tofalse, the decider behaves as if everything is sampled.sampling.strategies: list of strategy names in priority order. The first strategy that applies wins.
When sampling is enabled and a new decision is made, gosoline emits a metric sampling_decision (count) with dimension sampled=true|false.
Built-in strategies
Gosoline ships with a few strategies you can reference by name:
tracing: if a trace is present in the context, use its sampling flag.always: always sample.never: never sample.probabilistic: guarantees at least one sampled decision per time window and additionally samples a small percentage of extra traffic.
Probabilistic settings
The probabilistic strategy reads its settings from sampling.settings.probabilistic:
sampling:
enabled: true
strategies:
- probabilistic
settings:
probabilistic:
interval: 1s
fixed_sample_count: 1
extra_rate_percentage: 5
interval: time window for the per-interval guarantee.fixed_sample_count: number of guaranteedsampled=truedecisions per interval.extra_rate_percentage: probability (0–100) for sampling additional calls within the same interval.
Make a decision
Typical flow:
- Create (or provide) a decider.
- Call
Decide. - Use the returned context for the rest of your work.
The returned context is important because this is where the decision is stored.
package main
import (
"context"
"fmt"
"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/log"
"github.com/justtrackio/gosoline/pkg/smpl"
"github.com/justtrackio/gosoline/pkg/smpl/smplctx"
)
type ctxKey string
func init() {
// Register a custom sampling strategy that can be referenced in config.
//
// This is process-global and should be done before application startup, before a decider is created.
smpl.AddStrategy("force-by-context", func(ctx context.Context, config cfg.Config) (smpl.Strategy, error) {
return func(ctx context.Context) (applied bool, sampled bool, err error) {
if v, ok := ctx.Value(ctxKey("force_sample")).(bool); ok {
return true, v, nil
}
return false, false, nil
}, nil
})
}
func main() {
ctx := context.Background()
config := cfg.New()
logger := log.NewLogger()
// Pretend this is coming from an incoming request/message.
ctx = context.WithValue(ctx, ctxKey("force_sample"), false)
// Build a decider from config. It reads `sampling.enabled` and `sampling.strategies`.
// In real applications you typically use smpl.ProvideDecider(ctx, config).
decider, err := smpl.NewDecider(ctx, config)
if err != nil {
logger.Error(ctx, "can not create decider: %w", err)
return
}
// Decide applies the configured strategies.
ctx, sampled, err := decider.Decide(ctx)
if err != nil {
logger.Error(ctx, "can not decide: %w", err)
return
}
fmt.Printf("config decision: sampled=%v (smplctx.IsSampled=%v)\n", sampled, smplctx.IsSampled(ctx))
// Per-call overrides: additional strategies run before the configured strategies.
// This allows you to force sampling behaviour for specific code paths.
alwaysStrategy, err := smpl.DecideByAlways(ctx, config)
if err != nil {
logger.Error(ctx, "can not build override strategy: %w", err)
return
}
ctx, sampled, err = decider.Decide(ctx, alwaysStrategy)
if err != nil {
logger.Error(ctx, "can not decide with override: %w", err)
return
}
fmt.Printf("override decision: sampled=%v (smplctx.IsSampled=%v)\n", sampled, smplctx.IsSampled(ctx))
}
Override per call with additional strategies
Decide(ctx, additionalStrategies...) lets you influence the decision for a specific code path.
Additional strategies are applied before the configured ones. This is useful when you need more control in a single place, without changing global config.
For example:
-
Force sampling for a specific operation:
always, _ := smpl.DecideByAlways(ctx, config)
ctx, sampled, err := decider.Decide(ctx, always) -
Force not-sampled behavior:
never, _ := smpl.DecideByNever(ctx, config)
ctx, sampled, err := decider.Decide(ctx, never)
If your additional strategy can not decide, return applied=false so the configured strategies can decide.
A common example is HTTP request sampling: you can derive a strategy from the incoming request and pass it as an additional strategy (so it takes precedence over config). Gosoline's HTTP server middleware does this using the X-Goso-Sampled header.
Register a custom strategy for config
If you want to make a strategy configurable (usable via sampling.strategies), register it with smpl.AddStrategy(name, strategy).
- This is process-global.
- Register strategies during application startup (before building the decider), so config parsing can resolve the strategy name.
Example pattern:
smpl.AddStrategy("my-strategy", func(ctx context.Context, config cfg.Config) (smpl.Strategy, error) {
return func(ctx context.Context) (applied bool, sampled bool, err error) {
// Decide on certain contexts, otherwise abstain.
return false, false, nil
}, nil
})
Troubleshooting
- "It always says sampled": if
sampling.enabledisfalseor no decision is stored,smplctx.IsSampled(ctx)returnstrueby default. - "My strategy does nothing": some strategies only apply if required data exists on the context (e.g.
tracingneeds a trace in the context). - "I called Decide but later code doesn't see it": ensure you propagate the returned context (
ctx = newCtx).