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/velocitylog := velocity.New(os.Stdout)
log.Info("server started", velocity.String("addr", ":8080"), velocity.Int("workers", 4))Or use a preset:
log := velocity.NewDevelopment() // coloured console, debug level
log := velocity.NewWithBuilder(velocity.PresetProduction()) // structured JSON, info levelimport (
"github.com/tensorfoundrylabs/velocity" // core logging, writers, config, themes
"github.com/tensorfoundrylabs/velocity/pretty" // boxes, panels, banners, tables, trees, progress
velocityslog "github.com/tensorfoundrylabs/velocity/slog" // log/slog bridge
)| Package | Description |
|---|---|
velocity |
Core logger with typed fields, console/JSON/multi/ring-buffer writers, themes, templates |
velocity/pretty |
Rich CLI display: Box, Panel, Banner, Table, Tree, ProgressBar, Spinner |
velocity/slog |
Handler implementing log/slog.Handler (package name: velocityslog) |
- Zero-alloc on the hot path — typed fields (
String,Int,Float64,Bool,Duration,Error) useunsafe.Pointerstorage with nointerface{}boxing; 5 and 10 pre-built fields log at 34-39 ns with 0 allocs - Sub-100 ns logging — 27 ns with no fields, 2.1 ns for disabled levels, 5.5 ns through a sampler
- slog bridge —
velocityslog.NewHandlerimplementslog/slog.Handlerfor incremental adoption - Rich terminal output — boxes, panels, banners, tables, trees, progress bars and spinners in
velocity/pretty - 4 colour themes — Night Owl (RGB), Solarized, Dracula, Nord; ANSI codes pre-cached at init
- Log sampling —
CountSamplerchecked before pool acquisition; no allocs on the skip path - 5 presets — Development, Production, Container, Testing, HighPerformance
- Nil-safe and testable — every public method handles nil receivers; overridable
FatalHandler;NewForTesting() - Dynamic writers — add/remove writers at runtime;
Render/RenderRaw/Newlineserialised under the console writer mutex
Here's how Velocity stacks up against popular Go logging libraries (AMD Ryzen 9 5950X, Go 1.24, writing to io.Discard):
| Library | Info (no fields) | Info (3 fields) | With + Info | Disabled level |
|---|---|---|---|---|
| velocity | 31 ns / 0 alloc | 67 ns / 1 alloc | 186 ns / 4 alloc | 41 ns / 0 alloc |
| zerolog | 89 ns / 0 alloc | 204 ns / 0 alloc | 422 ns / 2 alloc | 10 ns / 0 alloc |
| zap | 240 ns / 0 alloc | 525 ns / 1 alloc | 1319 ns / 6 alloc | 9 ns / 0 alloc |
| slog | 663 ns / 0 alloc | 1666 ns / 4 alloc | 1684 ns / 11 alloc | 10 ns / 0 alloc |
| charmbracelet/log | 4 ns / 0 alloc | 6 ns / 0 alloc | 2618 ns / 5 alloc | 4 ns / 0 alloc |
| pterm | 12926 ns / 65 alloc | 25334 ns / 144 alloc | 13125 ns / 65 alloc | 19 ns / 0 alloc |
Velocity is ~3x faster than zerolog and ~8x faster than zap on the hot logging path. charmbracelet/log's near-zero numbers are from short-circuiting format work when writing to non-TTY output; its With cost (2618 ns) shows the real overhead. pterm is a display library first, and its allocation profile reflects that.
| Scenario | velocity | zerolog | zap | slog |
|---|---|---|---|---|
| Accumulated context (10 fields) | 45 ns / 0 alloc | 99 ns / 0 alloc | 344 ns / 0 alloc | 672 ns / 0 alloc |
| Mixed field types (8 types) | 153 ns / 4 alloc | 799 ns / 2 alloc | 1307 ns / 1 alloc | 2481 ns / 8 alloc |
| Error field | 96 ns / 1 alloc | 136 ns / 0 alloc | 510 ns / 1 alloc | 912 ns / 1 alloc |
| Large message (1 KB) | 43 ns / 0 alloc | 419 ns / 0 alloc | 1509 ns / 0 alloc | 2255 ns / 1 alloc |
| 10 inline fields | 117 ns / 3 alloc | 383 ns / 0 alloc | 1159 ns / 1 alloc | 3170 ns / 10 alloc |
| Parallel (16 goroutines) | 53 ns / 1 alloc | 22 ns / 0 alloc | 150 ns / 1 alloc | 279 ns / 0 alloc |
zerolog wins the parallel benchmark thanks to its lock-free event chaining design. Velocity wins everything else.
| Operation | ns/op | B/op | allocs/op |
|---|---|---|---|
| Info, no fields | 27 | 0 | 0 |
| Info, 5 pre-built fields | 34 | 0 | 0 |
| Info, 10 pre-built fields | 39 | 0 | 0 |
| Info, tree mode (v1.1) | 36 | 0 | 0 |
| Level check (disabled) | 2.1 | 0 | 0 |
| Sampler check | 5.5 | 0 | 0 |
| Entry pool round-trip | 14 | 0 | 0 |
| Int field construction | 1.3 | 0 | 0 |
| ConsoleWriter, 5 fields | 431 | 32 | 3 |
| JSONWriter, 5 fields | 582 | 0 | 0 |
| JSONWriter, parallel | 170 | 0 | 0 |
| Render / RenderRaw (v1.1) | 1.8 | 0 | 0 |
| slog handler, 3 attrs | 445 | 192 | 6 |
v1.1 highlights: JSON writer dropped from 949 ns/1 alloc to 582 ns/0 alloc (inline hex escape); tree-mode field rendering is now zero-alloc (cached indent string); Render/RenderRaw/Newline are essentially free at ~2 ns.
Run internal benchmarks: go test -bench=. -benchmem -count=3 ./...
The comparative benchmark suite lives in benchmarks/ as a separate Go module.
| Preset | Output | Level | Use Case |
|---|---|---|---|
PresetDevelopment |
Coloured console | Debug | Local dev |
PresetProduction |
JSON | Info | Structured log aggregation |
PresetContainer |
JSON to stdout | Info | Docker/K8s |
PresetTesting |
Provided writer | Debug | Test harnesses |
PresetHighPerformance |
JSON to stderr | Info | High-volume with sampling |
import velocityslog "github.com/tensorfoundrylabs/velocity/slog"
logger := velocity.NewDevelopment()
slog.SetDefault(velocityslog.NewLogger(logger))
slog.Info("request handled", "method", "GET", "status", 200, "duration", 42*time.Millisecond)Groups produce dotted keys: slog.WithGroup("server").With("host", "localhost") renders as server.host.
import "github.com/tensorfoundrylabs/velocity/pretty"
p := pretty.New(os.Stdout, velocity.ThemeNightOwl)
p.Box("Deploy Complete", "All services running")
p.Banner("v2.1.0 - Production release")When a logger exists, prefer NewFromLogger — output routes through the logger's console writer and aligns with the message column:
log := velocity.NewDevelopment()
p := pretty.NewFromLogger(log)
log.Info("deploying services")
log.Newline()
log.Render(p.NewTable([]string{"Service", "Status"}, [][]string{
{"api", "running"},
{"worker", "running"},
}))rotator := &lumberjack.Logger{Filename: "/var/log/app.log", MaxSize: 500, Compress: true}
cfg := velocity.DefaultProductionConfig()
cfg.StructuredOutput = rotator
log := velocity.NewWithConfig(cfg)One: golang.org/x/term for TTY detection. No other external dependencies.
