Velocity v2.2.0
Logging hardening: reliable shutdown, secure redaction, stable live output, and honest benchmarks.
Four independently reviewed fix-and-verification rounds went into this release. Every fix ships with a permanent regression test; the full finding-to-test mapping and measurement artifacts live in docs/specs/ and docs/benchmarks/ in the development tree.
Behaviour changes
- Shared family lifetime. Once any
Closecompletes, the whole parent/child family is closed for good: later log calls are dropped,AddWritercan no longer revive output, and concurrentClosecalls all wait for the same drain and return the same recorded result. Close drains calls that were already admitted - console and JSON writes register as in-flight for the whole formatting cycle, and the render helpers (Render,RenderRaw,Newline,BannerLines,KeyValues,Bullet) register before running - so an admitted call completes rather than writing after Close returned. Admitted family output (includingNotify) finishes before the final destination flush, so notifications shared with console logging on one buffered sink are always emitted. - Reliable fatal delivery.
Logger.Fatalis exempt from the sampler in every dispatch path and waits for preceding accepted entries, its own write, and a flush before invokingFatalHandler/exiting. Delivery is acknowledged per queue item (a FIFO barrier sentinel closed by the worker that dequeues it), so the wait cannot be satisfied early by racing aggregate counters. A custom handler that returns leaves the logger reusable.LogEntryand slog records atLevelFatalare logged but never exit the process. - Content-driven
<secure>tag handling. The maybe-secure flag derives from message content whenever scanning is enabled, independent of the writer mix, so adding a writer after a scan can no longer leak plaintext. On all-trusted topologies markers are stripped (plaintext shown); untrusted writers still see redaction. Group item text and continuation lines follow the same policy as headers on console and JSON. - Colour permission fixed at construction.
WithColour(false)survives every theme swap; a mono-to-coloured swap restores colour only where permission allows.FORCE_COLORcan style non-terminals but never grants trust or cursor control.Logger.Statusnow respects the resolved colour permission on terminals, with styling and trust propagated separately - a trusted terminal with styling disabled still showsSecurefield plaintext, without ANSI. - Honest close errors.
MultiWriter.Close(andLogger.Closethrough it) returns worker close errors joined witherrors.Join. - Deprecated
ConsoleWriterRB: the ring-full direct-write fallback is removed - a full queue drops the record, counts it inDroppedCount, andWritereturnsnil; a secondClosewaits for the same drain and returnsnil. Scheduled for removal in v3; useConsoleWriter. - Stable live displays. Widgets finalise exactly once (concurrent
Stop/Completeall wait for the one finalisation, no output after), cursor-control capability depends on the real destination rather thanFORCE_COLOR, and multi-row displays no longer walk down the terminal on each repaint: repeated repaints, grow/shrink, widget removal and interleaved log lines hold a stable vertical position. - Terminal cell widths. Table, box, banner, component-column and truncation widths are measured in terminal cells (uniseg grapheme widths) with an allocation-free printable ASCII fast path. Absent table cells are padded to the declared geometry; negative
Bulletnesting is clamped.
New APIs
live.NewOutput(io.Writer) *live.Output- opt-in shared terminal coordinator. Pass the same*OutputtoWithConsoleOutputand the widget constructors so log records and live displays serialise on one destination.StyledRenderable- optional extension toRenderablefor types that need resolved styling and trust propagated separately at render time (RenderStyled(w, styled, trusted)).Logger.Render/RenderRawdispatch to it first;StatusItemimplements it. LegacyRenderableandTTYRenderableimplementations are unaffected.velocity.Uint64(key string, val uint64) Field- lossless unsigned integer field on every output path, stored as bits without a float64 round trip.
Internal
ringbuffer.gorewritten as a mutex-guarded bounded byte queue with owned byte storage and a single drainer goroutine; the speculative CAS/skip reclamation protocol is gone.- The unused per-Logger buffer pool was removed;
WithBufferSizeandWithFieldPoolSizeremain deprecated compatibility options. PutFieldSliceclears the pooled slice through its capacity, so a later, smaller use cannot observe stale field pointers.- New direct dependency:
github.com/rivo/unisegv0.4.7 (terminal cell widths).golang.org/x/termremains.
Performance
Benchmarks now measure real serialisation (the previous fixtures routed enabled paths to io.Discard, which Velocity maps to a no-output fast path - the old numbers measured almost nothing). Corrected, interleaved measurements with a stable control benchmark are published in the README alongside delivery/drop ratios for the async figures. Disabled-level logging remains 0 B/op, 0 allocs/op, asserted by test.