Enrichment
Enrichment attaches ambient properties — a shard ID, a service name, a request ID — to every event a logger emits, without repeating them at each call site. Loggers are immutable: enrichment always derives a new child logger and leaves the parent untouched.
ForContext
ForContext(name, value) is Serilog's namesake: a child logger that includes the property on every event.
inv := log.ForContext("ShardId", 3)
inv.Information("rebalancing {Partitions} partitions", 12){"level":"info","ShardId":3,"@mt":"rebalancing {Partitions} partitions","Partitions":12,"message":"rebalancing 12 partitions"}Values are bound with typed zerolog context methods (string, bool, ints, float64, time.Time, time.Duration, error, and Interface fallback).
Named
Named(service) tags a component with a service property — the idiomatic way to identify which subsystem emitted an event:
billing := log.Named("billing")
billing.Information("invoice {InvoiceId} issued for {Amount}", "INV-2071", 129.90){"level":"info","service":"billing","@mt":"invoice {InvoiceId} issued for {Amount}","InvoiceId":"INV-2071","Amount":129.9,"message":"invoice INV-2071 issued for 129.9"}ForContextValues
Attach several properties at once from a map:
job := log.ForContextValues(map[string]any{
"PipelineId": "pl-778",
"Attempt": 2,
})
job.Warning("retrying flaky step {Step}", "integration-tests"){"level":"warn","PipelineId":"pl-778","Attempt":2,"@mt":"retrying flaky step {Step}","Step":"integration-tests","message":"retrying flaky step integration-tests"}TIP
Derived loggers share the parent's sinks — call Close once, on the root logger. Deriving is cheap; do it per request, per job, per connection.
Caller information
WithCaller(true) annotates each event with the logging call site as "caller":"file:line". srog walks the stack and skips its own frames, so the reported caller is correct regardless of wrappers or inlining:
{"level":"error","caller":"C:/dev/app/main.go:22", ...}Stack traces on errors
WithStackTrace(true) captures a call stack whenever an error is logged via Error or Fatal. The stack is stored as a single multi-line string under the stack field — one indexable block in Elasticsearch/OpenSearch — and pretty-printed by console sinks.
log := srog.MustNew(
srog.WithConsole(srog.NoColor()),
srog.WithTimestamp(false),
srog.WithStackTrace(true),
)
func chargeCard(log *srog.Logger) {
log.Error(errors.New("card declined"), "charge failed for invoice {InvoiceId}", "INV-2071")
}Console rendering (captured; directory prefix shortened for readability):
ERR charge failed for invoice INV-2071 card declined
main.chargeCard
.../enrichment/main.go:46
main.main
.../enrichment/main.go:42The same event through a JSON sink carries the trace in stack (real capture, paths shortened):
{"level":"error","caller":".../stackjson/main.go:22","error":"ledger out of balance","stack":"main.settle\n\t.../stackjson/main.go:22\nmain.main\n\t.../stackjson/main.go:18","@mt":"settlement aborted for batch {BatchId}","BatchId":20260713,"message":"settlement aborted for batch 20260713"}srog strips its own leading frames, so the trace begins at the code that logged. Depth is capped at 32 frames, and runtime.main/runtime.goexit noise is trimmed.
Per-logger control and custom stacks
Logger.WithStackTrace(on) toggles capture on a child logger. Its main use is attaching a better stack yourself — for example at panic recovery, where the useful frames only exist at recover() time:
log.WithStackTrace(false). // suppress srog's own capture
ForContext(srog.StackFieldName, string(debug.Stack())). // attach the real stack
Error(err, "panic recovered: {Panic}", err.Error())srog.StackFieldName ("stack") is the exported name of the field the console sink pretty-prints. The srogecho Recover middleware uses exactly this pattern.
Request and correlation IDs
srog.NewID() returns a random 128-bit identifier as a 32-character hex string, suitable as a request or correlation ID:
srog.NewID() -> d7c83aa4634a24a99b6f50bef79dcd48For carrying an enriched logger through context.Context, see Context.