For Go services on log/slog + OpenTelemetry

The slog.Handler that closes the logs↔traces loop.

Drop-in log/slog backend. Auto-injects trace_id and span_id from context.Context. Zero allocations on the hot path. Passes testing/slogtest.TestHandler conformance.

go get go.klarlabs.de/bolt
5-min quickstart →

Nox security grade Test coverage

Pick the tool by the job, not the tagline.

Three jobs developers come to bolt for. Each links to the how-to that gets you there.

I'm on slog

I want OTel correlation without a perf cliff.

Stdlib log/slog has no first-class trace/span injection and the JSON handler allocates per attribute. bolt.NewSlogHandler is a drop-in replacement that auto-attaches trace context and keeps the encoder zero-alloc.

Migrate from slog →

I'm on zerolog

I want a path off it without rewriting every call site.

bolt's chained API is intentionally close to zerolog's. The migration is mechanical: import-path swap plus a constructor change. Hooks gain a richer EventHook surface (zerolog's hook equivalent — but as a v2 alongside the legacy interface).

Migrate from zerolog →

I'm building LLM features

I need GenAI semconv logs without a vendor SDK.

bolt/genai ships Call/ToolCall helpers that emit the OTel GenAI semantic conventions. Output ingests direct into Langfuse, Arize Phoenix, Braintrust, or the OTel Collector's GenAI processor.

See bolt/genai →

Bolt is not trying to win a single-digit-nanosecond benchmark shootout. The encoder is fast — measurably faster than zerolog and zap on the same hardware — but the wedge is the slog-and-OTel combination, not the headline ns/op.

Three lines for trace-correlated logs.

The Logger.Ctx(ctx) path attaches the active span's trace_id and span_id automatically. No middleware, no custom encoder, no per-call extraction.

stdlib slog
import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

// You extract trace context yourself, every call site.
sp := trace.SpanFromContext(ctx).SpanContext()
logger.InfoContext(ctx, "order placed",
    "trace_id", sp.TraceID().String(),
    "span_id", sp.SpanID().String())
bolt
import "go.klarlabs.de/bolt"

logger := bolt.New(bolt.NewJSONHandler(os.Stdout))

// Ctx(ctx) does it once, for everything that follows.


logger.Ctx(ctx).Info().Msg("order placed")

Want the slog API instead? slog.New(bolt.NewSlogHandler(os.Stdout, nil)) — same auto-injection, same encoder, every existing slog call site keeps working. Both modes pass testing/slogtest.TestHandler.

Quick start

As a slog.Handler

package main

import (
    "log/slog"
    "os"

    "go.klarlabs.de/bolt"
)

func main() {
    h := bolt.NewSlogHandler(os.Stdout, nil)
    slog.SetDefault(slog.New(h))
    slog.Info("server starting", "port", 8080)
}

Chained API (hot paths)

package main

import (
    "os"

    "go.klarlabs.de/bolt"
)

func main() {
    log := bolt.New(bolt.NewJSONHandler(os.Stdout))
    log.Info().
        Str("service", "api").
        Int("port", 8080).
        Msg("server starting")
}

Next: tutorial · migration guides · reference · design rationale

Performance receipts.

Numbers vary by hardware. The CI gate (ci.yml) requires 0 allocs/op on BenchmarkZeroAllocation. The benchstat regression workflow fails the PR on statistically significant deltas (Mann–Whitney U-test, α=0.05).

Libraryns/opB/opallocs/op
bolt12700
zerolog17500
zap1901281
logrus2,8471,73723

Linux/amd64, GitHub Actions ubuntu-latest. Reproduce locally with go test -bench=. -benchmem -run=^$. The benchstat artefact for each PR is attached to the performance-regression workflow run.

Supply chain

cosign keyless signatures · SPDX SBOM · SLSA-3 provenance attestation per release. Verification commands in SECURITY.md.

Continuous security

nox scan per PR · scheduled nox-remediate for OSV-driven upgrades · hourly fuzz · nightly mutation testing on hot paths.

Governance

Solo maintainer, MIT, response-time SLAs documented in SECURITY.md. ADOPTERS.md currently empty — PR yourself in if you ship bolt.