
Golang Samber Slog
- 33.1k installs
- 2.8k repo stars
- Updated July 27, 2026
- samber/cc-skills-golang
samber/slog-**** is a collection of composable Go handler packages that extend slog with multi-handler pipelines, sampling, formatting, and backend routing.
About
samber/slog-**** is a collection of 20+ composable slog.Handler packages for Go 1.21+ implementing a canonical logging pipeline. Developers use it to build production logging architectures where records flow through sampling (to reduce noise), formatters (to strip PII), and routers (to send errors to Sentry while info goes to Loki). It matters because sampling before formatting saves CPU, and routing separates concerns without duplicating handlers.
- 20+ composable handler packages for structured logging
- Canonical pipeline: sampling - formatting - routing - sinks
- Integrates with Datadog, Sentry, Loki, and 15+ backends
Golang Samber Slog by the numbers
- 33,144 all-time installs (skills.sh)
- +434 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #24 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
golang-samber-slog capabilities & compatibility
- Works with
- datadog · sentry · kafka · slack
- Use cases
- debugging
What golang-samber-slog says it does
Six composition patterns, each for a different routing need
npx skills add https://github.com/samber/cc-skills-golang --skill golang-samber-slogAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33.1k |
|---|---|
| repo stars | ★ 2.8k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | samber/cc-skills-golang ↗ |
What it does
Building multi-sink logging pipelines in Go with sampling, PII scrubbing, and backend routing (Datadog, Sentry, Loki) for production observability.
Who is it for?
Production logging architectures; multi-sink pipelines; PII scrubbing; throughput control; APM backend integration
Skip if: CLI tools with stdout-only logging or services using zap or zerolog instead of slog ecosystems.
When should I use this skill?
Using Go 1.21+ with slog; building multi-sink logging; need sampling for high-throughput systems; must route different log levels to different backends
What you get
Ordered slog handler chain, masked PII fields, Sentry-routed errors, and sampled log output at configured rates.
- ordered slog handler pipeline
- pii masking rules
- sentry error routing
By the numbers
- pipeline-ordering eval configures 10% log sampling rate
- Combines 3 libraries: slog-multi, slog-sampling, and slog-formatter
Files
Persona: You are a Go logging architect. You design log pipelines where every record flows through the right handlers — sampling drops noise early, formatters strip PII before records leave the process, and routers send errors to Sentry while info goes to Loki.
samber/slog-\\\\ — Structured Logging Pipeline for Go
20+ composable slog.Handler packages for Go 1.21+. Three core pipeline libraries plus HTTP middlewares and backend sinks that all implement the standard slog.Handler interface.
Official resources:
- github.com/samber/slog-multi — handler composition
- github.com/samber/slog-sampling — throughput control
- github.com/samber/slog-formatter — attribute transformation
This skill is not exhaustive. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform. For Go package docs, versions, symbols, and known vulnerabilities, → See samber/cc-skills-golang@golang-pkg-go-dev skill.
The Pipeline Model
Every samber/slog pipeline follows a canonical ordering. Records flow left to right — place sampling first to drop early and avoid wasting CPU on records that never reach a sink.
record → [Sampling] → [Pipe: trace/PII] → [Router] → [Sinks]Order matters: sampling before formatting saves CPU. Formatting before routing ensures all sinks receive clean attributes. Reversing this wastes work on records that get dropped.
Core Libraries
| Library | Purpose | Key constructors |
|---|---|---|
slog-multi | Handler composition | Fanout, Router, FirstMatch, Failover, Pool, Pipe |
slog-sampling | Throughput control | UniformSamplingOption, ThresholdSamplingOption, AbsoluteSamplingOption, CustomSamplingOption |
slog-formatter | Attribute transforms | PIIFormatter, ErrorFormatter, FormatByType[T], FormatByKey, FlattenFormatterMiddleware |
slog-multi — Handler Composition
Six composition patterns, each for a different routing need:
| Pattern | Behavior | Latency impact |
|---|---|---|
Fanout(handlers...) | Broadcast to all handlers sequentially | Sum of all handler latencies |
Router().Add(h, predicate).Handler() | Route to ALL matching handlers | Sum of matching handlers |
Router().Add(...).FirstMatch().Handler() | Route to FIRST match only | Single handler latency |
Failover()(handlers...) | Try sequentially until one succeeds | Primary handler latency (happy path) |
Pool()(handlers...) | Load-balance: sends each record to ONE handler | Single handler latency |
Pipe(middlewares...).Handler(sink) | Middleware chain before sink | Middleware overhead + sink |
// Route errors to Sentry, all logs to stdout
logger := slog.New(
slogmulti.Router().
Add(sentryHandler, slogmulti.LevelIs(slog.LevelError)).
Add(slog.NewJSONHandler(os.Stdout, nil)).
Handler(),
)Built-in predicates: LevelIs, LevelIsNot, MessageIs, MessageIsNot, MessageContains, MessageNotContains, AttrValueIs, AttrKindIs.
For full code examples of every pattern, see Pipeline Patterns.
slog-sampling — Throughput Control
| Strategy | Behavior | Best for |
|---|---|---|
| Uniform | Drop fixed % of all records | Dev/staging noise reduction |
| Threshold | Log first N per interval, then sample at rate R | Production — preserves initial visibility |
| Absolute | Cap at N records per interval globally | Hard cost control |
| Custom | User function returns sample rate per record | Level-aware or time-aware rules |
Sampling MUST be the outermost handler in the pipeline — placing it after formatting wastes CPU on records that get dropped.
// Threshold: log first 10 per 5s, then 10% — errors always pass through via Router
logger := slog.New(
slogmulti.
Pipe(slogsampling.ThresholdSamplingOption{
Tick: 5 * time.Second, Threshold: 10, Rate: 0.1,
}.NewMiddleware()).
Handler(innerHandler),
)Matchers group similar records for deduplication: MatchByLevel(), MatchByMessage(), MatchByLevelAndMessage() (default), MatchBySource(), MatchByAttribute(groups, key).
For strategy comparison and configuration details, see Sampling Strategies.
slog-formatter — Attribute Transformation
Apply as a Pipe middleware so all downstream handlers receive clean attributes.
logger := slog.New(
slogmulti.Pipe(slogformatter.NewFormatterMiddleware(
slogformatter.PIIFormatter("user"), // mask PII fields
slogformatter.ErrorFormatter("error"), // structured error info
slogformatter.IPAddressFormatter("client"), // mask IP addresses
)).Handler(slog.NewJSONHandler(os.Stdout, nil)),
)Key formatters: PIIFormatter, ErrorFormatter, TimeFormatter, UnixTimestampFormatter, IPAddressFormatter, HTTPRequestFormatter, HTTPResponseFormatter. Generic formatters: FormatByType[T], FormatByKey, FormatByKind, FormatByGroup, FormatByGroupKey. Flatten nested attributes with FlattenFormatterMiddleware.
HTTP Middlewares
Consistent pattern across frameworks: router.Use(slogXXX.New(logger)).
Available: slog-gin, slog-echo, slog-fiber, slog-chi, slog-http (net/http).
All share a Config struct with: DefaultLevel, ClientErrorLevel, ServerErrorLevel, WithRequestBody, WithResponseBody, WithUserAgent, WithRequestID, WithTraceID, WithSpanID, Filters.
// Gin with filters — skip health checks
router.Use(sloggin.NewWithConfig(logger, sloggin.Config{
DefaultLevel: slog.LevelInfo,
ClientErrorLevel: slog.LevelWarn,
ServerErrorLevel: slog.LevelError,
WithRequestBody: true,
Filters: []sloggin.Filter{
sloggin.IgnorePath("/health", "/metrics"),
},
}))For framework-specific setup, see HTTP Middlewares.
Backend Sinks
All follow the Option{}.NewXxxHandler() constructor pattern.
| Category | Packages |
|---|---|
| Cloud | slog-datadog, slog-sentry, slog-loki, slog-graylog |
| Messaging | slog-kafka, slog-fluentd, slog-logstash, slog-nats |
| Notification | slog-slack, slog-telegram, slog-webhook |
| Storage | slog-parquet |
| Bridges | slog-zap, slog-zerolog, slog-logrus |
Batch handlers require graceful shutdown — slog-datadog, slog-loki, slog-kafka, and slog-parquet buffer records internally. Flush on shutdown (e.g., handler.Stop(ctx) for Datadog, lokiClient.Stop() for Loki, writer.Close() for Kafka) or buffered logs are lost.
For configuration examples and shutdown patterns, see Backend Handlers.
Common Mistakes
| Mistake | Why it fails | Fix |
|---|---|---|
| Sampling after formatting | Wastes CPU formatting records that get dropped | Place sampling as outermost handler |
| Fanout to many synchronous handlers | Blocks caller — latency is sum of all handlers | Use Pool() for concurrent dispatch |
| Missing shutdown flush on batch handlers | Buffered logs lost on shutdown | defer handler.Stop(ctx) (Datadog), defer lokiClient.Stop() (Loki), defer writer.Close() (Kafka) |
| Router without default/catch-all handler | Unmatched records silently dropped | Add a handler with no predicate as catch-all |
AttrFromContext without HTTP middleware | Context has no request attributes to extract | Install slog-gin/echo/fiber/chi middleware first |
Using Pipe with no middleware | No-op wrapper adding per-record overhead | Remove Pipe() if no middleware needed |
Performance Warnings
- Fanout latency = sum of all handler latencies (sequential). With 5 handlers at 10ms each, every log call costs 50ms. Use
Pool()to reduce to max(latencies) - Pipe middleware adds per-record function call overhead — keep chains short (2-4 middlewares)
- slog-formatter processes attributes sequentially — many formatters compound. For hot-path attribute formatting, prefer implementing
slog.LogValueron your types instead - Benchmark your pipeline with
go test -benchbefore production deployment
Diagnose: measure per-record allocation and latency of your pipeline and identify which handler in the chain allocates most.
Best Practices
1. Sample first, format second, route last — this canonical ordering minimizes wasted work and ensures all sinks see clean data 2. Use Pipe for cross-cutting concerns — trace ID injection and PII scrubbing belong in middleware, not per-handler logic 3. Test pipelines with `slogmulti.NewHandleInlineHandler` — assert on records reaching each stage without real sinks 4. Use `AttrFromContext` to propagate request-scoped attributes from HTTP middleware to all handlers 5. Prefer Router over Fanout when handlers need different record subsets — Router evaluates predicates and skips non-matching handlers
Cross-References
- → See
samber/cc-skills-golang@golang-observabilityskill for slog fundamentals (levels, context, handler setup, migration) - → See
samber/cc-skills-golang@golang-error-handlingskill for the log-or-return rule - → See
samber/cc-skills-golang@golang-securityskill for PII handling in logs - → See
samber/cc-skills-golang@golang-samber-oopsskill for structured error context withsamber/oops
If you encounter a bug or unexpected behavior in any samber/slog-\* package, open an issue at the relevant repository (e.g., slog-multi/issues, slog-sampling/issues).
[
{
"id": 1,
"name": "pipeline-ordering",
"description": "Tests whether the model places sampling first in the pipeline (before formatting)",
"prompt": "Set up a slog pipeline in Go using samber/slog libraries with PII scrubbing (mask email fields), error routing to Sentry, and log sampling at 10%. Show the complete handler composition using slog-multi, slog-sampling, and slog-formatter.",
"trap": "Without the skill, the model places sampling last (after formatting) or in the middle, wasting CPU on records that get dropped",
"assertions": [
{"id": "1.1", "text": "Sampling is the outermost/first handler in the pipeline (applied before formatting)"},
{"id": "1.2", "text": "Uses slog-sampling library (slogsampling) for sampling, not custom code"},
{"id": "1.3", "text": "Uses slog-formatter for PII scrubbing (PIIFormatter or FormatByKey)"},
{"id": "1.4", "text": "Uses slogmulti.Router or Fanout for routing to Sentry"},
{"id": "1.5", "text": "Explains or demonstrates why sampling should be first (avoid wasted CPU on dropped records)"}
]
},
{
"id": 2,
"name": "router-firstmatch-vs-router-all-matches",
"description": "Tests whether the model distinguishes Router (all matching) from Router+FirstMatch (first match only)",
"prompt": "I have a slog-multi Router with two rules: (1) ERROR and above → PagerDuty, (2) WARN and above → Slack. My problem: ERROR logs are going to BOTH PagerDuty AND Slack. I only want errors in PagerDuty, not duplicated in Slack. How do I fix this with slog-multi?",
"trap": "Without the skill, the model adds a LevelIs(slog.LevelWarn) predicate to the Slack handler thinking that fixes it — but WARN and above includes ERROR, so errors still reach Slack. The correct fix is Router().Add(...).FirstMatch() so routing stops at the first matching handler.",
"assertions": [
{"id": "2.1", "text": "Identifies that the default Router sends to ALL matching handlers (both Slack and PagerDuty match for ERROR)"},
{"id": "2.2", "text": "Recommends using .FirstMatch() on the Router to stop at the first matching handler"},
{"id": "2.3", "text": "Does NOT suggest just changing Slack's predicate to LevelIs(slog.LevelWarn) as the complete fix (errors still match WARN-and-above)"},
{"id": "2.4", "text": "Shows the correct Router().Add(pagerduty, LevelIs(ERROR)).Add(slack, LevelIs(WARN)).FirstMatch().Handler() pattern"},
{"id": "2.5", "text": "Explains the semantic difference: Router routes to ALL matches, FirstMatch routes to FIRST match only"}
]
},
{
"id": 3,
"name": "missing-close-batch",
"description": "Tests whether the model remembers to call Close() on batch handlers",
"prompt": "Set up slog to send logs to Datadog using samber/slog-datadog in a Go HTTP server with graceful shutdown. Show the complete main() function.",
"trap": "Without the skill, the model forgets to close the handler on shutdown, causing buffered logs to be lost",
"assertions": [
{"id": "3.1", "text": "Calls handler.Stop(ctx) or defer handler.Stop(ctx) for the Datadog handler (not Close)"},
{"id": "3.2", "text": "Uses Option{}.NewDatadogHandler() constructor pattern"},
{"id": "3.3", "text": "Close happens during graceful shutdown (signal handling or defer in main)"},
{"id": "3.4", "text": "Mentions or demonstrates that Datadog handler batches logs (data loss risk without Close)"},
{"id": "3.5", "text": "Import path is github.com/samber/slog-datadog/v2 or later"}
]
},
{
"id": 4,
"name": "sampling-matcher-grouping",
"description": "Tests whether the model understands slog-sampling Matchers for deduplication grouping",
"prompt": "I've set up ThresholdSamplingOption in my Go service: log the first 10 per 5s, then sample at 10%. It works, but I notice that 'user not found' (DEBUG) and 'cache miss' (DEBUG) are being counted together in the same bucket. After 10 total DEBUG logs from both messages, both get sampled. I want each distinct log message to have its own counter of 10. How do I fix this with samber/slog-sampling?",
"trap": "Without the skill, the model either rebuilds sampling from scratch or suggests separate logger instances per message. The correct fix is to set the Matcher to MatchByLevelAndMessage() (or MatchByMessage()) so each unique message gets its own deduplication bucket instead of all records sharing one bucket.",
"assertions": [
{"id": "4.1", "text": "Identifies that the default Matcher groups all records into one bucket (regardless of message)"},
{"id": "4.2", "text": "Recommends setting the Matcher field on ThresholdSamplingOption to MatchByLevelAndMessage() or MatchByMessage()"},
{"id": "4.3", "text": "Explains that each unique message will then have its own independent threshold counter"},
{"id": "4.4", "text": "Does NOT suggest creating separate logger instances per message type as the solution"},
{"id": "4.5", "text": "Does NOT suggest rewriting custom sampling logic outside of slog-sampling"}
]
},
{
"id": 5,
"name": "router-warn-level-gap",
"description": "Tests whether the model identifies that WARN records are silently dropped in a Router with only ERROR and INFO predicates",
"prompt": "I have this slog-multi Router setup. My ERROR logs reach Sentry, my INFO logs reach Loki, but my WARN logs are silently disappearing — they don't reach either handler. My global slog level is set to Debug so filtering isn't the issue. What's wrong?\n\n```go\nlogger := slog.New(\n slogmulti.Router().\n Add(sentryHandler, slogmulti.LevelIs(slog.LevelError)).\n Add(lokiHandler, slogmulti.LevelIs(slog.LevelInfo)).\n Handler(),\n)\n```",
"trap": "Without the skill, the model suspects Loki configuration, slog level filters, or assumes LevelIs(Info) includes Warn. The skill teaches that Router predicates are exact — LevelIs(Info) matches ONLY Info, not Warn or above. WARN records match no predicate and are silently dropped. Fix: add a catch-all handler or use a predicate that covers Warn.",
"assertions": [
{"id": "5.1", "text": "Correctly identifies that LevelIs(slog.LevelInfo) matches ONLY Info level, not Warn"},
{"id": "5.2", "text": "Explains that Router silently drops records that match no predicate"},
{"id": "5.3", "text": "Does NOT suggest the global slog level or Loki configuration as the root cause"},
{"id": "5.4", "text": "Proposes adding a catch-all handler (no predicate) or a LevelIs(slog.LevelWarn) rule to capture WARN records"},
{"id": "5.5", "text": "Correctly explains that Router predicates are exact-level matches, not 'level and above'"}
]
},
{
"id": 6,
"name": "http-middleware-error-level-differentiation",
"description": "Tests whether the model uses slog-gin Config to log 4xx and 5xx at different levels instead of uniform INFO",
"prompt": "I've added sloggin.New(logger) to my Gin server for request logging. My log aggregation tool shows that 404s and 500s both appear as INFO level, making it hard to alert on server errors. How do I make 4xx client errors log at WARN and 5xx server errors log at ERROR, while keeping normal requests at INFO, using samber/slog-gin?",
"trap": "Without the skill, the model wraps sloggin in a custom middleware or post-processes log records to change the level. The skill teaches that slog-gin's Config struct has ClientErrorLevel and ServerErrorLevel fields that handle this natively — no custom middleware needed.",
"assertions": [
{"id": "6.1", "text": "Replaces sloggin.New() with sloggin.NewWithConfig() to pass a Config struct"},
{"id": "6.2", "text": "Sets ClientErrorLevel: slog.LevelWarn in the Config for 4xx responses"},
{"id": "6.3", "text": "Sets ServerErrorLevel: slog.LevelError in the Config for 5xx responses"},
{"id": "6.4", "text": "Sets DefaultLevel: slog.LevelInfo in the Config for 2xx/3xx responses"},
{"id": "6.5", "text": "Does NOT implement a custom Gin middleware or post-processing logic to change log levels"}
]
},
{
"id": 7,
"name": "pipe-middleware-chain",
"description": "Tests whether the model uses Pipe middleware instead of custom Handler wrapper",
"prompt": "I need to inject a trace_id from OpenTelemetry context and scrub email addresses from all log records before they reach any handler. I'm using samber/slog-multi. Show me how to set this up as reusable middleware.",
"trap": "Without the skill, the model creates a custom slog.Handler wrapper struct instead of using slogmulti.Pipe with inline middleware",
"assertions": [
{"id": "7.1", "text": "Uses slogmulti.Pipe() for middleware chaining"},
{"id": "7.2", "text": "Creates middleware for trace_id injection (inline middleware or AttrFromContext)"},
{"id": "7.3", "text": "Creates middleware or formatter for email/PII scrubbing"},
{"id": "7.4", "text": "Pipe wraps the final sink handler(s)"},
{"id": "7.5", "text": "Does NOT implement a custom slog.Handler struct with Enabled/Handle/WithAttrs/WithGroup methods"}
]
},
{
"id": 8,
"name": "failover-handler",
"description": "Tests whether the model uses Failover instead of custom retry logic",
"prompt": "My slog pipeline sends logs to Loki over the network, but Loki has occasional outages lasting 1-5 minutes. I want to automatically fall back to local file logging when Loki is unavailable, then resume Loki when it's back. Show the setup using samber/slog-multi.",
"trap": "Without the skill, the model builds custom retry/fallback logic with goroutines and channels instead of using slog-multi's Failover handler",
"assertions": [
{"id": "8.1", "text": "Uses slogmulti.Failover() handler"},
{"id": "8.2", "text": "Loki handler is the primary (first) handler in the Failover chain"},
{"id": "8.3", "text": "File/local handler is the fallback (second) handler"},
{"id": "8.4", "text": "Does NOT implement custom retry/fallback logic with goroutines or channels"},
{"id": "8.5", "text": "Uses slog-loki library for the Loki handler (not a custom HTTP client)"}
]
},
{
"id": 9,
"name": "pool-vs-fanout-latency",
"description": "Tests whether the model recommends Pool over Fanout and correctly describes Pool's latency model",
"prompt": "I'm replacing slogmulti.Fanout with slogmulti.Pool to reduce log-call latency. I have 4 handlers with these p99 write times: stdout (1ms), Loki (20ms), Datadog (30ms), Sentry (15ms). My current Fanout p99 latency per log call is ~66ms. What will Pool's p99 latency be, and are there any risks I should know about with Pool that don't exist with Fanout?",
"trap": "Without the skill, the model says Pool will have ~0ms latency (wrongly treating it as fire-and-forget) or cannot identify the risks. The skill teaches: Pool latency = max of all handlers (30ms, not 66ms); risk = Pool may silently swallow handler errors that Fanout would surface synchronously.",
"assertions": [
{"id": "9.1", "text": "Correctly states Pool p99 latency will be ~30ms (the slowest handler, Datadog) not the sum"},
{"id": "9.2", "text": "Explains that Pool dispatches concurrently so latency = max(handler latencies), not sum"},
{"id": "9.3", "text": "Identifies at least one risk of Pool vs Fanout (e.g. handler errors may not surface synchronously, or error handling differs)"},
{"id": "9.4", "text": "Shows correct Pool syntax: slogmulti.Pool()(handlers...) with the double call"},
{"id": "9.5", "text": "Does NOT claim Pool is fully asynchronous/fire-and-forget with zero latency impact"}
]
},
{
"id": 10,
"name": "formatter-pii-scrubbing",
"description": "Tests whether the model uses slog-formatter as Pipe middleware for cross-cutting PII scrubbing",
"prompt": "I need to ensure no email addresses or IP addresses appear in any log output from my Go service. We use slog with slog-multi routing to 3 different handlers (stdout, Loki, Sentry). Show me how to add PII protection that applies to ALL handlers in one place.",
"trap": "Without the skill, the model adds per-handler filtering or regex on output instead of using slog-formatter as a Pipe middleware wrapping all downstream handlers",
"assertions": [
{"id": "10.1", "text": "Uses slog-formatter library (slogformatter package)"},
{"id": "10.2", "text": "Uses PIIFormatter and/or IPAddressFormatter from slog-formatter"},
{"id": "10.3", "text": "Applies formatter as a Pipe middleware wrapping all downstream handlers (not per-handler)"},
{"id": "10.4", "text": "Formatter is applied once in the pipeline, before the Router/Fanout"},
{"id": "10.5", "text": "Does NOT implement custom regex-based filtering or per-handler PII logic"}
]
},
{
"id": 11,
"name": "backend-option-pattern",
"description": "Tests whether the model uses correct Option{} constructor pattern for backend handlers",
"prompt": "Set up slog to send error-level logs to Sentry and all logs to Grafana Loki using samber/slog-sentry and samber/slog-loki. Show the complete setup with correct imports and handler creation.",
"trap": "Without the skill, the model uses incorrect constructor patterns (e.g., New() function, slogsentry.New(), or positional args) instead of the Option{}.NewXxxHandler() pattern",
"assertions": [
{"id": "11.1", "text": "Uses slogsentry.Option{}.NewSentryHandler() constructor pattern"},
{"id": "11.2", "text": "Uses slogloki.Option{}.NewLokiHandler() constructor pattern"},
{"id": "11.3", "text": "Uses slogmulti.Router() for level-based routing (errors to Sentry, all to Loki)"},
{"id": "11.4", "text": "Import paths use versioned modules (github.com/samber/slog-sentry/v2, github.com/samber/slog-loki/v3)"},
{"id": "11.5", "text": "Calls lokiClient.Stop() or defer lokiClient.Stop() for graceful shutdown (flush buffered logs)"}
]
},
{
"id": 12,
"name": "attrfromcontext-without-middleware",
"description": "Tests whether the model identifies that AttrFromContext needs HTTP middleware to populate context",
"prompt": "I'm using slog-multi's Pipe with AttrFromContext to add request_id to all log records in my Go HTTP server. But the request_id is always empty in the logs. I'm NOT using any HTTP logging middleware (no slog-gin or slog-chi). Here's my handler setup:\n\n```go\nhandler := slogsentry.Option{\n Level: slog.LevelError,\n AttrFromContext: []func(ctx context.Context) []slog.Attr{\n func(ctx context.Context) []slog.Attr {\n if reqID := ctx.Value(\"request_id\"); reqID != nil {\n return []slog.Attr{slog.String(\"request_id\", reqID.(string))}\n }\n return nil\n },\n },\n}.NewSentryHandler()\n```\n\nWhat's wrong and how do I fix it?",
"trap": "Without the skill, the model debugs context.Value key types or suggests adding context.WithValue manually in every handler, instead of identifying the missing HTTP middleware that injects attributes",
"assertions": [
{"id": "12.1", "text": "Identifies that no HTTP middleware is populating the request_id into context"},
{"id": "12.2", "text": "Recommends adding slog-gin/echo/fiber/chi middleware (or manual context injection middleware)"},
{"id": "12.3", "text": "Explains that the HTTP middleware is what injects request attributes into context"},
{"id": "12.4", "text": "Does NOT suggest only changing the context key type as the primary fix"},
{"id": "12.5", "text": "Shows the connection between HTTP middleware and AttrFromContext"},
{"id": "12.6", "text": "Mentions WithRequestID config option or equivalent for the middleware"},
{"id": "12.7", "text": "Does NOT blame slog-multi or slog-sentry configuration as the root cause"}
]
}
]
Backend Handlers
All backend handlers implement slog.Handler and follow the Option{}.NewXxxHandler() constructor pattern.
Common Option Fields
Every handler's Option struct includes:
| Field | Purpose |
|---|---|
Level | Minimum log level (default: slog.LevelDebug) |
AddSource | Include source file/line in log output |
ReplaceAttr | Callback to modify attributes before emission |
Converter | Custom payload builder for the target format |
AttrFromContext | Slice of functions extracting attributes from context.Context |
Cloud Backends
Datadog — slog-datadog
import slogdatadog "github.com/samber/slog-datadog/v2"
handler := slogdatadog.Option{
Level: slog.LevelInfo,
// Service, Source, Hostname, Tags configured via Datadog client
}.NewDatadogHandler()
defer handler.(interface{ Stop(context.Context) error }).Stop(context.Background()) // REQUIRED: flush buffered logsBatch mode is the default — logs are buffered and sent periodically (default 5s). Call Stop(ctx) on shutdown or buffered logs are lost. The handler also exposes Flush(ctx) for mid-lifecycle flushes. For synchronous delivery, check the Option configuration.
Sentry — slog-sentry
import slogsentry "github.com/samber/slog-sentry/v2"
handler := slogsentry.Option{
Level: slog.LevelWarn,
Hub: sentry.CurrentHub(),
AddSource: true,
}.NewSentryHandler()
// Flush on shutdown
defer sentry.Flush(2 * time.Second)Recognized attributes: error (any error type), request (\*http.Request), dist, environment, release, server_name, transaction. Use slog.Group("tags", ...) for Sentry tags and slog.Group("user", ...) for user context.
Error keys: Global ErrorKeys = []string{"error", "err"} — attributes with these keys are treated as error objects.
Loki — slog-loki
import slogloki "github.com/samber/slog-loki/v3"
lokiClient, _ := loki.New(lokiCfg)
defer lokiClient.Stop() // REQUIRED: flush buffered logs
handler := slogloki.Option{
Level: slog.LevelDebug,
Client: lokiClient,
}.NewLokiHandler()Labels vs metadata: By default, attributes are sent as Loki labels. For high-cardinality data (request IDs, trace IDs), enable HandleRecordsWithMetadata: true to send as structured metadata instead — this avoids label explosion that degrades Loki performance.
Graylog — slog-graylog
import sloggraylog "github.com/samber/slog-graylog/v2"
gelfWriter, _ := gelf.NewWriter("localhost:12201")
handler := sloggraylog.Option{
Level: slog.LevelDebug,
Writer: gelfWriter,
}.NewGraylogHandler()Uses GELF (Graylog Extended Log Format) over UDP.
Messaging Backends
Kafka — slog-kafka
import slogkafka "github.com/samber/slog-kafka/v2"
writer := &kafka.Writer{
Addr: kafka.TCP("localhost:9092"),
Topic: "logs",
Async: true, // non-blocking writes
}
handler := slogkafka.Option{
Level: slog.LevelDebug,
KafkaWriter: writer,
Timeout: 60 * time.Second,
}.NewKafkaHandler()
defer writer.Close() // REQUIRED: flush pending messagesFluentd — slog-fluentd
import slogfluentd "github.com/samber/slog-fluentd/v2"
client, _ := fluent.New(fluent.Config{
FluentHost: "localhost", FluentPort: 24224,
})
handler := slogfluentd.Option{
Level: slog.LevelDebug,
Client: client,
Tag: "api",
}.NewFluentdHandler()
defer client.Close()Logstash — slog-logstash
import sloglogstash "github.com/samber/slog-logstash/v2"
conn, _ := net.Dial("tcp", "localhost:9999")
handler := sloglogstash.Option{
Level: slog.LevelDebug,
Conn: conn,
}.NewLogstashHandler()
defer conn.Close()Output format: JSON with @timestamp, level, message, error, extra fields.
Notification Backends
Slack — slog-slack
import slogslack "github.com/samber/slog-slack/v2"
// Via webhook
handler := slogslack.Option{
Level: slog.LevelError,
WebhookURL: "https://hooks.slack.com/services/...",
Channel: "alerts",
}.NewSlackHandler()
// Via bot token
handler := slogslack.Option{
Level: slog.LevelError,
BotToken: "xoxb-...",
Channel: "alerts",
}.NewSlackHandler()Telegram — slog-telegram
import slogtelegram "github.com/samber/slog-telegram/v2"
handler := slogtelegram.Option{
Level: slog.LevelError,
Token: "your-bot-token",
Username: "@your-channel",
}.NewTelegramHandler()Webhook — slog-webhook
import slogwebhook "github.com/samber/slog-webhook/v2"
handler := slogwebhook.Option{
Level: slog.LevelError,
Endpoint: "https://webhook.site/your-id",
Timeout: 10 * time.Second,
}.NewWebhookHandler()Storage Backends
Parquet — slog-parquet
import slogparquet "github.com/samber/slog-parquet/v2"
buffer := slogparquet.NewParquetBuffer(bucket, "logs/", 10000, 5*time.Minute)
defer buffer.Flush(true) // REQUIRED: flush remaining records synchronously
handler := slogparquet.Option{
Level: slog.LevelDebug,
Buffer: buffer,
}.NewParquetHandler()Uses Thanos objstore.Bucket for cloud storage (S3, GCS, Azure). Records are buffered and written as Parquet files when either maxRecords or maxInterval is reached.
Logging Bridges
Bridge the slog.Handler interface to legacy logging frameworks. Use during incremental migration from Zap/Zerolog/Logrus to slog.
slog-zap
import slogzap "github.com/samber/slog-zap/v2"
zapLogger, _ := zap.NewProduction()
handler := slogzap.Option{
Level: slog.LevelDebug,
Logger: zapLogger,
}.NewZapHandler()
slog.SetDefault(slog.New(handler))
// Now all slog.Info() calls route through Zapslog-zerolog
import slogzerolog "github.com/samber/slog-zerolog/v2"
zerologLogger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
handler := slogzerolog.Option{
Level: slog.LevelDebug,
Logger: &zerologLogger,
}.NewZerologHandler()slog-logrus
import sloglogrus "github.com/samber/slog-logrus/v2"
handler := sloglogrus.Option{
Level: slog.LevelDebug,
Logger: logrus.StandardLogger(),
}.NewLogrusHandler()Graceful Shutdown Checklist
Handlers that buffer records internally and MUST be closed on shutdown:
| Handler | Shutdown method | What happens without it |
|---|---|---|
slog-datadog | handler.Stop(ctx) | Buffered logs lost (default 5s batch) |
slog-loki | lokiClient.Stop() | Pending push requests dropped |
slog-kafka | writer.Close() | Pending messages never sent |
slog-parquet | buffer.Flush(true) | Partial Parquet file not flushed to storage |
For non-batched handlers (Sentry, Slack, Telegram, Webhook), logs are sent synchronously — no close required, but sentry.Flush(timeout) is recommended.
// Production shutdown pattern
func main() {
lokiClient, _ := loki.New(lokiCfg)
defer lokiClient.Stop() // flush buffered logs
lokiHandler := slogloki.Option{
Level: slog.LevelDebug, Client: lokiClient,
}.NewLokiHandler()
// Use signal handling for graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// ... start server ...
<-ctx.Done()
// deferred Stop() runs here, flushing buffered logs
}HTTP Middlewares
All samber/slog HTTP middlewares share a consistent pattern and configuration structure.
Shared Config Fields
Every middleware provides a Config struct with these common fields:
| Field | Type | Default | Purpose |
|---|---|---|---|
DefaultLevel | slog.Level | slog.LevelInfo | Log level for 2xx/3xx responses |
ClientErrorLevel | slog.Level | slog.LevelWarn | Log level for 4xx responses |
ServerErrorLevel | slog.Level | slog.LevelError | Log level for 5xx responses |
WithUserAgent | bool | false | Include User-Agent header |
WithRequestID | bool | false | Include request ID |
WithRequestBody | bool | false | Include request body (capped) |
WithResponseBody | bool | false | Include response body (capped) |
WithRequestHeader | bool | false | Include request headers |
WithResponseHeader | bool | false | Include response headers |
WithSpanID | bool | false | Include OpenTelemetry span ID |
WithTraceID | bool | false | Include OpenTelemetry trace ID |
WithClientIP | bool | false | Include client IP address |
Filters | []Filter | nil | Request filter functions |
Global configuration variables (set before creating middleware):
RequestBodyMaxSize/ResponseBodyMaxSize— default 64KB eachHiddenRequestHeaders/HiddenResponseHeaders— headers to redactTraceIDKey/SpanIDKey— context key names for OpenTelemetry
Default Log Fields
All middlewares emit these fields by default: method, path, status, latency, request-length, response-length.
Gin — slog-gin
import sloggin "github.com/samber/slog-gin"
// Simple
router := gin.New()
router.Use(sloggin.New(logger))
// With config
router.Use(sloggin.NewWithConfig(logger, sloggin.Config{
DefaultLevel: slog.LevelInfo,
ClientErrorLevel: slog.LevelWarn,
ServerErrorLevel: slog.LevelError,
WithRequestBody: true,
WithUserAgent: true,
Filters: []sloggin.Filter{
sloggin.IgnorePath("/health", "/metrics"),
sloggin.IgnorePathPrefix("/static"),
},
}))
// Custom attributes per request
router.GET("/api/users", func(c *gin.Context) {
sloggin.AddCustomAttributes(c, slog.String("user_id", userID))
c.JSON(200, users)
})Echo — slog-echo
import slogecho "github.com/samber/slog-echo"
e := echo.New()
e.Use(slogecho.New(logger))
// With config
e.Use(slogecho.NewWithConfig(logger, slogecho.Config{
DefaultLevel: slog.LevelInfo,
ClientErrorLevel: slog.LevelWarn,
ServerErrorLevel: slog.LevelError,
WithRequestBody: true,
Filters: []slogecho.Filter{
slogecho.IgnoreStatus(404),
slogecho.IgnorePath("/health"),
},
}))
// Custom attributes
e.GET("/api/users", func(c echo.Context) error {
slogecho.AddCustomAttributes(c, slog.String("user_id", userID))
return c.JSON(200, users)
})Fiber — slog-fiber
import slogfiber "github.com/samber/slog-fiber"
app := fiber.New()
app.Use(slogfiber.New(logger))
// With config
app.Use(slogfiber.NewWithConfig(logger, slogfiber.Config{
DefaultLevel: slog.LevelInfo,
ClientErrorLevel: slog.LevelWarn,
ServerErrorLevel: slog.LevelError,
WithRequestBody: true,
Filters: []slogfiber.Filter{
slogfiber.IgnorePath("/health"),
slogfiber.IgnoreStatus(404),
},
}))
// Custom attributes
app.Get("/api/users", func(c fiber.Ctx) error {
slogfiber.AddCustomAttributes(c, slog.String("user_id", userID))
return c.JSON(users)
})Note: Fiber uses fasthttp, not net/http. Request/response types differ.
Chi — slog-chi
import slogchi "github.com/samber/slog-chi"
router := chi.NewRouter()
router.Use(slogchi.New(logger))
// With config
router.Use(slogchi.NewWithConfig(logger, slogchi.Config{
DefaultLevel: slog.LevelInfo,
ClientErrorLevel: slog.LevelWarn,
ServerErrorLevel: slog.LevelError,
WithRequestBody: true,
Filters: []slogchi.Filter{
slogchi.IgnorePath("/health", "/ready"),
slogchi.IgnoreStatus(401, 404),
},
}))
// Custom attributes
router.Get("/api/users", func(w http.ResponseWriter, r *http.Request) {
slogchi.AddCustomAttributes(r, slog.String("user_id", userID))
json.NewEncoder(w).Encode(users)
})net/http — slog-http
import sloghttp "github.com/samber/slog-http"
mux := http.NewServeMux()
handler := sloghttp.New(logger)(mux)
http.ListenAndServe(":8080", handler)Filters
All middlewares support the same filter functions:
sloggin.IgnorePath("/health", "/metrics") // exact path match
sloggin.IgnorePathPrefix("/static", "/assets") // path prefix
sloggin.IgnoreStatus(401, 404) // skip specific status codes
// Custom filter
sloggin.Accept(func(c *gin.Context) bool {
return c.Request.Method != "OPTIONS" // skip CORS preflight
})Logger Grouping
Wrap the logger with WithGroup("http") to namespace all middleware attributes under an http group:
router.Use(sloggin.New(logger.WithGroup("http")))
// Output: {"http": {"method": "GET", "path": "/api", "status": 200, ...}}Pipeline Patterns
Complete code examples for every slog-multi composition pattern.
Fanout — Broadcast to All
Sends every record to every handler sequentially. Latency = sum of all handler latencies.
import slogmulti "github.com/samber/slog-multi"
logger := slog.New(
slogmulti.Fanout(
slog.NewJSONHandler(os.Stdout, nil), // stdout
slog.NewTextHandler(logFile, nil), // file
slogsentry.Option{Level: slog.LevelError}.NewSentryHandler(), // Sentry
),
)When to use: Every destination must receive every record (audit logs, compliance). When NOT to use: Handlers have different record needs (use Router) or high latency (use Pool).
Router — Predicate-Based Routing
Routes records to ALL handlers whose predicate matches. Unmatched records go nowhere unless a default handler is added.
logger := slog.New(
slogmulti.Router().
Add(sentryHandler, slogmulti.LevelIs(slog.LevelError)).
Add(slackHandler, slogmulti.LevelIs(slog.LevelWarn)).
Add(lokiHandler, slogmulti.LevelIs(slog.LevelInfo, slog.LevelDebug)).
Add(slog.NewJSONHandler(os.Stdout, nil)). // catch-all: no predicate
Handler(),
)Built-in predicates:
slogmulti.LevelIs(slog.LevelError) // match specific levels
slogmulti.LevelIsNot(slog.LevelDebug) // exclude levels
slogmulti.MessageIs("payment processed") // exact message match
slogmulti.MessageIsNot("healthcheck") // exclude exact message
slogmulti.MessageContains("timeout") // partial message match
slogmulti.MessageNotContains("debug") // exclude partial message
slogmulti.AttrValueIs("module", "billing") // match attribute value
slogmulti.AttrKindIs(slog.KindString) // match attribute kindCustom predicate:
func recordMatchRegion(region string) func(ctx context.Context, r slog.Record) bool {
return func(ctx context.Context, r slog.Record) bool {
match := false
r.Attrs(func(attr slog.Attr) bool {
if attr.Key == "region" && attr.Value.String() == region {
match = true
return false
}
return true
})
return match
}
}
logger := slog.New(
slogmulti.Router().
Add(slackUS, recordMatchRegion("us")).
Add(slackEU, recordMatchRegion("eu")).
Handler(),
)Warning: Records matching no predicate are silently dropped. Always add a catch-all handler (no predicate) unless you intentionally want to discard unmatched records.
FirstMatch — Short-Circuit Routing
Like Router but stops at the first matching handler. Each record goes to exactly one destination.
logger := slog.New(
slogmulti.Router().
Add(queryHandler, matchQueryLogs). // priority 1
Add(requestHandler, matchRequestLogs). // priority 2
Add(defaultHandler). // fallback
FirstMatch().
Handler(),
)When to use: Priority-based routing where each record should be processed exactly once. Order matters — put the most specific handlers first.
Failover — Sequential Fallback
Tries handlers in order until one succeeds (returns nil error). If primary fails, falls through to secondary.
logger := slog.New(
slogmulti.Failover()(
slogloki.Option{Level: slog.LevelDebug, Client: lokiClient}.NewLokiHandler(),
slog.NewJSONHandler(localFile, nil), // fallback to local file
slog.NewTextHandler(os.Stderr, nil), // last resort
),
)When to use: Network sinks that may be unreliable (Loki, Logstash, remote syslog). Primary handles 99.9% of traffic; fallback catches the rest.
Pool — Load-Balanced Dispatch
Randomly distributes each record to one handler from the pool. Useful when you have equivalent handlers and want to spread load.
logger := slog.New(
slogmulti.Pool()(
lokiHandler1, // shard 1
lokiHandler2, // shard 2
lokiHandler3, // shard 3
),
)When to use: Multiple equivalent sinks where you want throughput distribution. Latency = single handler latency (not sum like Fanout).
Pipe — Middleware Chain
Chains middleware functions that intercept, transform, or enrich records before they reach the final handler.
logger := slog.New(
slogmulti.
Pipe(samplingMiddleware). // step 1: drop noise
Pipe(piiScrubbingMiddleware). // step 2: mask PII
Pipe(traceInjectionMiddleware). // step 3: add trace_id
Pipe(slogmulti.RecoverHandlerError( // step 4: catch handler panics
func(ctx context.Context, record slog.Record, err error) {
log.Println("handler error:", err)
},
)).
Handler(slog.NewJSONHandler(os.Stdout, nil)),
)Inline Handlers and Middleware
Create quick handlers without defining a full struct.
// Inline handler — for testing or simple consumers
handler := slogmulti.NewHandleInlineHandler(
func(ctx context.Context, groups []string, attrs []slog.Attr, record slog.Record) error {
fmt.Printf("LOG: %s %s\n", record.Level, record.Message)
return nil
},
)
// Inline middleware — intercept and transform records
middleware := slogmulti.NewHandleInlineMiddleware(
func(ctx context.Context, record slog.Record, next func(context.Context, slog.Record) error) error {
record.AddAttrs(slog.String("service", "my-api"))
return next(ctx, record)
},
)AttrFromContext — Request-Scoped Attributes
HTTP middlewares (slog-gin, slog-echo, etc.) inject request attributes into context. Backend handlers extract them via AttrFromContext.
// Backend handler extracts trace_id from context
handler := slogsentry.Option{
Level: slog.LevelError,
AttrFromContext: []func(ctx context.Context) []slog.Attr{
func(ctx context.Context) []slog.Attr {
if traceID := ctx.Value("trace_id"); traceID != nil {
return []slog.Attr{slog.String("trace_id", traceID.(string))}
}
return nil
},
},
}.NewSentryHandler()Important: AttrFromContext only works when the context actually contains the expected values. This requires an HTTP middleware (like slog-gin) to populate the context first. Without the middleware, AttrFromContext silently returns nil.
Full Production Pipeline
Canonical ordering: sampling → middleware (PII, trace) → routing → sinks.
import (
slogmulti "github.com/samber/slog-multi"
slogsampling "github.com/samber/slog-sampling"
slogformatter "github.com/samber/slog-formatter"
slogsentry "github.com/samber/slog-sentry/v2"
slogloki "github.com/samber/slog-loki/v3"
)
// 1. Sampling: first 20 per 5s, then 10%
sampling := slogsampling.ThresholdSamplingOption{
Tick: 5 * time.Second, Threshold: 20, Rate: 0.1,
}.NewMiddleware()
// 2. PII scrubbing
pii := slogformatter.NewFormatterMiddleware(
slogformatter.PIIFormatter("user"),
slogformatter.IPAddressFormatter("client_ip"),
)
// 3. Error recovery
recovery := slogmulti.RecoverHandlerError(func(ctx context.Context, r slog.Record, err error) {
log.Printf("slog handler error: %v", err)
})
// 4. Sinks
sentryHandler := slogsentry.Option{Level: slog.LevelError}.NewSentryHandler()
lokiHandler := slogloki.Option{Level: slog.LevelDebug, Client: lokiClient}.NewLokiHandler()
defer lokiClient.Stop() // flush buffered logs
// 5. Compose — errors bypass sampling, everything else is sampled
logger := slog.New(
slogmulti.
Pipe(pii). // scrub PII on all records
Pipe(recovery). // catch panics
Handler(
slogmulti.Router().
Add(sentryHandler, slogmulti.LevelIs(slog.LevelError)). // errors: no sampling
Add(slogmulti. // everything else: sampled
Pipe(sampling).
Handler(lokiHandler),
).
FirstMatch(). // stop at first matching route — errors won't fall through to sampled path
Handler(),
),
)
slog.SetDefault(logger)Sampling Strategies
Why Sample
High-throughput services generate enormous log volumes. At 10k RPS with 1KB per log entry, you produce 10MB/s — 864GB/day. Sampling reduces cost, network bandwidth, and storage without losing visibility into critical events.
The key insight: sample noise (Debug/Info), never errors. Combine sampling strategies with level-based routing so Warn/Error records always reach every sink.
Strategy Comparison
| Strategy | Constructor | Behavior | Overhead | Best for |
|---|---|---|---|---|
| Uniform | UniformSamplingOption | Drop fixed % of all records randomly | Minimal | Dev/staging noise reduction |
| Threshold | ThresholdSamplingOption | Log first N per interval, then sample at rate R | Low | Production — initial visibility then throttle |
| Absolute | AbsoluteSamplingOption | Cap at N records per interval globally | Medium | Hard cost/throughput cap |
| Custom | CustomSamplingOption | User function returns sample rate per record | Varies | Level-aware, time-aware, context-aware rules |
Uniform Sampling
Simplest strategy. Drops a fixed percentage of all records uniformly.
import slogsampling "github.com/samber/slog-sampling"
option := slogsampling.UniformSamplingOption{
Rate: 0.33, // keep 33% of records
}
logger := slog.New(
slogmulti.Pipe(option.NewMiddleware()).
Handler(slog.NewJSONHandler(os.Stdout, nil)),
)Warning: Uniform sampling drops errors and warnings at the same rate as debug logs. Only use in dev/staging or combine with level-based routing that bypasses sampling for high-severity records.
Threshold Sampling
Logs the first N records with the same "hash" per interval, then switches to rate-based sampling. The hash is determined by the Matcher.
option := slogsampling.ThresholdSamplingOption{
Tick: 5 * time.Second,
Threshold: 10, // first 10 records per hash: always logged
Rate: 0.1, // after threshold: 10% sampling
Matcher: slogsampling.MatchByLevelAndMessage(), // default grouping
}Pattern: "Show me the first 10 occurrences of each message per 5s window. After that, show every 10th." This preserves initial visibility into new issues while limiting noise from repeated messages.
Absolute Sampling
Caps total throughput at a fixed number of records per interval, regardless of how many unique messages exist.
option := slogsampling.AbsoluteSamplingOption{
Tick: 1 * time.Second,
Max: 1000, // cap at 1000 records/sec
Matcher: slogsampling.MatchAll(), // all records share one counter
}Use when: You have a hard budget — e.g., "our log backend can handle 1000 records/sec max" or "we pay per GB ingested."
Custom Sampling
Full control: return a sample rate [0.0, 1.0] per record based on any criteria.
option := slogsampling.CustomSamplingOption{
Sampler: func(ctx context.Context, record slog.Record) float64 {
// Always log errors and warnings
if record.Level >= slog.LevelWarn {
return 1.0
}
// Night hours: log everything (low traffic)
if record.Time.Hour() < 6 || record.Time.Hour() > 22 {
return 1.0
}
// Business hours: heavy sampling for info/debug
return 0.01 // 1%
},
}When to use: Complex rules that depend on time of day, log level, specific attributes, or context values. Higher overhead than other strategies because the function runs per record.
Matchers — Record Grouping
Matchers determine how records are grouped for threshold/absolute counting. Records with the same hash share a counter.
| Matcher | Groups by | Use when |
|---|---|---|
MatchAll() | All records share one counter | Global throughput cap |
MatchByLevel() | Log level | Different rates per level |
MatchByMessage() | Message text | Deduplicate repeated messages |
MatchByLevelAndMessage() | Level + message (default) | Standard deduplication |
MatchBySource() | Source file:line | Group by call site |
MatchByAttribute(groups, key) | Attribute value | Group by module, user, etc. |
MatchByContextValue(key) | Context value | Group by request-scoped value |
Chaining Multiple Strategies
Stack sampling strategies for layered control:
// Layer 1: per-message deduplication (threshold)
threshold := slogsampling.ThresholdSamplingOption{
Tick: 5 * time.Second, Threshold: 100, Rate: 0.1,
Matcher: slogsampling.MatchByLevelAndMessage(),
}.NewMiddleware()
// Layer 2: global throughput cap (absolute)
absolute := slogsampling.AbsoluteSamplingOption{
Tick: 1 * time.Second, Max: 1000,
Matcher: slogsampling.MatchAll(),
}.NewMiddleware()
logger := slog.New(
slogmulti.
Pipe(threshold). // first: per-message dedup
Pipe(absolute). // then: global cap
Handler(handler),
)Pipeline Ordering
Sampling MUST be the first stage in the pipeline. Placing it after formatting or routing wastes CPU on records that get dropped.
// WRONG: format then sample — CPU wasted on dropped records
record → [Formatter] → [Sampling] → [Sink]
// RIGHT: sample then format — only surviving records get processed
record → [Sampling] → [Formatter] → [Sink]To exempt errors from sampling, use a FirstMatch Router so error records match the first route and skip sampling:
logger := slog.New(
slogmulti.Router().
Add(sentryHandler, slogmulti.LevelIs(slog.LevelError)). // errors: no sampling, first match wins
Add(slogmulti. // everything else: sampled
Pipe(samplingMiddleware).
Handler(lokiHandler),
).
FirstMatch(). // stop at first matching route — errors won't fall through to sampled path
Handler(),
)Hook Functions — Observability on Sampling
Track how many records are dropped via OnAccepted and OnDropped hooks:
var (
acceptedCounter = prometheus.NewCounter(...)
droppedCounter = prometheus.NewCounter(...)
)
option := slogsampling.ThresholdSamplingOption{
Tick: 5 * time.Second, Threshold: 10, Rate: 0.1,
OnAccepted: func(ctx context.Context, record slog.Record) {
acceptedCounter.Inc()
},
OnDropped: func(ctx context.Context, record slog.Record) {
droppedCounter.Inc()
},
}This lets you monitor your sampling ratio in Prometheus/Grafana and tune thresholds based on actual traffic patterns.
Related skills
How it compares
Use golang-samber-slog for multi-handler slog stacks; use basic slog skills when a single JSON handler without sampling or PII rules is enough.
FAQ
Why put sampling first in the pipeline?
Sampling drops records early before formatting wastes CPU. If you format then sample, you wasted work formatting records that get dropped. Order matters: sampling - formatting - routing - sinks.
Do I need all the packages or just a few?
Start with slog-multi (composition), slog-sampling (throughput control), and slog-formatter (PII). Add backend sinks (slog-datadog, slog-sentry, slog-loki) only for those destinations.
Is Golang Samber Slog safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.