Sampling & fingers-crossed
Gosoline supports log sampling and "fingers-crossed" logging to help you manage log volume without losing critical debug information when errors occur.
What is fingers-crossed logging?
Fingers-crossed logging is a strategy where logs are buffered for a request instead of being written immediately.
- If the request completes successfully, the buffered logs (usually verbose debug/info logs) are discarded.
- If an error occurs (or the request fails), the entire buffer is flushed, preserving the full history of what led to the failure.
This allows you to run with high-verbosity logging (like debug level) in production for sampled traffic or failed requests, while keeping log costs low for successful, non-sampled traffic.
How it works
The logic depends on two conditions:
- Logger Sampling is Enabled: The logger must be configured with sampling enabled.
- Context is Not Sampled: The request context must have a sampling decision of
false.
| Sampling Decision | Logger Behavior |
|---|---|
Sampled: true | Standard: Logs are written immediately. |
Sampled: false | Fingers-crossed: Logs are buffered in the context scope. They flush only on error. |
Enable sampling
Configuration
You can enable sampling in your application configuration. You also need to configure a sampling strategy (decider) so the system knows which requests to sample.
# Sampling decision configuration.
#
# The sampling decider reads `sampling.enabled` and `sampling.strategies`.
# If sampling is enabled, the decider stores the final decision in the context.
#
# Available built-in strategies:
# - "tracing": uses the trace sampling flag from the context
# - "always": always sample
# - "never": never sample
# - "probabilistic": samples a small percentage of requests and guarantees at least one sampled decision per time window
sampling:
enabled: true
strategies:
- tracing
Logger sampling itself is enabled via application option app.WithSampling (see next section).
Application Options
When setting up your application, ensure you enable the sampling option.
This enables sampling on the logger and also propagates the sampling decision across stream messages via the message attribute sampled.
app.Run(
// ...
app.WithSampling,
)
Usage in HTTP Services
The HTTP server provides a SamplingMiddleware that integrates automatically with the sampling decider and the logger.
Decision flow
For each incoming request, gosoline makes a sampling decision early in the request lifecycle.
- It first ensures there is a place to buffer logs (the fingers-crossed scope).
- If the request includes the
X-Goso-Sampledheader, it is used first as an override.- Values are parsed using
strconv.ParseBool(e.g.true/false,1/0). - If the header is missing, the configured strategies decide.
- Values are parsed using
- The decision is attached to the request context so every log call during request handling can use it.
What gets flushed (and when)
- Sampled request: logs are written immediately.
- Not sampled request: logs are buffered and only become visible when the request fails.
- For HTTP, "fails" means the response status code is >= 400, which causes gosoline to flush the buffered logs at the end of the request.
This means you can keep successful, not-sampled requests quiet, but still get full context for failed requests.
Usage in Stream Consumers
Stream consumers use the same idea, but apply it to message processing.
When publishing messages, gosoline can attach the sampling decision to message attributes (attribute sampled). Consumers can then restore the decision from the message and use it for fingers-crossed logging.
Decision flow
For each message:
- Gosoline ensures there is a fingers-crossed scope, so logs can be buffered.
- If the incoming message contains the attribute
sampled, gosoline parses it (viastrconv.ParseBool) and attaches the decision to the context.- This propagated decision takes precedence over any locally configured sampling strategies.
- If no
sampledattribute is present, gosoline falls back to your configured sampling decider (sampling.enabled/sampling.strategies) to make the decision.
What gets flushed (and when)
Because consumers do not have an HTTP response status, the "fail" signal is usually an error log:
- Sampled message: logs are written immediately.
- Not sampled message: logs are buffered and flush when you log an error (or if you flush manually via
log.FlushFingersCrossedScope(ctx)).
Manual Usage
If you are writing a custom worker, CLI tool, or background process, you can use the fingers-crossed features manually.
package main
import (
"context"
"fmt"
"os"
"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/clock"
"github.com/justtrackio/gosoline/pkg/log"
"github.com/justtrackio/gosoline/pkg/smpl/smplctx"
)
func main() {
// 1. Create a logger with sampling enabled
handler := log.NewHandlerIoWriter(cfg.New(), log.PriorityDebug, log.FormatterConsole, "main", "15:04:05.000", os.Stdout)
logger := log.NewLoggerWithInterfaces(clock.NewRealClock(), []log.Handler{handler})
if err := logger.Option(log.WithSamplingEnabled(true)); err != nil {
panic(err)
}
// 2. Prepare a context that is NOT sampled (to trigger buffering)
// In a real app, this decision comes from the sampling middleware or decider.
ctx := context.Background()
ctx = smplctx.WithSampling(ctx, smplctx.Sampling{Sampled: false})
// 3. Add the fingers-crossed scope to the context
// This scope will buffer logs until an error occurs or it is flushed.
ctx = log.WithFingersCrossedScope(ctx)
fmt.Println("--- Phase 1: Logging Info (buffered, should not appear yet) ---")
logger.Info(ctx, "This is an info message (buffered)")
logger.Debug(ctx, "This is a debug message (buffered)")
fmt.Println("--- Phase 2: Logging Error (triggers flush) ---")
// This error log will cause the buffer to flush, printing the previous messages + the error.
logger.Error(ctx, "An error occurred!")
fmt.Println("--- Phase 3: Done ---")
}
Key API Methods
app.WithSampling: Enables the sampling logic on the logger.log.WithFingersCrossedScope(ctx): Wraps the context with a buffer for fingers-crossed logging.log.FlushFingersCrossedScope(ctx): Manually flushes any buffered logs in the current scope.