pl tails and pretty-prints JSONL logs produced by zap
(the zap.NewProductionConfig JSON encoder, as used by go-faster/sdk).
It also understands OpenTelemetry log records in the
logs data model JSON
form (the Severity/Body/Attributes/Scope shape emitted by go-faster/sdk's
console log exporter). Both formats are detected per line and rendered the same
way, so a stream that mixes them — as oteldb emits — reads uniformly:
Bodybecomes the message, the instrumentationScopename the logger;SeverityText(or the numericSeverityNumber) becomes the level;Attributesbecomekey=valuefields, with thecode.*ones folded into the caller andexception.message/exception.stacktraceshown as the error;- non-zero
TraceID/SpanIDare surfaced astrace_id/span_idfor correlation, while resource attributes and zero ids are omitted as noise.
When go-faster/sdk's zctx runs in otelzap mode (zctx.WithOpenTelemetryZap),
zap lines carry the trace correlation as a reflected ctx object rather than
flat fields. pl flattens that object so span_id/trace_id (and any other
context-scoped members) read as ordinary fields — identical to zctx's default
mode — again dropping all-zero ids.
go install github.com/go-faster/pl/cmd/pl@latestPipe logs in:
my-service 2>&1 | plFollow a file (tail -f style):
pl -f service.logRead a file once and exit:
pl service.logOutput is colorized when stdout is a terminal. Lines in none of the formats below
are passed through untouched, so mixed output is safe. Disable colors with
--no-color or by setting NO_COLOR.
Besides the two JSON formats, pl renders the plain-text logs that surround a
Kubernetes deployment, so kubectl logs output reads like the rest:
- logfmt (logrus, Grafana, Loki,
log15, …) —level/lvl/severity,t/ts/time/timestamp,msg/message,logger,caller,err/errorand the trace id keys map onto the same rendering as zap's; every other pair becomes akey=valuefield, with numbers and booleans kept as such. A line is only treated as logfmt when it carries at least two of level, timestamp and message — plain prose is passed through rather than mangled into fields. Text before the first pair — journalctl'sJul 26 12:28:52 host unit[1063]:prefix, say — is kept verbatim ahead of the entry instead of being scanned into fields. - klog (kube-apiserver, kubelet, and everything else built on
k8s.io/klog) — the header supplies the level, timestamp,thread.idand caller; structured klog's quoted message and trailingkey="value"pairs are parsed like logfmt, soerr=shows up as the error. The year, which klog omits, is taken from the current date.
$ kubectl logs kubelet | pl
01:22:17.141 E Startup probe already exists for container containerName=cilium-envoy pod=kube-system/cilium-envoy-vxgzg thread.id=1386 (prober_manager.go:197)JSON lines from log/slog's handler, which names its fields time and msg,
are rendered like zap's.
Timestamps in these formats are parsed from RFC3339 or from a numeric epoch, whose unit (seconds, milliseconds, microseconds or nanoseconds) is deduced from its magnitude.
Isolate a single trace with --trace-id; it keeps only lines whose trace_id
matches (case-insensitively), whichever format they arrive in — a flat zap
trace_id, zctx's reflected ctx object, or an OTEL TraceID:
pl --trace-id a30d8906e0e519424360816608e11188 service.log-f, --follow follow the file, waiting for new lines (like tail -f)
--no-color disable ANSI colors
--no-time omit timestamps from the output
--level minimum level to display (debug|info|warn|error)
--trace-id show only lines whose trace_id matches (case-insensitive)
--timezone convert timestamps to this timezone (e.g. UTC, Local, America/New_York)
--otel-resource include OpenTelemetry resource attributes (OTEL logs only)
--otel-func include the function name in the caller (OTEL logs only)
--no-stacktrace omit the zap stacktrace block
--no-error-verbose omit the go-faster/errors verbose error block
--no-stacks omit both stack blocks
--otel-resource prints the resource attributes on their own indented lines
below the entry, in a color distinct from the inline fields, so they stay
readable instead of crowding the message:
20:02:07.563 I app ClickHouse disabled (oteldb/app.go:114)
host.name=198f0b5f008f
service.name=oteldb
telemetry.sdk.version=1.44.0
Levels render as a single colored character by default — D, I, W, E,
and C for dpanic/panic/fatal:
03:00:00.099 D verbose detail
03:00:00.200 I metrics Starting attempt=3
03:00:00.299 W disk low note="needs attention"
03:00:00.400 E boom err=x
03:00:00.500 C giving up
When used as a library, override per-level label and color via
Formatter.LevelStyles (levels absent from the map keep their defaults):
f := &pl.Formatter{
Color: true,
LevelStyles: map[zapcore.Level]pl.LevelStyle{
zapcore.WarnLevel: {Label: "WARN", Color: "\033[33m"},
},
}