Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/logsrv"
A slog.Handler implemented on zerolog. Application code calls log/slog; zerolog does the encoding.
logger := logsrv.NewLogger(cfg)
It reuses the configuration model from logutil, so format selection, common attributes, trace-ID injection, hooks and the full syslog level names work identically across both backends.
Native handler, not an adapter
The distinction matters for what it costs. A naive bridge formats the slog record into a string or a map and hands that to zerolog, paying for an intermediate representation on every line.
This writes each attribute directly onto a zerolog Event, so there is no intermediate structure. The measured result is that a flat record costs 539 ns and zero allocations.
| Benchmark | ns/op | B/op | allocs/op |
|---|---|---|---|
NewHandler_FormatNoneNoHook | 6.8 | 0 | 0 |
NewHandler_GroupedEmpty | 425 | 0 | 0 |
NewHandler_Ungrouped | 539 | 0 | 0 |
NewHandler_CommonAttrs | 541 | 0 | 0 |
NewHandler_Grouped | 568 | 0 | 0 |
NewHandler_RecordWithLogValuer | 635 | 144 | 3 |
NewHandler_RecordWithGroup | 808 | 320 | 6 |
Measured with go test -bench on Go 1.27.1, linux/amd64, Intel Core i7-1260P. Run go test -bench=. ./pkg/logsrv/ to reproduce them on your own hardware; the absolute values will differ, the ratios much less so.
Against the standard-library backend in logutil, which logs the same flat record in 893 ns, this is roughly 40% faster. Grouping is where the gap widens: Grouped here costs 568 ns and zero allocations, against 1,114 ns and 8 allocations through the standard library.
Discard mode is 6.8 ns in both, since neither backend does any work for a disabled logger.
Times and durations use standard-library encoding, deliberately
The handler works against zerolog’s own defaults here, on purpose.
zerolog has process-global TimeFieldFormat and DurationFieldUnit settings. They default to whole-second RFC 3339 and milliseconds, which would truncate every timestamp to the second and render a slog.Duration in different units than the other backend does.
So the handler writes times and durations explicitly, matching the standard library: RFC 3339 with nanosecond precision, and a nanosecond count for durations.
Two things follow. Sub-second timestamps survive, which matters for ordering log records within a request. And the same slog.Attr produces the same bytes through either backend, so switching between them is a configuration change rather than a log-format migration that breaks every downstream parser and dashboard.
Field order is insertion order
Fields are written in the order the attributes were added, with record attributes following the common and WithAttrs attributes. They are not sorted.
Insertion order is stable and predictable, which makes golden-file tests over log output workable. It does mean that comparing two records field by field requires parsing rather than string comparison when the call sites differ.
Logging format lineage
The model follows Software Logging Format, written by this library’s author in 2014, which is where the syslog-range severities and the field conventions come from.
Dependencies
Reaches github.com/rs/zerolog, plus logutil for the configuration model.
Links
- Reference documentation
- Source code
- In depth: A Production slog.Handler on Top of zerolog
- The configuration model: logutil
- All nurago packages