Fast, allocation-optimised structured logging for Go with rich terminal output. Battle-tested in TensorFoundry's FoundryOS where it powers all CLI logging.
go get github.com/tensorfoundrylabs/velocity/v2log := velocity.New(velocity.WithDevelopment())
log.Info("server started", velocity.String("addr", ":8080"), velocity.Int("workers", 4))import (
"github.com/tensorfoundrylabs/velocity/v2" // core logging, writers, renderables, themes
"github.com/tensorfoundrylabs/velocity/v2/live" // spinners and progress bars
"github.com/tensorfoundrylabs/velocity/v2/slogbridge" // log/slog bridge
)| Package | Description |
|---|---|
velocity |
Core logger, typed fields, console/JSON/multi/ring-buffer writers, themes, renderables (Box, Table, Tree, Banner, …), secure-field redaction, Hyperlink helper |
velocity/live |
Stateful animated types: ProgressBar, Spinner, MultiProgress |
velocity/slogbridge |
Handler implementing log/slog.Handler (package name: slogbridge) |
- Zero-alloc on the hot path — typed fields (
String,Int,Float64,Bool,Duration,Error) useunsafe.Pointerstorage; 5 pre-built fields log at ~34 ns with 0 allocs - Sub-100 ns logging — ~27 ns with no fields, ~2 ns for disabled levels, ~5 ns through a sampler
- Options-only construction — single
New(opts ...Option)with preset options:WithDevelopment(),WithProduction(),WithContainer(),WithTesting(t),WithNop() - Immutable themes —
NewThemewithThemeOption, semanticStyleSlotenum,Theme.Format(slot, s)for coloured output without raw ANSI, five built-in themes - Renderables in root —
Box,Table,Tree,Banner,KeyValue,SystemInfoall live in the root package;log.Table(...),log.Box(...)etc. are convenience methods - Field-level redaction —
Secure,SecureURL,Redacted,Truncatedconstructors;<secure>...</secure>tag scanning; per-writer trust model viaWriterTrusted() - StatusItem / Group / ContinuationBlock — structured visual primitives for check-lists, count-headed route lists, and multi-line server startup output
- OSC 8 hyperlinks —
Hyperlink(uri, text)with TTY detection, three fallback modes, composes withTheme.Format - Notify channel —
Logger.Notify/NotifyLines/NotifyBoxfor ephemeral operator output that bypasses the structured pipeline - Ring buffer writer —
RingBufferWriterwithSnapshot(n)andSubscribe(ctx, bufSize)for in-process log capture - slog bridge —
slogbridge.NewHandlerimplementslog/slog.Handlerfor incremental adoption - Log sampling —
CountSamplerchecked before pool acquisition; no allocs on the skip path - Nil-safe and testable — every public method handles nil receivers; overridable
FatalHandler;WithTesting(t)preset
AMD Ryzen 9 5950X, Go 1.24, all libraries writing structured output to io.Discard.
Velocity runs JSON-only (console writer disabled via WithLevel(LevelOff)), same as every other library here.
| Library | Info (no fields) | Info (3 fields) | With + Info (per call) | Disabled level |
|---|---|---|---|---|
| velocity | 30 ns / 0 alloc | 63 ns / 1 alloc | 155 ns / 4 alloc | 3.3 ns / 0 alloc |
| zerolog | 66 ns / 0 alloc | 162 ns / 0 alloc | 459 ns / 2 alloc | 7.3 ns / 0 alloc |
| zap | 233 ns / 0 alloc | 475 ns / 1 alloc | 3045 ns / 6 alloc | 6.5 ns / 0 alloc |
| slog | 417 ns / 0 alloc | 992 ns / 4 alloc | 1177 ns / 11 alloc | 6.8 ns / 0 alloc |
| charmbracelet/log | 3.2 ns / 0 alloc | 3.9 ns / 0 alloc | 2815 ns / 5 alloc | 3.2 ns / 0 alloc |
| pterm | 8376 ns / 65 alloc | 16637 ns / 144 alloc | 8213 ns / 65 alloc | 17 ns / 0 alloc |
Velocity leads zerolog by ~2x on Info throughput and zap by ~8x. The disabled-level check (~3 ns) is the fastest among the structured loggers. charmbracelet/log's sub-5 ns per-call numbers come from skipping format work when the output is not a TTY; its With cost (2815 ns) reflects the real allocation overhead. pterm is a display library, not a structured logger — its numbers are expected.
| Operation | v1.1.3 ns/op | v2.0.0 ns/op | delta | B/op | allocs/op |
|---|---|---|---|---|---|
| Info, no fields | 26 | 28 | +8% | 0 | 0 |
| Info, 5 pre-built fields | 38 | 33 | -13% | 0 | 0 |
| Info, 10 pre-built fields | 39 | 35 | -10% | 0 | 0 |
| Info, tree mode | 36 | 33 | -8% | 0 | 0 |
| Level check (disabled) | 2.1 | 2.2 | +5% | 0 | 0 |
| Sampler check | 5.3 | 5.8 | +9% | 0 | 0 |
| Entry pool round-trip | 14 | 14 | 0% | 0 | 0 |
| Int field construction | 1.3 | 1.4 | +8% | 0 | 0 |
| ConsoleWriter, 5 fields | 433 | 483 | +12% | 32 | 3 |
| JSONWriter, 5 fields | 594 | 642 | +8% | 0 | 0 |
| JSONWriter, parallel | 170 | 192 | +13% | 0 | 0 |
| WithComponent child | 270 | 159 | -41% | 192 | 3 |
| Secure scan, no match | 67 | 35 | -48% | 0 | 0 |
| slog handler, 3 attrs | 468 | 99 | -79% | 144 | 3 |
Notes on v2 changes: Info (no fields) and the writer paths carry a small overhead from the added scanSecure flag check and immutable theme lookup (vs mutable cached fields). The multi-field paths are faster due to the unified any-field path elimination. WithComponent improved significantly from child-logger construction changes. SecureScan_NoMatch halved due to early-exit on the IndexByte fast path. The slog bridge numbers dropped from ~468 ns to ~99 ns because the v1 benchmark was writing to stdout rather than discarding — that was a measurement bug, not a real v1 advantage.
Run benchmarks: go test -bench=. -benchmem -count=3 ./...
log := velocity.New(velocity.WithDevelopment()) // coloured console, debug level
log := velocity.New(velocity.WithProduction()) // JSON to stderr, info level
log := velocity.New(velocity.WithContainer()) // JSON to stdout, info level
log := velocity.New(velocity.WithTesting(t)) // writes via t.Log, cleaned up on test exit
log := velocity.New(velocity.WithNop()) // discards all outputlog.Info("request handled",
velocity.String("method", "GET"),
velocity.Int("status", 200),
velocity.Float64("duration_ms", 12.4),
velocity.Bool("cached", true),
velocity.Duration("elapsed", 42*time.Millisecond),
velocity.Error("err", err),
)reqLog := log.With(velocity.String("request_id", "req-abc123"))
reqLog.Info("handling request")
compLog := log.WithComponent("scheduler")
compLog.Debug("job queued", velocity.Int("job_id", 7))// Plaintext on TTY console, [REDACTED] in JSON and non-TTY output.
log.Info("user authenticated", velocity.Secure("token", "tok_abc123"))
// <secure> tag scanning works in message strings too.
log.Info("connecting to <secure>redis://admin:hunter2@cache.internal</secure>")// Built-in themes.
log := velocity.New(velocity.WithTheme(velocity.ThemeNightOwl))
// Custom theme with semantic slots.
theme := velocity.NewTheme("Custom",
velocity.WithLevelColours(debug, info, warn, err, fatal),
velocity.WithStyleSlot(velocity.SlotGood, velocity.RGB(0x00, 0xFF, 0xAA)),
)
styled := theme.Format(velocity.SlotGood, "all systems go")Colour model. Colour is automatically enabled when stdout is a real terminal (via term.IsTerminal). Two environment variables override detection:
| Variable | Effect |
|---|---|
NO_COLOR=1 |
Always disable ANSI, regardless of terminal type |
FORCE_COLOR=1 |
Always enable ANSI, regardless of terminal type |
NO_COLOR takes precedence over FORCE_COLOR. FORCE_COLOR=1 is useful on Windows where terminal emulators such as VS Code, Windows Terminal, and Git Bash proxy stdout through a named pipe, which causes term.IsTerminal to return false even in a fully colour-capable terminal.
// Convenience methods route through the console writer mutex.
log.Table([]string{"Service", "Status"}, [][]string{{"api", "running"}})
log.Box("Deploy Complete", "3/4 nodes healthy")
// Standalone construction for embedding or capture.
t := velocity.NewTable(headers, rows, velocity.ThemeNightOwl)
fmt.Print(t.String())// StatusItem: themed badge with level-aware routing.
log.Status(velocity.LevelInfo, velocity.StatusOK, "postgres connected",
velocity.Duration("latency", 4*time.Millisecond))
// Group: count-headed indented list.
log.Group(velocity.LevelInfo, "Registered routes",
velocity.GroupItem{Text: "GET /api/users"},
velocity.GroupItem{Text: "POST /api/orders"},
)
// ContinuationBlock: multi-line output anchored to one structured entry.
log.Continue(velocity.LevelInfo, "Server listening",
"API: "+velocity.Hyperlink("http://localhost:8080", "http://localhost:8080"),
"Metrics: "+velocity.Hyperlink("http://localhost:9090/metrics", "http://localhost:9090/metrics"),
)import "github.com/tensorfoundrylabs/velocity/v2/slogbridge"
vlog := velocity.New(velocity.WithDevelopment())
slog.SetDefault(slogbridge.NewLogger(vlog))
slog.Info("request handled", "method", "GET", "status", 200)rotator := &lumberjack.Logger{Filename: "/var/log/app.log", MaxSize: 500, Compress: true}
log := velocity.New(
velocity.WithConsoleOutput(os.Stdout),
velocity.WithStructuredOutput(rotator),
)One: golang.org/x/term for TTY detection. No other external dependencies.
