Skip to main content

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:

  1. Logger Sampling is Enabled: The logger must be configured with sampling enabled.
  2. Context is Not Sampled: The request context must have a sampling decision of false.
Sampling DecisionLogger Behavior
Sampled: trueStandard: Logs are written immediately.
Sampled: falseFingers-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-Sampled header, 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.
  • 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 (via strconv.ParseBool) and attaches the decision to the context.
    • This propagated decision takes precedence over any locally configured sampling strategies.
  • If no sampled attribute 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.

main.go
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.

Troubleshooting

"I don't see any logs"

  • Did you add a scope? If sampling is enabled on the logger, and the context is not sampled, the logger expects a fingers-crossed scope to buffer into. If you didn't call log.WithFingersCrossedScope(ctx), the logs may be dropped because there is nowhere to buffer them.

  • Is the context sampled? If smplctx.IsSampled(ctx) is true (which is the default for empty contexts), logs write immediately. You won't see buffering behavior.

"Why did it flush?"

The buffer flushes when:

  1. You log a message with level Error or higher.
  2. You manually call log.FlushFingersCrossedScope(ctx).
  3. (In HTTP) The response status code is >= 400.