diff --git a/.gitignore b/.gitignore index 3b38253..8481ac2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ coverage.out *.test *.out +*.exe # IDE .idea/ diff --git a/.golangci.yml b/.golangci.yml index 3943363..9854336 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -131,6 +131,10 @@ linters: - noinlineerr settings: + exhaustive: + # A default case that handles remaining types is semantically exhaustive. + default-signifies-exhaustive: true + gocyclo: min-complexity: 30 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b66c521 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,122 @@ +# Changelog + +## Unreleased + +### New features + +- `FORCE_COLOR=` environment variable forces ANSI colour output regardless of whether stdout is a real terminal. Useful on Windows where terminal emulators proxy stdout through a named pipe, causing `term.IsTerminal` to return false even in a colour-capable terminal. +- `NO_COLOR=` environment variable unconditionally disables ANSI colour output, following the https://no-color.org convention. Takes precedence over `FORCE_COLOR`. +- `TTYRenderable` interface — optional extension to `Renderable` for types that need the terminal state at render time. `Logger.Render` and `Logger.RenderRaw` detect this interface and pass the console writer's resolved TTY state so colour decisions are correct even when rendering through an intermediate buffer. + +### Bug fixes + +- Console writer now correctly emits colour when no theme is explicitly configured; previously, the default-theme path silently disabled colour. +- `StatusItem`, `Group`, and `ContinuationBlock` now implement `TTYRenderable` and expose a `RenderTTY(w, isTTY)` method. Previously, when rendered via `Logger.Render`, `IsTerminalWriter` on the intermediate buffer always returned false, producing plain (uncoloured) badge/item output even on real terminals. +- `template.useColours` is now gated on actual TTY state at writer construction, not just on whether the theme has colours. Previously, ANSI sequences were always emitted when the theme was non-mono, including when stdout was a pipe or file. +- `ConsoleWriter.SetTheme` now updates `template.useColours` to reflect the new theme and current TTY state; previously it left `useColours=false` from initial construction when the writer was built on a non-TTY. +- `ConsoleWriterRB` now uses TTY detection (`resolveColourForWriter`) to set its trust state, matching `ConsoleWriter`'s model. Previously, the template path always rendered Secure fields as plaintext regardless of whether the output was a terminal or a file/pipe. +- `StatusItem.writeStatusFields` now applies the same TTY-as-trust model as `ConsoleWriter`: Secure fields show plaintext on terminal output and are redacted in plain (non-TTY) renders. Previously, Secure fields were always redacted in Status badge output even on trusted terminals. +- `SetTheme(nil)` now documents and enforces "nil = reset to NightOwl" semantics; `Style()` and `cfg.ConsoleTheme` both reflect the reset. Previously, the nil behaviour was not regression-tested. + +## v2.0.0 — 2026-05-15 + +Tag when ready: `git tag v2.0.0 feature/v2` + +### Breaking + +- Module path changed to `github.com/tensorfoundrylabs/velocity/v2` — update all imports accordingly +- `NewWithBuilder`, `NewWithOptions`, `NewWithConfig`, `NewDevelopment`, `NewForTesting` removed — use `New(opts ...Option)` with preset options +- `NopLogger()` retained for compatibility — equivalent to `New(WithNop())` +- `Builder` type removed — configure via options only +- `Config` struct unexported — no direct field access +- `Default*Config` family removed +- `Fields` struct, `NewFields`, `F`, `Milliseconds` removed — use typed field constructors directly +- `*Detailed` methods removed (`DebugDetailed`, `InfoDetailed`, etc.) — use `Logger.Detailed()` for a child logger with tree display +- `Logger.Raw` removed +- `Logger.Banner` renamed to `Logger.BannerLines` (avoids collision with `Banner` renderable type) +- `Logger.SetTemplate` / `Logger.WithTemplate` removed +- `Logger.Status() *StatusFormatter` removed — use `Logger.Style() *Theme` +- `StatusFormatter` type removed +- `AtomicLevel` exported type removed — level is now an internal `atomic.Int32` +- `velocity/pretty` package removed — all renderables moved to root package +- `velocity/slog` package removed — replaced by `velocity/slogbridge` (`package slogbridge`) +- `BoxResult`, `TableResult`, `TreeResult`, `BannerResult`, `KeyValueResult`, `SystemInfoResult` removed — types renamed to `Box`, `Table`, `Tree`, `Banner`, `KeyValue`, `SystemInfo` +- `BulletResult` removed — `Bullet` was only ever a `Logger` method, not a standalone type +- `NewFromLogger` constructor pattern removed from pretty — use `Logger.Box(...)`, `Logger.Table(...)`, etc. directly +- Theme `Cache()` and `EnsureCached()` removed — themes are immutable post-construction +- Colour options consolidated to `WithColour(bool)` +- `WithDisplayTimezone` now takes `*time.Location` directly; helper `MustLocation(name string)` added +- `Logger.AddWriter` now accepts `...WriterOption` for trust and capability configuration + +### New features + +- `New(opts ...Option)` and `TryNew(opts ...Option)` — single constructor entry point +- Preset options: `WithDevelopment()`, `WithProduction()`, `WithContainer()`, `WithTesting(t)`, `WithNop()`, `WithHighThroughput()` +- `Logger.WithComponent(name string) *Logger` — named child logger +- `Logger.WithRequest(id string) *Logger` — request-scoped child logger +- `Logger.Detailed() *Logger` — child logger with forced tree display +- `Logger.Style() *Theme` — theme accessor +- `Logger.Close()` — idempotent, flushes all owned writers; after-close calls are silent no-ops +- `ParseLevel(string) (Level, error)` — non-panicking sibling to `MustParseLevel` +- `NewTheme(name, ...ThemeOption) *Theme` — immutable theme builder +- `StyleSlot` enum with 16 semantic slots: `SlotGood`, `SlotBad`, `SlotWarn`, `SlotMuted`, `SlotStrong`, `SlotHeading`, `SlotEndpoint`, `SlotHyperlink`, `SlotContinuation`, `SlotCount`, `SlotSecure`, `SlotStatusOK`, `SlotStatusFail`, `SlotStatusWarn`, `SlotStatusInfo`, `SlotTableHeader` +- `Theme.Format(slot, s)`, `Theme.Wrap(slot)`, `Theme.Stylish(w)` — theme styling API +- `ThemeMono` — new colour-free built-in theme +- Writer capability interfaces: `ThemedWriter`, `LeveledWriter`, `FlushableWriter`, `TrustedWriter` +- `WriterTrusted()` writer option — marks a writer as trusted for receiving un-redacted secure fields +- `FilteredWriter` — wraps any writer with level filtering +- `Logger.Writer(name) Writer` — accessor for named writers +- `Logger.RemoveWriter(name) Writer` — returns removed writer for caller cleanup +- `RingBufferWriter` — in-process log capture with `Snapshot`, `Subscribe`, `Stats` +- `EntrySnapshot` — deep-copy value type; redacted unless writer is trusted +- `RingStats` — capacity, fill, drop counts +- `Logger.Notify(format, args...)`, `Logger.NotifyLines(lines...)`, `Logger.NotifyBox(*Box)` — ephemeral operator output that bypasses structured pipeline +- `WithNotifyOutput(io.Writer)` — override notify target (default `os.Stderr`) +- Secure field constructors: `Secure(k, v)`, `SecureURL(k, u)`, `Redacted(k)`, `Truncated(k, v, maxLen)` +- `...` tag scanning in message strings — redacted in JSON and non-TTY console output +- `WithSecureTags(bool)` option — explicit opt-out of tag scanning +- `scanSecure` per-instance atomic flag — recomputed on writer add/remove; zero cost when all writers are trusted +- `StatusItem` renderable and `Logger.Status(level, kind, msg, fields...)` log-call form + - Console path: renders an inline indented badge (no timestamp, no level label) via `Logger.Render` + - JSON path: emits a full structured record with `"status"` field for log queries + - `NewStatusItem` no longer takes `isTTY bool` — TTY is resolved at `Render(w)` time +- `StatusKind` enum: `StatusOK`, `StatusFail`, `StatusWarn`, `StatusInfo`, `StatusPending`, `StatusSkipped` +- `Group` renderable and `Logger.Group(level, msg, items...)` — count-headed indented block + - `NewGroup` no longer takes `isTTY bool` — TTY resolved at `Render(w)` time +- `GroupItem{Marker, Text}` — individual group entry +- `ContinuationBlock` renderable and `Logger.Continue(level, msg, lines...)` — `│`-glyph continuation lines + - `NewContinuationBlock` no longer takes `isTTY bool` — TTY resolved at `Render(w)` time +- `Hyperlink(uri, text, ...opts) string` — OSC 8 hyperlink with TTY detection and three fallback modes +- `HyperlinkFallbackNone`, `HyperlinkFallbackParens`, `HyperlinkFallbackBrackets` — fallback modes +- `HyperlinksSupported()` — cached TTY detection +- `WithHyperlinkFallback(mode)` — per-call fallback override +- `Pretty` facade moved to root; `NewPretty(w, theme)` and `NewPrettyFromLogger(logger)` constructors +- Logger convenience methods: `Logger.Box`, `Logger.Table`, `Logger.Tree`, `Logger.BannerLines`, `Logger.KeyValues`, `Logger.SystemInfo` — render directly through console writer mutex +- `velocity/live` package — stateful animated types (`Spinner`, `ProgressBar`, `MultiProgress`) extracted from old `velocity/pretty`; all types suppress control sequences (\r, ANSI erase) when writer is not a terminal +- `velocity/slogbridge` — slog bridge package renamed, with corrected benchmark (was writing to stdout in v1) +- 18 examples covering all major features (up from 11 in v1) + +### Performance + +- `Info, 5 fields`: 38 ns → 33 ns (-13%); zero allocs preserved +- `Info, 10 fields`: 39 ns → 35 ns (-10%); zero allocs preserved +- `Info, tree mode`: 36 ns → 33 ns (-8%); zero allocs preserved +- `WithComponent child`: 270 ns → 159 ns (-41%); 3 allocs preserved +- `SecureScan_NoMatch`: 67 ns → 35 ns (-48%); zero allocs preserved; `IndexByte` fast-exit before any field inspection +- `slog handler, 3 attrs`: benchmark corrected (v1 was writing to stdout); real v2 cost is ~99 ns / 3 allocs / 144 B +- `Info, no fields`: 26 ns → 28 ns (+8%); scanSecure flag check adds ~2 ns on the no-field path +- `ConsoleWriter, 5 fields`: 433 ns → 483 ns (+12%); immutable theme lookup and writer capability checks added +- `JSONWriter, 5 fields`: 594 ns → 642 ns (+8%); `` tag scan path added; zero allocs preserved +- `BufferPool_GetPut`: 25 ns → 17 ns (-32%); tiered pool restructure +- Allocation profile unchanged on all zero-alloc paths + +### Internal + +- Package `velocity/pretty` eliminated; import cycle resolved by moving all renderables to root +- Package `velocity/slog` renamed `velocity/slogbridge` (`package slogbridge`) +- Package `velocity/live` created for stateful animated types +- `AtomicLevel` is now an internal `atomic.Int32`; API surface uses `Level` type throughout +- Theme construction is eager — no `sync.Once`, no `Cache()` call needed by callers +- `scanSecure atomic.Bool` recomputed on writer topology changes, not per log call +- All built-in themes ported to `NewTheme` immutable form +- `slogbridge` benchmark fixed to use `WithNop()` instead of writing to stdout diff --git a/CLAUDE.md b/CLAUDE.md index 0b971ae..325c065 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,161 +1,95 @@ # Velocity -Standalone Go logging library with zero-allocation fields and rich terminal output. Extracted from FoundryOS. +Standalone Go logging library. Zero-allocation hot path, rich terminal output, hand-rolled JSON. Module path: `github.com/tensorfoundrylabs/velocity/v2`. ## Commands ```bash -make ready # Pre-commit gate: tidy, fmt, align, lint, vet, test-race -make test # Run all tests -make test-race # Tests with race detector -make test-cover # Tests with coverage report -make lint # golangci-lint (strict, all linters) -make fmt # goimports + gofumpt -make install-tools # Install golangci-lint, betteralign, goimports, gofumpt -make help # Show all targets +make ready # Pre-commit gate: tidy, fmt, align, lint, vet, test-race +make perf-gate # Alloc-regression gate vs docs/bench-baseline.txt (slow; pre-tag) +make test # Run all tests +make test-race # Tests with race detector +make test-cover # Tests with coverage report +make lint # golangci-lint (strict, all linters) +make fmt # goimports + gofumpt +make bench # Quick bench (count=3) with allocs +make bench-baseline # Capture count=10 run to docs/bench-baseline.txt +make install-tools # golangci-lint, betteralign, goimports, gofumpt, benchstat +make help # All targets ``` -Benchmarks: `go test -bench=. -benchmem -count=3 ./...` +## Packages -## Package Structure +Three: root `velocity`, `velocity/live`, `velocity/slogbridge`. `live` has no root imports; `slogbridge` imports root. -Three packages: root `velocity`, `velocity/pretty`, and `velocity/slog`. - -### Root (`package velocity`) +### Root | File | Purpose | |------|---------| -| `logger.go` | Core `Logger` type, log methods, `logInternal` | -| `entry.go` | Pooled `Entry` with atomic ref counting | -| `field.go` | Zero-alloc typed fields via `unsafe.Pointer`, typed nil guards via `reflect` | -| `field_convert.go` | Field value extraction and string conversion | -| `config.go` | `Config` struct, `Builder`, presets, default config values, TTY detection | -| `options.go` | Functional options (`WithLevel`, `WithTheme`, etc.) | -| `level.go` | Log levels, `AtomicLevel`, `MustParseLevel` | -| `writer.go` | `Writer` interface, `WriterFunc`, `NoOpWriter` | -| `writer_console.go` | Themed ANSI console output with caller rendering | -| `writer_console_rb.go` | Lock-free ring buffer console writer with timezone support | -| `writer_json.go` | Hand-rolled JSON output (no `encoding/json`) with caller rendering | -| `writer_multi.go` | Async fan-out to named writers, workers close own writer on shutdown | -| `ringbuffer.go` | CAS-based ring buffer, bounded spins, min size 2, batched flushing | -| `template.go` | Log line templates with level styles and caller output | -| `theme.go` | Colour themes with pre-cached ANSI codes via `Theme.Cache()` | -| `sampler.go` | `CountSampler` for high-volume log reduction | +| `logger.go` | `Logger`, log methods, child loggers, `writerSet` sharing | +| `entry.go` | Pooled `Entry`, atomic ref counting | +| `field.go` / `field_convert.go` | Zero-alloc typed fields via `unsafe.Pointer`; typed nil guards via `reflect` | +| `config.go` | `Config`, TTY detection, `resolveColourForWriter` (NO_COLOR/FORCE_COLOR) | +| `options.go` | `New(opts...)` functional options; `WithDevelopment`, `WithProduction`, `WithContainer`, `WithNop`, `WithHighThroughput`, `WithTheme`, `WithLevel`, `WithStructuredLevel`, etc. | +| `level.go` | Log levels, `ParseLevel`, `MustParseLevel`, `AtomicLevel` | +| `writer.go` | `Writer`, `WriterFunc`, `NoOpWriter`, `FilteredWriter`, capability interfaces, `WriterTrusted()` | +| `writer_console.go` | Themed ANSI console output | +| `writer_console_rb.go` | Lock-free ring-buffer console writer | +| `writer_json.go` | Hand-rolled JSON (no `encoding/json`) | +| `writer_multi.go` | Async fan-out to named writers; workers close own writer on shutdown | +| `writer_ring.go` | `RingBufferWriter`, `EntrySnapshot`, `Snapshot`, `Subscribe`, `Stats` | +| `ringbuffer.go` | CAS-based ring buffer, bounded spins, batched flush | +| `template.go` | Log line templates with level styles and caller | +| `theme.go` | Immutable themes via `NewTheme` + `ThemeOption`; `StyleSlot` enum; `Theme.Format`/`Wrap`/`Stylish` | +| `sampler.go` | `CountSampler` for high-volume reduction | | `context.go` | `context.Context` integration | -| `buffer.go` | Tiered `BufferPool`, zero-copy `BytesBuffer`, `AppendTime`, `UnsafeString` | -| `pool.go` | `sync.Pool` instances for entries, fields, buffers | +| `buffer.go` / `pool.go` | Tiered buffer pool, `UnsafeString`, entry/field pools | | `errors.go` | Sentinel errors | -| `renderable.go` | `Renderable` interface; all `*Result` types (`BoxResult`, `TableResult`, `BannerResult`, `TreeResult`, `KeyValueResult`, `SystemInfoResult`) | -| `doc.go` | Package documentation | - -### `velocity/pretty` (`package pretty`) +| `renderable.go` | `Renderable` and `TTYRenderable` interfaces; `Box`, `Table`, `Banner`, `Tree`, `KeyValue`, `SystemInfo` | +| `status.go` | `StatusItem`, `StatusKind` (`StatusOK/Fail/Warn/Info/Pending/Skipped`), `Logger.Status` (inline render) | +| `group.go` | `Group`, `GroupItem`, `Logger.Group` | +| `continuation.go` | `ContinuationBlock`, `Logger.Continue` | +| `pretty.go` | `Pretty` facade, `NewPretty`, `NewPrettyFromLogger`, `CreateBanner` | +| `secure.go` | `Secure`, `SecureURL`, `Redacted`, `Truncated` field constructors; `` tag scanner | +| `hyperlink.go` | OSC 8 `Hyperlink`, `HyperlinksSupported`, `HyperlinkFallback`, `WithHyperlinkFallback` | -| File | Purpose | -|------|---------| -| `pretty.go` | `Pretty` type, `Box`, `Panel`, `Banner`, `Table`, `Tree`, `Bullet`, `KeyValue`, `SystemInfo`, `TreeItem` | -| `renderable.go` | Result types implementing `velocity.Renderable`; pooled render paths for all pretty primitives | -| `progress.go` | `ProgressBar`, `Spinner`, `MultiProgress`, `SpinnerStyle` with CAS-guarded stop | -| `doc.go` | Package documentation | +### `velocity/live` -### `velocity/slog` (`package velocityslog`) +`progress.go` — `ProgressBar`, `Spinner`, `MultiProgress`, `SpinnerStyle` with CAS-guarded stop and TTY detection (NO_COLOR/FORCE_COLOR aware). -| File | Purpose | -|------|---------| -| `handler.go` | `Handler` implementing `log/slog.Handler`, `NewHandler`, `NewLogger` | -| `doc.go` | Package documentation | +### `velocity/slogbridge` -## Test Files +`handler.go` — `Handler` implementing `log/slog.Handler`. `NewHandler`, `NewLogger`. `WithAttrs` pre-converts to velocity `Field`s; `WithGroup` caches dotted prefix. -### Root +## Design -| File | Coverage | -|------|----------| -| `field_test.go` | `itoa` edge cases, typed nil error/stringer | -| `writer_json_test.go` | Nil fields, caller output | -| `writer_console_test.go` | Invalid level bounds, caller output | -| `writer_console_rb_test.go` | Timezone in fallback path | -| `writer_multi_test.go` | Multi-writer fan-out, shutdown drain | -| `ringbuffer_test.go` | Concurrent writes, overflow, bounded spin, zero-length, min size | -| `progress_test.go` | Concurrent Complete/Stop, nil SetStyle (in `pretty/`) | -| `benchmark_test.go` | Benchmarks covering hot paths, fields, writers, pooling, tree-mode, Render API | -| `entry_test.go` | Entry pool, ref counting, concurrent access | -| `with_test.go` | `With()`, `WithTemplate`, nil/empty | -| `fatal_test.go` | Fatal handler, nil logger subprocess test | -| `testutil_test.go` | Shared helpers: `waitFor`, `safeBuffer` | -| `buffer_test.go` | Buffer pool, `UnsafeString` | -| `context_test.go` | Context integration | -| `level_test.go` | Level parsing, atomic level | -| `logger_addwriter_test.go` | Dynamic writer add/remove | -| `logger_detailed_test.go` | Detailed logger behaviour | -| `logger_render_test.go` | `Logger.Render`, `RenderRaw`, `Newline`; JSON writer ignore; no-console no-op | -| `logger_settheme_test.go` | `Logger.Theme()`, `SetTheme` propagation, `With()` clone inheritance | -| `integration_test.go` | End-to-end integration | - -### `velocity/pretty` - -| File | Coverage | -|------|----------| -| `box_test.go` | Long title, border alignment, empty content, Unicode | -| `banner_test.go` | Banner rendering | -| `progress_test.go` | Concurrent Complete/Stop, nil SetStyle | -| `renderable_test.go` | Compile-time Renderable compliance; render parity for all result types | -| `pretty_logger_test.go` | `NewFromLogger` routing; nil logger returns nil | - -### `velocity/slog` - -| File | Coverage | -|------|----------| -| `handler_test.go` | slog bridge: basic, attrs, groups, levels, types, nil, concurrency | - -## Dependencies - -- `golang.org/x/term` for TTY detection only -- Zero third-party test dependencies - -## Design Principles - -- **Zero-alloc hot path**: Fields use `unsafe.Pointer` + `int64` storage. Integer fields write directly via `formatInt` stack buffer. Entry pooling via `sync.Pool` with CAS-based return. ANSI codes pre-cached on `Theme`. Timestamps via `time.AppendFormat`. Floats via `strconv.FormatFloat`. Writers format outside the mutex, locking only for I/O. -- **Three-package split**: Core logging stays in the root package. Pretty-printing (boxes, panels, banners, tables, trees, progress) lives in `velocity/pretty` to keep the root package focused on the hot path. The slog bridge lives in `velocity/slog` (`package velocityslog`) to avoid pulling `log/slog` into callers that don't need it. -- **Field constructors**: `String` (formerly `StringField`), `Error` (formerly `ErrorField`), `Int`, `Float64`, `Bool`, `Duration`, `Time`, `Stringer`, `Bytes`. Typed nils caught via `reflect` in `Error`/`Stringer` constructors. -- **Nil-safe**: Every public method handles nil receivers. Typed nils caught via `reflect` in `Error`/`Stringer` constructors. -- **Thread-safe**: Atomic level checks, mutex-protected writers, lock-free ring buffer. Progress/spinner stop uses `CompareAndSwap` to prevent double-close panics. -- **No `encoding/json`**: JSON writer is hand-rolled for performance. -- **Caller capture**: `AddCaller` populates file:line, rendered by all four writer paths (JSON, template, console fallback, ring buffer fallback). -- **slog bridge**: `velocityslog.Handler` implements `log/slog.Handler`. WithAttrs pre-converts to velocity Fields. WithGroup caches dotted prefix. Level mapping via `mapSlogLevel`. Entry pool used for Handle. +- **Zero-alloc hot path**: `unsafe.Pointer` + `int64` field storage. Integer fields via `formatInt` stack buffer. Entry pooling with CAS-based return. ANSI codes pre-cached on `Theme`. Timestamps via `time.AppendFormat`. Writers format outside the mutex; lock only for I/O. +- **Nil-safe**: every public method handles nil receivers. Typed nils caught via `reflect` in `Error`/`Stringer` constructors. +- **Thread-safe**: atomic level checks, mutex-protected writers, lock-free ring buffer. +- **Trust model**: writers default-untrusted. `WriterTrusted()` opt-in. `Secure` field plaintext only shown to trusted writers; `...` tags in messages auto-scanned and redacted for untrusted writers. +- **Colour resolution**: `NO_COLOR` env disables. `FORCE_COLOR` env forces on. Otherwise `term.IsTerminal` on the writer's fd. All decisions go through `resolveColourForWriter`. +- **`Logger.Status`** renders inline (indented under parent log line, no own timestamp) on the console; JSON writers still receive structured records with `status` field. +- **Shared `writerSet`**: parent and child loggers (`With`, `Detailed`, `WithComponent`, `Request`) share writer topology and `scanSecure` atomic, so `AddWriter` after child creation is visible everywhere. +- **No `encoding/json`** in hot paths. ## Concurrency -- `AtomicLevel`: single atomic load per log call, entries below threshold never allocate -- `MultiWriter`: per-writer buffered channels (256 cap), non-blocking send, `Retain()`/`Release()` lifecycle. Workers close their own writer via defer. Shutdown drain uses `for range ch` to guarantee all entries are processed. -- `RingBuffer`: CAS-based circular buffer, power-of-2 sizing, atomic commit flags, batched flush. Bounded spins (1000 iterations) in both writer and flusher to prevent hangs. Minimum size of 2 enforced. Flusher uses a single reusable batch buffer to avoid per-entry allocations. Shutdown flushes batchBuf before draining the ring. -- `Entry` ref counting: `atomic.Int32`, CAS return to pool prevents double-release -- `Logger.Render`/`RenderRaw`/`Newline`: render into a pooled buffer outside the lock, then acquire `consoleWriter.mu` only for the final write — same mutex as log calls, so rich output cannot interleave with log lines - -## Dependency Graph - -``` -velocity/pretty --> velocity (imports root for Logger, Field types) -velocity/slog --> velocity (imports root for Logger, Entry, Field, Level) -``` +- `AtomicLevel`: single atomic load per log call; sub-threshold entries never allocate. +- `MultiWriter`: per-writer buffered channels (256 cap), non-blocking send, `Retain`/`Release` lifecycle. Workers close their own writer via defer. Shutdown drain via `for range ch`. +- `RingBuffer`: CAS-based, power-of-2 sized, atomic commit flags, batched flush. Bounded spins (1000 iterations). +- `Entry`: `atomic.Int32` ref count; CAS to pool prevents double-release. +- `Logger.Render`/`RenderRaw`/`Newline`: render into a pooled buffer outside the lock, acquire `consoleWriter.mu` only for the final write — same mutex as log calls, so rich output cannot interleave. ## Linting -Uses `default: all` in `.golangci.yml`. `unsafe.Pointer` usage in field/buffer files is excluded from gosec G103. Run `make lint` to verify. +`default: all` in `.golangci.yml`. `unsafe.Pointer` usage excluded from gosec G103 in field/buffer files. Run `make lint` to verify. -## Code Quality +## Code quality -### Always -- Run `make ready` before commit -- Australian English in comments/docs -- Comment **why**, not what +**Always**: run `make ready` before commit · Australian English · comment **why**, not what. -### Never -- Add dependencies without discussion -- Use `encoding/json` in hot paths -- Use `interface{}` where typed fields exist -- Create `_v2`, `_new`, `.bak` files -- Use `fmt.Sprintf` or `strconv.Itoa` on hot paths; use buffer writes or `formatInt` +**Never**: add dependencies without discussion · use `encoding/json` in hot paths · use `interface{}` where typed fields exist · create `_v2`/`_new`/`.bak` files · use `fmt.Sprintf` / `strconv.Itoa` on hot paths. ## Review -Run `/review-velocity` for a comprehensive Opus-powered code review covering concurrency, memory, correctness, performance, API, user expectations, and benchmark validation. +Run `/review-velocity` for an Opus-powered review covering concurrency, memory, correctness, performance, API, and benchmark validation. diff --git a/Makefile b/Makefile index f8445ce..5990901 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ PKG := github.com/tensorfoundrylabs/velocity # Tool versions (pinned) GOLANGCI_LINT_VERSION := v2.11.4 BETTERALIGN_VERSION := latest +BENCHSTAT_VERSION := v0.0.0-20250106010028-fc9b84ea4b35 GOBIN := $(shell go env GOBIN) ifeq ($(GOBIN),) @@ -11,6 +12,7 @@ endif .PHONY: all clean test test-race test-short test-cover lint fmt vet align tidy \ install-tools check-tools ready ready-tools ci help \ + bench bench-baseline perf-gate \ bench-compare bench-compare-short # ── Test ───────────────────────────────────────────────────────────────────── @@ -104,6 +106,7 @@ install-tools: @go install mvdan.cc/gofumpt@latest @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) @go install github.com/dkorunic/betteralign/cmd/betteralign@$(BETTERALIGN_VERSION) + @go install golang.org/x/perf/cmd/benchstat@$(BENCHSTAT_VERSION) @echo "Tools installed." check-tools: @@ -129,6 +132,54 @@ check-tools: else \ printf " goimports: \033[31mnot installed\033[0m\n"; \ fi + @if command -v benchstat >/dev/null 2>&1; then \ + printf " benchstat: installed\n"; \ + else \ + printf " benchstat: \033[33mnot installed (optional, needed for perf-gate)\033[0m\n"; \ + fi + +# ── Benchmarks ─────────────────────────────────────────────────────────────── + +# bench: quick single-run with allocs — use for spot-checking during development. +bench: + @echo "Running benchmarks..." + @go test -bench=. -benchmem -count=3 ./... + +# bench-baseline: captures count=10 run to docs/bench-baseline.txt. +# Run this after any intentional perf improvement so the gate tracks the new normal. +bench-baseline: + @echo "Capturing baseline..." + @mkdir -p docs + @go test -bench=. -benchmem -count=10 ./... > docs/bench-baseline.txt 2>&1 + @echo "Baseline written to docs/bench-baseline.txt" + +# perf-gate: gates on allocation counts vs docs/bench-baseline.txt. +# Allocation counts are deterministic (unlike timing on Windows with short runs), +# so any increase in allocs/op is a definitive regression regardless of count. +# Timing regressions are logged informatively but do not fail the gate here — +# use "make bench-baseline" + manual benchstat for timing verification at release. +# Not run by `make ready` — invoke manually before tagging or when changing hot paths. +perf-gate: + @if [ ! -f docs/bench-baseline.txt ]; then \ + printf "\033[33m no baseline found at docs/bench-baseline.txt -- skipping perf gate\033[0m\n"; \ + exit 0; \ + fi + @echo "Running perf gate (allocation counts)..." + @go test -bench=. -benchmem -count=5 ./... > /tmp/bench-current.txt 2>&1 + @if command -v benchstat >/dev/null 2>&1; then \ + benchstat -col /pkg docs/bench-baseline.txt /tmp/bench-current.txt > /tmp/bench-delta.txt 2>&1 || \ + benchstat docs/bench-baseline.txt /tmp/bench-current.txt > /tmp/bench-delta.txt 2>&1; \ + if awk '/allocs\/op/ && !/~/ && /\+[0-9]/ { print "ALLOC REGRESSION:", $$0; found=1 } END { exit found+0 }' /tmp/bench-delta.txt; then \ + printf "\033[32m perf gate passed (zero-alloc paths unchanged)\033[0m\n"; \ + else \ + printf "\033[31m perf gate FAILED -- allocation count regression detected\033[0m\n"; \ + grep "allocs/op" /tmp/bench-delta.txt; \ + exit 1; \ + fi; \ + else \ + printf "\033[33m benchstat not installed -- skipping alloc comparison\033[0m\n"; \ + printf "\033[33m run: go install golang.org/x/perf/cmd/benchstat@$(BENCHSTAT_VERSION)\033[0m\n"; \ + fi # ── Comparative benchmarks ─────────────────────────────────────────────────── @@ -140,8 +191,6 @@ bench-compare: bench-compare-short: cd benchmarks && go test -bench=. -benchmem -count=1 ./... -.PHONY: bench-compare bench-compare-short - # ── Cleanup ────────────────────────────────────────────────────────────────── clean: @@ -156,29 +205,32 @@ help: @echo " tensorfoundry.io" @echo "" @echo "Test:" - @echo " make test Run tests" - @echo " make test-race Run tests with race detector" - @echo " make test-short Run short tests only" - @echo " make test-cover Run tests with coverage report" + @echo " make test Run tests" + @echo " make test-race Run tests with race detector" + @echo " make test-short Run short tests only" + @echo " make test-cover Run tests with coverage report" @echo "" @echo "Quality:" - @echo " make fmt Format code (goimports + gofumpt)" - @echo " make lint Run golangci-lint (v2, --fix)" - @echo " make vet Run go vet" - @echo " make align Run betteralign (struct field alignment)" - @echo " make tidy Run go mod tidy" + @echo " make fmt Format code (goimports + gofumpt)" + @echo " make lint Run golangci-lint (v2, --fix)" + @echo " make vet Run go vet" + @echo " make align Run betteralign (struct field alignment)" + @echo " make tidy Run go mod tidy" @echo "" @echo "Ready (pre-commit):" - @echo " make ready Full quality gate: tidy, fmt, align, lint, vet, test-race" - @echo " make ready-tools Quick check: fmt, align, lint, vet (no tests)" + @echo " make ready Pre-commit gate: tidy, fmt, align, lint, vet, test-race" + @echo " make ready-tools Quick check: fmt, align, lint, vet (no tests)" @echo "" @echo "CI:" - @echo " make ci Full CI pipeline: quality + tests + coverage" + @echo " make ci Full CI pipeline: quality + tests + coverage" @echo "" @echo "Benchmarks:" + @echo " make bench Quick bench run (count=3) with allocs" + @echo " make bench-baseline Capture count=10 run to docs/bench-baseline.txt" + @echo " make perf-gate Compare allocs vs baseline; fail on any alloc/op regression" @echo " make bench-compare Compare against zap, zerolog, slog, charmbracelet, pterm" @echo " make bench-compare-short Quick single-run comparison" @echo "" @echo "Tools:" - @echo " make install-tools Install golangci-lint, betteralign, goimports, gofumpt" - @echo " make check-tools Show installed tool versions" + @echo " make install-tools Install golangci-lint, betteralign, goimports, gofumpt, benchstat" + @echo " make check-tools Show installed tool versions" diff --git a/README.md b/README.md index c35094e..a1a3ef4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Velocity
CI - Go Reference + Go Reference Go Report Card Release License @@ -13,161 +13,210 @@ Fast, allocation-optimised structured logging for Go with rich terminal output. ## Install ```bash -go get github.com/tensorfoundrylabs/velocity +go get github.com/tensorfoundrylabs/velocity/v2 ``` ## Quick Start ```go -log := velocity.New(os.Stdout) +log := velocity.New(velocity.WithDevelopment()) log.Info("server started", velocity.String("addr", ":8080"), velocity.Int("workers", 4)) ``` -Or use a preset: - -```go -log := velocity.NewDevelopment() // coloured console, debug level -log := velocity.NewWithBuilder(velocity.PresetProduction()) // structured JSON, info level -``` - ## Packages ```go import ( - "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 + "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 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`) | +| `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`) | ## Features -- **Zero-alloc on the hot path** — typed fields (`String`, `Int`, `Float64`, `Bool`, `Duration`, `Error`) use `unsafe.Pointer` storage with no `interface{}` 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.NewHandler` implements `log/slog.Handler` for 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 +- **Zero-alloc on the hot path** — typed fields (`String`, `Int`, `Float64`, `Bool`, `Duration`, `Error`) use `unsafe.Pointer` storage; 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** — `NewTheme` with `ThemeOption`, semantic `StyleSlot` enum, `Theme.Format(slot, s)` for coloured output without raw ANSI, five built-in themes +- **Renderables in root** — `Box`, `Table`, `Tree`, `Banner`, `KeyValue`, `SystemInfo` all live in the root package; `log.Table(...)`, `log.Box(...)` etc. are convenience methods +- **Field-level redaction** — `Secure`, `SecureURL`, `Redacted`, `Truncated` constructors; `...` tag scanning; per-writer trust model via `WriterTrusted()` +- **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 with `Theme.Format` +- **Notify channel** — `Logger.Notify/NotifyLines/NotifyBox` for ephemeral operator output that bypasses the structured pipeline +- **Ring buffer writer** — `RingBufferWriter` with `Snapshot(n)` and `Subscribe(ctx, bufSize)` for in-process log capture +- **slog bridge** — `slogbridge.NewHandler` implements `log/slog.Handler` for incremental adoption - **Log sampling** — `CountSampler` checked 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`/`Newline` serialised under the console writer mutex +- **Nil-safe and testable** — every public method handles nil receivers; overridable `FatalHandler`; `WithTesting(t)` preset ## Performance ### Comparative benchmarks -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](https://github.com/rs/zerolog) | 89 ns / 0 alloc | 204 ns / 0 alloc | 422 ns / 2 alloc | 10 ns / 0 alloc | -| [zap](https://github.com/uber-go/zap) | 240 ns / 0 alloc | 525 ns / 1 alloc | 1319 ns / 6 alloc | 9 ns / 0 alloc | -| [slog](https://pkg.go.dev/log/slog) | 663 ns / 0 alloc | 1666 ns / 4 alloc | 1684 ns / 11 alloc | 10 ns / 0 alloc | -| [charmbracelet/log](https://github.com/charmbracelet/log) | 4 ns / 0 alloc | 6 ns / 0 alloc | 2618 ns / 5 alloc | 4 ns / 0 alloc | -| [pterm](https://github.com/pterm/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. - -### Realistic workload benchmarks - -| Scenario | velocity | [zerolog](https://github.com/rs/zerolog) | [zap](https://github.com/uber-go/zap) | [slog](https://pkg.go.dev/log/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. - -### Internal benchmarks (v1.1, AMD Ryzen 9 5950X, Go 1.24) - -| 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. - -## Presets - -| 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 | +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. -## Integration +| 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](https://github.com/rs/zerolog) | 66 ns / 0 alloc | 162 ns / 0 alloc | 459 ns / 2 alloc | 7.3 ns / 0 alloc | +| [zap](https://github.com/uber-go/zap) | 233 ns / 0 alloc | 475 ns / 1 alloc | 3045 ns / 6 alloc | 6.5 ns / 0 alloc | +| [slog](https://pkg.go.dev/log/slog) | 417 ns / 0 alloc | 992 ns / 4 alloc | 1177 ns / 11 alloc | 6.8 ns / 0 alloc | +| [charmbracelet/log](https://github.com/charmbracelet/log) | 3.2 ns / 0 alloc | 3.9 ns / 0 alloc | 2815 ns / 5 alloc | 3.2 ns / 0 alloc | +| [pterm](https://github.com/pterm/pterm) | 8376 ns / 65 alloc | 16637 ns / 144 alloc | 8213 ns / 65 alloc | 17 ns / 0 alloc | -### log/slog bridge +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. + +### Internal benchmarks (v2.0.0, AMD Ryzen 9 5950X, Go 1.24) + +| 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 ./...` + +## Usage + +### Presets + +```go +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 output +``` + +### Typed fields + +```go +log.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), +) +``` + +### Child loggers + +```go +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)) +``` + +### Secure fields and redaction ```go -import velocityslog "github.com/tensorfoundrylabs/velocity/slog" +// Plaintext on TTY console, [REDACTED] in JSON and non-TTY output. +log.Info("user authenticated", velocity.Secure("token", "tok_abc123")) + +// tag scanning works in message strings too. +log.Info("connecting to redis://admin:hunter2@cache.internal") +``` + +### Themes -logger := velocity.NewDevelopment() -slog.SetDefault(velocityslog.NewLogger(logger)) +```go +// Built-in themes. +log := velocity.New(velocity.WithTheme(velocity.ThemeNightOwl)) -slog.Info("request handled", "method", "GET", "status", 200, "duration", 42*time.Millisecond) +// 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") ``` -Groups produce dotted keys: `slog.WithGroup("server").With("host", "localhost")` renders as `server.host`. +**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 | -### Pretty printing +`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. + +### Renderables ```go -import "github.com/tensorfoundrylabs/velocity/pretty" +// Convenience methods route through the console writer mutex. +log.Table([]string{"Service", "Status"}, [][]string{{"api", "running"}}) +log.Box("Deploy Complete", "3/4 nodes healthy") -p := pretty.New(os.Stdout, velocity.ThemeNightOwl) -p.Box("Deploy Complete", "All services running") -p.Banner("v2.1.0 - Production release") +// Standalone construction for embedding or capture. +t := velocity.NewTable(headers, rows, velocity.ThemeNightOwl) +fmt.Print(t.String()) ``` -When a logger exists, prefer `NewFromLogger` — output routes through the logger's console writer and aligns with the message column: +### Visual primitives ```go -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"}, -})) +// 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"), +) ``` +### log/slog bridge + +```go +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) +``` + +## Integration + ### Log rotation with lumberjack ```go rotator := &lumberjack.Logger{Filename: "/var/log/app.log", MaxSize: 500, Compress: true} -cfg := velocity.DefaultProductionConfig() -cfg.StructuredOutput = rotator -log := velocity.NewWithConfig(cfg) +log := velocity.New( + velocity.WithConsoleOutput(os.Stdout), + velocity.WithStructuredOutput(rotator), +) ``` ## Dependencies @@ -176,7 +225,7 @@ One: [`golang.org/x/term`](https://pkg.go.dev/golang.org/x/term) for TTY detecti ## Similar Libraries -- [pTerm](https://github.com/pterm/pterm) — visually rich terminal output library that Velocity's styles are modelled on; Velocity trades some visual features for speed and lower allocations +- [pTerm](https://github.com/pterm/pterm) — visually rich terminal output library; Velocity trades some visual features for speed and lower allocations - [logrus](https://github.com/sirupsen/logrus) — popular structured logger; Velocity targets significantly lower latency for high-volume CLI workloads ## Licence diff --git a/assets/banner.png b/assets/banner.png index 556978b..563d593 100644 Binary files a/assets/banner.png and b/assets/banner.png differ diff --git a/benchmark_pretty_test.go b/benchmark_pretty_test.go new file mode 100644 index 0000000..2024694 --- /dev/null +++ b/benchmark_pretty_test.go @@ -0,0 +1,47 @@ +package velocity_test + +import ( + "io" + "testing" + + velocity "github.com/tensorfoundrylabs/velocity/v2" +) + +var ( + benchPrettyHeaders = []string{"Service", "Status"} + benchPrettyRows = [][]string{ + {"api-gateway", "running"}, + {"worker", "stopped"}, + {"scheduler", "running"}, + } +) + +func newBenchLogger() *velocity.Logger { + return velocity.New( + velocity.WithConsoleOutput(io.Discard), + velocity.WithStructuredOutput(io.Discard), + velocity.WithLevel(velocity.LevelDebug), + velocity.WithStructuredLevel(velocity.LevelDebug), + ) +} + +// BenchmarkPretty_NewFromLogger_Table measures the full render path via NewPrettyFromLogger. +func BenchmarkPretty_NewFromLogger_Table(b *testing.B) { + log := newBenchLogger() + p := velocity.NewPrettyFromLogger(log) + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + p.Table(benchPrettyHeaders, benchPrettyRows) + } +} + +// BenchmarkPretty_New_Table measures the same table render via the standalone NewPretty path. +func BenchmarkPretty_New_Table(b *testing.B) { + p := velocity.NewPretty(io.Discard, velocity.ThemeNightOwl) + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + p.Table(benchPrettyHeaders, benchPrettyRows) + } +} diff --git a/benchmark_test.go b/benchmark_test.go index ae261b0..c97234e 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -9,13 +9,13 @@ import ( // newDiscardLogger builds a real logger that formats output but discards it, // so we measure formatting cost rather than I/O cost. func newDiscardLogger() *Logger { - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = io.Discard cfg.StructuredOutput = io.Discard // Force both writers active so we measure full formatting overhead. cfg.ConsoleLevel = LevelDebug cfg.StructuredLevel = LevelDebug - return NewWithConfig(cfg) + return newFromConfig(cfg) } // fiveFields returns a representative slice of mixed-type fields. @@ -139,23 +139,23 @@ func BenchmarkFloat64Field(b *testing.B) { _ = f } -// BenchmarkF_String measures the type-switch overhead in the generic constructor. -func BenchmarkF_String(b *testing.B) { +// BenchmarkAny_String measures the Any() generic constructor overhead. +func BenchmarkAny_String(b *testing.B) { b.ReportAllocs() b.ResetTimer() var f Field for b.Loop() { - f = F("key", "value") + f = Any("key", "value") } _ = f } -func BenchmarkF_Int(b *testing.B) { +func BenchmarkAny_Int(b *testing.B) { b.ReportAllocs() b.ResetTimer() var f Field for b.Loop() { - f = F("port", 8080) + f = Any("port", 8080) } _ = f } @@ -280,12 +280,12 @@ func BenchmarkJSONWriter_Parallel(b *testing.B) { // BenchmarkInfo_TreeMode measures the badge-style tree-mode path where the // cachedIndentStr is used in place of strings.Repeat on every field. func BenchmarkInfo_TreeMode(b *testing.B) { - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = io.Discard cfg.StructuredOutput = nil cfg.ConsoleLevel = LevelDebug cfg.FieldDisplayMode = FieldDisplayTree - l := NewWithConfig(cfg) + l := newFromConfig(cfg) fields := fiveFields() b.ReportAllocs() b.ResetTimer() @@ -296,12 +296,12 @@ func BenchmarkInfo_TreeMode(b *testing.B) { // BenchmarkInfo_TreeMode_Parallel measures concurrent tree-mode throughput. func BenchmarkInfo_TreeMode_Parallel(b *testing.B) { - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = io.Discard cfg.StructuredOutput = nil cfg.ConsoleLevel = LevelDebug cfg.FieldDisplayMode = FieldDisplayTree - l := NewWithConfig(cfg) + l := newFromConfig(cfg) fields := fiveFields() b.ReportAllocs() b.ResetTimer() @@ -312,15 +312,15 @@ func BenchmarkInfo_TreeMode_Parallel(b *testing.B) { }) } -// BenchmarkInfoDetailed_TreeMode measures the InfoDetailed path which always forces +// BenchmarkDetailed_TreeMode measures the Detailed() child path which forces // tree display regardless of the configured FieldDisplayMode. -func BenchmarkInfoDetailed_TreeMode(b *testing.B) { - l := newDiscardLogger() +func BenchmarkDetailed_TreeMode(b *testing.B) { + l := newDiscardLogger().Detailed() fields := fiveFields() b.ReportAllocs() b.ResetTimer() for b.Loop() { - l.InfoDetailed("detailed entry", fields...) + l.Info("detailed entry", fields...) } } @@ -340,7 +340,7 @@ func BenchmarkBufferPool_GetPut(b *testing.B) { // ---- Render API benchmarks -------------------------------------------------- // BenchmarkLogger_Render measures the cost of one Logger.Render call with a -// small pre-built TableResult (3 rows, 2 columns). Construction is excluded +// small pre-built Table (3 rows, 2 columns). Construction is excluded // from the timer so we isolate the indentation + write path. func BenchmarkLogger_Render(b *testing.B) { l := newDiscardLogger() @@ -389,3 +389,47 @@ func (r *bytesRenderable) Render(w io.Writer) error { _, err := w.Write(r.data) return err } + +// ---- v2 budget stubs -------------------------------------------------------- +// These benchmarks establish the v1 baselines against which v2 targets are measured. +// See docs/bench-v1.1.3.txt for the captured numbers. + +// BenchmarkWithComponent_Equivalent measures the v1 cost of producing a child +// logger with a component field via With(). v2 replaces this with WithComponent(). +func BenchmarkWithComponent_Equivalent(b *testing.B) { + l := newDiscardLogger() + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + child := l.With(String("component", "auth-service")) + _ = child + } +} + +// BenchmarkSecureScan_NoMatch is the v1 baseline for the v2 -tag scan path. +// In v2 an IndexByte scan runs before field formatting when untrusted writers are +// present; this bench establishes the pre-scan cost for a message containing no '<'. +func BenchmarkSecureScan_NoMatch(b *testing.B) { + l := newDiscardLogger() + fields := fiveFields() + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + l.Info("request completed successfully with no sensitive data in message", fields...) + } +} + +// BenchmarkSecureField_UntrustedWriter documents the cost of a Secure field +// hitting an untrusted JSON writer. Documents one redacted string alloc per +// affected (entry × untrusted writer) pair. The Secure() call itself is one +// alloc at construction; the alloc here is for string allocation on format path. +func BenchmarkSecureField_UntrustedWriter(b *testing.B) { + l := newDiscardLogger() + // Attach an untrusted JSON writer so scanSecure=true and redaction fires. + l.AddWriter("json", NewJSONWriter(io.Discard)) + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + l.Info("request completed", Secure("session", "abc123def456")) + } +} diff --git a/benchmarks/bench_test.go b/benchmarks/bench_test.go index 335e8cc..29a566d 100644 --- a/benchmarks/bench_test.go +++ b/benchmarks/bench_test.go @@ -16,7 +16,7 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" - velocity "github.com/tensorfoundrylabs/velocity" + velocity "github.com/tensorfoundrylabs/velocity/v2" ) // Library describes the subset of a logging library's API needed for comparison. @@ -50,11 +50,14 @@ var libraries = []Library{ { Name: "velocity", Setup: func() any { - cfg := velocity.DefaultConfig() - cfg.StructuredOutput = io.Discard - cfg.StructuredLevel = velocity.LevelDebug - cfg.ConsoleOutput = nil - return velocity.NewWithConfig(cfg) + // JSON-only to io.Discard: console level set to Off so only the + // JSON serialisation path runs. Makes the comparison fair against + // pure structured loggers (zap, zerolog, slog). + return velocity.New( + velocity.WithLevel(velocity.LevelOff), + velocity.WithStructuredOutput(io.Discard), + velocity.WithStructuredLevel(velocity.LevelDebug), + ) }, Info: func(l any) { l.(*velocity.Logger).Info("request completed") @@ -76,11 +79,11 @@ var libraries = []Library{ ) }, AccumulatedCtx: func() any { - cfg := velocity.DefaultConfig() - cfg.StructuredOutput = io.Discard - cfg.StructuredLevel = velocity.LevelDebug - cfg.ConsoleOutput = nil - l := velocity.NewWithConfig(cfg) + l := velocity.New( + velocity.WithLevel(velocity.LevelOff), + velocity.WithStructuredOutput(io.Discard), + velocity.WithStructuredLevel(velocity.LevelDebug), + ) return l.With( velocity.String("k1", "v1"), velocity.String("k2", "v2"), @@ -497,11 +500,11 @@ var disabledLibraries = []Library{ { Name: "velocity", Setup: func() any { - cfg := velocity.DefaultConfig() - cfg.StructuredOutput = io.Discard - cfg.StructuredLevel = velocity.LevelError - cfg.ConsoleOutput = nil - return velocity.NewWithConfig(cfg) + return velocity.New( + velocity.WithLevel(velocity.LevelOff), + velocity.WithStructuredOutput(io.Discard), + velocity.WithStructuredLevel(velocity.LevelError), + ) }, InfoDisabled: func(l any) { l.(*velocity.Logger).Debug("suppressed") diff --git a/benchmarks/go.mod b/benchmarks/go.mod index 54a603e..299570e 100644 --- a/benchmarks/go.mod +++ b/benchmarks/go.mod @@ -1,14 +1,14 @@ -module github.com/tensorfoundrylabs/velocity/benchmarks +module github.com/tensorfoundrylabs/velocity/v2/benchmarks go 1.24.0 -replace github.com/tensorfoundrylabs/velocity => ../ +replace github.com/tensorfoundrylabs/velocity/v2 => ../ require ( github.com/charmbracelet/log v1.0.0 github.com/pterm/pterm v0.12.83 github.com/rs/zerolog v1.35.0 - github.com/tensorfoundrylabs/velocity v0.0.0-00010101000000-000000000000 + github.com/tensorfoundrylabs/velocity/v2 v2.0.0-00010101000000-000000000000 go.uber.org/zap v1.27.1 ) diff --git a/config.go b/config.go index 9dd084b..4077b07 100644 --- a/config.go +++ b/config.go @@ -1,8 +1,6 @@ package velocity import ( - "errors" - "fmt" "io" "os" "time" @@ -10,6 +8,7 @@ import ( "golang.org/x/term" ) +// Format is the output format for structured (JSON) writers. type Format int const ( @@ -25,6 +24,7 @@ func (f Format) String() string { } } +// FieldDisplayMode controls whether fields render inline or as a tree. type FieldDisplayMode int const ( @@ -47,43 +47,48 @@ func (m FieldDisplayMode) String() string { // Default behaviour is os.Exit(1). Override in tests to prevent process exit. type FatalHandler func() -type Config struct { +// config holds all logger configuration. Unexported — callers configure via Options. +type config struct { ConsoleOutput io.Writer - // FatalHandler overrides the default os.Exit(1) called after Fatal(). - // Useful in tests. If nil, defaults to os.Exit(1). - FatalHandler FatalHandler - StructuredOutput io.Writer - ConsoleTheme *Theme Sampler Sampler - // Logs are always stored in UTC, but can be displayed in a different timezone - DisplayTimezone *time.Location + // NotifyOutput is the destination for Notify/NotifyLines/NotifyBox calls. + // Defaults to os.Stderr. Override via WithNotifyOutput — useful in tests + // where stderr is not captured by the test runner. + NotifyOutput io.Writer + + FatalHandler FatalHandler - TimeFormat string + ConsoleTheme *Theme + DisplayTimezone *time.Location + TimeFormat string StructuredFormat Format BufferSize int FieldPoolSize int FieldDisplayMode FieldDisplayMode - ConsoleLevel Level + // CallerSkip is extra frames to skip beyond the standard 4; use for wrapper functions. + CallerSkip int + ConsoleLevel Level StructuredLevel Level DisableColour bool + AddCaller bool - // AddCaller enables capturing file:line and function name for each log entry - AddCaller bool - // CallerSkip is the number of stack frames to skip when capturing caller information - // Default is 0, increase for wrapper functions - CallerSkip int + // DisableSecureTags permanently disables the ... message scanner. + // Set via WithSecureTags(false). When true, no IndexByte scan runs on any log call + // regardless of which writers are attached. Use for extreme-perf consumers that + // never embed sensitive data in message strings. + DisableSecureTags bool } -func DefaultConfig() *Config { - return &Config{ +func defaultConfig() *config { + return &config{ ConsoleOutput: os.Stdout, ConsoleLevel: LevelDebug, StructuredOutput: nil, @@ -97,241 +102,6 @@ func DefaultConfig() *Config { } } -type Builder struct { - config *Config -} - -func NewConfig() *Builder { - return &Builder{ - config: DefaultConfig(), - } -} - -func (b *Builder) WithLevel(level Level) *Builder { - b.config.ConsoleLevel = level - return b -} - -func (b *Builder) WithFormat(format Format) *Builder { - b.config.StructuredFormat = format - return b -} - -func (b *Builder) WithOutput(w io.Writer) *Builder { - b.config.ConsoleOutput = w - return b -} - -func (b *Builder) WithStructuredOutput(w io.Writer) *Builder { - b.config.StructuredOutput = w - return b -} - -func (b *Builder) WithStructuredLevel(level Level) *Builder { - b.config.StructuredLevel = level - return b -} - -func (b *Builder) WithTheme(theme *Theme) *Builder { - b.config.ConsoleTheme = theme - return b -} - -func (b *Builder) WithTimeFormat(format string) *Builder { - b.config.TimeFormat = format - return b -} - -func (b *Builder) WithBufferSize(size int) *Builder { - b.config.BufferSize = size - return b -} - -func (b *Builder) WithFieldPoolSize(size int) *Builder { - b.config.FieldPoolSize = size - return b -} - -func (b *Builder) WithColour(enabled bool) *Builder { - b.config.DisableColour = !enabled - return b -} - -func (b *Builder) DisableColour() *Builder { - b.config.DisableColour = true - return b -} - -func (b *Builder) WithSampling(initial, thereafter uint32) *Builder { - b.config.Sampler = NewCountSampler(uint64(initial), uint64(thereafter)) - return b -} - -// WithDisplayTimezone sets the timezone for displaying timestamps in console output. -// Logs are always stored in UTC internally, but this controls how they're displayed. -func (b *Builder) WithDisplayTimezone(tz string) (*Builder, error) { - loc, err := time.LoadLocation(tz) - if err != nil { - return b, fmt.Errorf("invalid timezone %q: %w", tz, err) - } - b.config.DisplayTimezone = loc - return b, nil -} - -func (b *Builder) MustWithDisplayTimezone(tz string) *Builder { - loc, err := time.LoadLocation(tz) - if err != nil { - panic(fmt.Sprintf("velocity: invalid timezone %q: %v", tz, err)) - } - b.config.DisplayTimezone = loc - return b -} - -func (b *Builder) WithFieldDisplayMode(mode FieldDisplayMode) *Builder { - b.config.FieldDisplayMode = mode - return b -} - -func (b *Builder) WithFatalHandler(fn FatalHandler) *Builder { - b.config.FatalHandler = fn - return b -} - -func (b *Builder) Build() (*Config, error) { - if err := b.validate(); err != nil { - return nil, err - } - return b.config, nil -} - -func (b *Builder) MustBuild() *Config { - cfg, err := b.Build() - if err != nil { - panic(fmt.Sprintf("velocity: invalid configuration: %v", err)) - } - return cfg -} - -func (b *Builder) validate() error { - if b.config.BufferSize < 256 { - return fmt.Errorf("buffer size must be at least 256 bytes, got %d", b.config.BufferSize) - } - if b.config.BufferSize > 1024*1024 { - return fmt.Errorf("buffer size must not exceed 1MB, got %d", b.config.BufferSize) - } - - if b.config.FieldPoolSize < 0 { - return fmt.Errorf("field pool size must not be negative, got %d", b.config.FieldPoolSize) - } - if b.config.FieldPoolSize > 10000 { - return fmt.Errorf("field pool size must not exceed 10000, got %d", b.config.FieldPoolSize) - } - - if b.config.Sampler != nil { - // CountSampler validation (check if it's our concrete type) - if cs, ok := b.config.Sampler.(*CountSampler); ok { - if cs.Initial == 0 && cs.Thereafter == 0 { - return errors.New("sampling initial and thereafter counts must not both be zero") - } - } - } - - return nil -} - -func (b *Builder) Clone() *Builder { - cfgCopy := *b.config - // Sampler is copied by value - interface reference is shared - // If deep copy is needed for custom samplers, implement Clone() on sampler - return &Builder{ - config: &cfgCopy, - } -} - -// DefaultDevelopmentConfig creates a config for development: coloured console, debug level, no structured output. -func DefaultDevelopmentConfig() *Config { - return &Config{ - ConsoleOutput: os.Stdout, - ConsoleTheme: nil, - ConsoleLevel: LevelDebug, - StructuredOutput: nil, - StructuredFormat: FormatJSON, - StructuredLevel: LevelOff, - BufferSize: 1024, - FieldPoolSize: 50, - DisableColour: false, - TimeFormat: "2006-01-02 15:04:05", - DisplayTimezone: time.Local, - } -} - -// DefaultProductionConfig creates a config for production: JSON output, info level, no console. -func DefaultProductionConfig() *Config { - return &Config{ - ConsoleOutput: io.Discard, - ConsoleTheme: nil, - ConsoleLevel: LevelOff, - StructuredOutput: nil, - StructuredFormat: FormatJSON, - StructuredLevel: LevelInfo, - BufferSize: 4096, - FieldPoolSize: 200, - DisableColour: true, - TimeFormat: "2006-01-02T15:04:05Z07:00", - } -} - -// DefaultContainerConfig creates a config for containerised environments: JSON to stdout, info level. -func DefaultContainerConfig() *Config { - disableColour := !isTerminal(os.Stdout) - - return &Config{ - ConsoleOutput: nil, - ConsoleTheme: nil, - ConsoleLevel: LevelOff, - StructuredOutput: os.Stdout, - StructuredFormat: FormatJSON, - StructuredLevel: LevelInfo, - BufferSize: 2048, - FieldPoolSize: 100, - DisableColour: disableColour, - TimeFormat: "2006-01-02T15:04:05Z07:00", - } -} - -// DefaultTestingConfig creates a config for tests: writes to w, debug level, colours off. -func DefaultTestingConfig(w io.Writer) *Config { - return &Config{ - ConsoleOutput: w, - ConsoleTheme: nil, - ConsoleLevel: LevelDebug, - StructuredOutput: nil, - StructuredFormat: FormatJSON, - StructuredLevel: LevelOff, - BufferSize: 512, - FieldPoolSize: 25, - DisableColour: true, - TimeFormat: "15:04:05.000", - } -} - -// DefaultHighPerformanceConfig creates a config for high throughput: minimal output, sampling enabled. -func DefaultHighPerformanceConfig() *Config { - return &Config{ - ConsoleOutput: io.Discard, - ConsoleTheme: nil, - ConsoleLevel: LevelOff, - StructuredOutput: os.Stderr, - StructuredFormat: FormatJSON, - StructuredLevel: LevelInfo, - BufferSize: 8192, - FieldPoolSize: 500, - DisableColour: true, - TimeFormat: "2006-01-02T15:04:05Z07:00", - Sampler: NewCountSampler(uint64(1000), uint64(100)), - } -} - // isTerminal reports whether f is connected to a terminal. func isTerminal(f *os.File) bool { if f == nil { @@ -351,31 +121,41 @@ func isTerminal(f *os.File) bool { } } +// resolveColourForWriter reports whether ANSI colour should be emitted to w, +// applying the standard environment overrides in priority order: +// +// 1. NO_COLOR= — always disable (https://no-color.org) +// 2. FORCE_COLOR= — always enable +// 3. term.IsTerminal — auto-detect from the file descriptor +// +// Windows terminal emulators (VS Code, Git Bash, Windows Terminal) often +// present stdout as a named pipe rather than a console handle, which causes +// term.IsTerminal to return false even on a real terminal. FORCE_COLOR=1 is +// the documented escape hatch for those environments. +func resolveColourForWriter(w io.Writer) bool { + // NO_COLOR has highest priority — explicit opt-out. + if os.Getenv("NO_COLOR") != "" { + return false + } + // FORCE_COLOR overrides TTY detection — explicit opt-in. + if os.Getenv("FORCE_COLOR") != "" { + return true + } + // Fall back to fd-level detection. + return IsTerminalWriter(w) +} + // IsTerminalWriter reports whether w is a terminal, using term.IsTerminal when possible. // Used to auto-detect colour support. +// +// Note: on Windows, terminal emulators that run shells as child processes (VS Code, +// Git Bash, Windows Terminal) may proxy stdout through a pipe, causing this to return +// false even when the output is visible in a colour-capable terminal. In that case, +// set FORCE_COLOR=1 to override detection, or use resolveColourForWriter which +// handles both env vars and fd detection. func IsTerminalWriter(w io.Writer) bool { if f, ok := w.(*os.File); ok { return term.IsTerminal(int(f.Fd())) //nolint:gosec // G115: uintptr fd fits in int on all supported platforms } return false } - -func PresetDevelopment() *Builder { - return &Builder{config: DefaultDevelopmentConfig()} -} - -func PresetProduction() *Builder { - return &Builder{config: DefaultProductionConfig()} -} - -func PresetContainer() *Builder { - return &Builder{config: DefaultContainerConfig()} -} - -func PresetTesting(w io.Writer) *Builder { - return &Builder{config: DefaultTestingConfig(w)} -} - -func PresetHighPerformance() *Builder { - return &Builder{config: DefaultHighPerformanceConfig()} -} diff --git a/context.go b/context.go index c4b9bc0..49ada59 100644 --- a/context.go +++ b/context.go @@ -15,11 +15,11 @@ func NewContext(ctx context.Context, l *Logger) context.Context { // FromContext retrieves the logger from ctx. // If ctx carries additional fields via ContextWithFields, they are prepended // via With() before returning. -// Returns NopLogger() if no logger is stored — never returns nil. +// Returns New(WithNop()) if no logger is stored — never returns nil. func FromContext(ctx context.Context) *Logger { l, ok := ctx.Value(contextKey{}).(*Logger) if !ok || l == nil { - return NopLogger() + return New(WithNop()) } if fields, ok := ctx.Value(contextFieldsKey{}).([]Field); ok && len(fields) > 0 { return l.With(fields...) diff --git a/context_test.go b/context_test.go index 1950ae2..7add3a7 100644 --- a/context_test.go +++ b/context_test.go @@ -11,7 +11,7 @@ func TestNewContext_FromContext_RoundTrip(t *testing.T) { t.Parallel() var buf bytes.Buffer - l := NewForTesting(&buf) + l := newForTesting(&buf) ctx := NewContext(context.Background(), l) got := FromContext(ctx) @@ -37,7 +37,7 @@ func TestContextWithFields_Accumulates(t *testing.T) { t.Parallel() var buf bytes.Buffer - parent := NewForTesting(&buf) + parent := newForTesting(&buf) ctx := NewContext(context.Background(), parent) ctx = ContextWithFields(ctx, String("layer", "middleware")) diff --git a/continuation.go b/continuation.go new file mode 100644 index 0000000..b480075 --- /dev/null +++ b/continuation.go @@ -0,0 +1,294 @@ +package velocity + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "time" + "unsafe" +) + +// continuationGlyph is the Unicode box-drawing pipe used on continuation lines. +// It renders cleanly in every modern terminal and editor — far more common than +// some full-block alternatives, and visually distinct from the ASCII pipe character. +const continuationGlyph = "│" + +// continuationGlyphSep is the glyph followed by a single space, written before each line. +const continuationGlyphSep = continuationGlyph + " " + +// continuationKey is the JSON field name for the continuation lines array. +const continuationKey = "continuation" + +// osc8Open and osc8Close are the byte sequences that delimit an OSC 8 hyperlink. +// Format: \x1b]8;;\x07\x1b]8;;\x07 +// We strip these from JSON because log aggregators and structured pipelines have +// no use for terminal control sequences and must not receive raw ESC bytes. +const ( + osc8Open = "\x1b]8;;" + osc8Close = "\x1b]8;;\x07" +) + +// stripOSC8 removes OSC 8 hyperlink escape sequences from s, leaving only the +// visible display text. A compliant OSC 8 sequence looks like: +// +// \x1b]8;;\x07\x1b]8;;\x07 +// +// When no sequences are present the original string is returned without allocation. +func stripOSC8(s string) string { + if !strings.Contains(s, osc8Open) { + return s + } + + var b strings.Builder + for len(s) > 0 { + idx := strings.Index(s, osc8Open) + if idx < 0 { + b.WriteString(s) + break + } + // Write everything before the opening escape. + b.WriteString(s[:idx]) + s = s[idx+len(osc8Open):] + + // Skip to the \x07 that terminates the URI part, then the display text begins. + bell := strings.IndexByte(s, '\x07') + if bell < 0 { + // Malformed sequence — emit the remainder as-is. + b.WriteString(s) + break + } + s = s[bell+1:] // advance past \x07 + + // Collect display text up to the closing osc8Close sequence. + end := strings.Index(s, osc8Close) + if end < 0 { + // No closing tag — treat the rest as visible text. + b.WriteString(s) + break + } + b.WriteString(s[:end]) + s = s[end+len(osc8Close):] + } + return b.String() +} + +// ContinuationBlock is a Renderable that displays a primary log line followed by +// continuation lines prefixed with a │ glyph and indented to the message column. +// Designed for structured "server started at "-style output that needs both +// the log discipline of a proper entry and the readability of multi-line display. +// +// On TTY the glyph is coloured with SlotContinuation; on non-TTY the same Unicode +// glyph is used without colour — keeping visual parity across pipe and terminal +// while only the decoration differs. TTY detection happens at Render time via +// IsTerminalWriter, so the same block may be rendered to both a terminal and a file. +// +// In JSON the continuation lines are emitted as a "continuation" array. Any OSC 8 +// hyperlink sequences in the lines are stripped from the JSON form because log +// aggregators cannot render terminal control sequences. +type ContinuationBlock struct { + theme *Theme + msg string + lines []string +} + +// NewContinuationBlock constructs a ContinuationBlock. theme may be nil (falls +// back to ThemeNightOwl). TTY detection is deferred to Render time — callers do +// not need to pass isTTY. +func NewContinuationBlock(msg string, lines []string, theme *Theme) *ContinuationBlock { + if theme == nil { + theme = ThemeNightOwl + } + ls := make([]string, len(lines)) + copy(ls, lines) + return &ContinuationBlock{ + msg: msg, + lines: ls, + theme: theme, + } +} + +// Render writes the continuation block to w. TTY is detected from w at call time: +// when w is a real terminal the SlotContinuation glyph is coloured; otherwise plain. +// The first line is the message; subsequent lines follow with the │ prefix and indent. +// +// Note: when called via Logger.Render the writer is an intermediate buffer. +// Logger.Render detects TTYRenderable and calls RenderTTY with the correct TTY state. +func (c *ContinuationBlock) Render(w io.Writer) error { + if c == nil { + return nil + } + return c.RenderTTY(w, IsTerminalWriter(w)) +} + +// RenderTTY writes the continuation block to w with explicit TTY state. Callers +// that already know the terminal state (e.g. Logger.Render) should use this to +// avoid false-negative TTY detection on intermediate buffers. +func (c *ContinuationBlock) RenderTTY(w io.Writer, isTTY bool) error { + if c == nil { + return nil + } + var buf bytes.Buffer + if isTTY { + renderContinuationTTY(&buf, c.msg, c.lines, c.theme) + } else { + renderContinuationPlain(&buf, c.msg, c.lines) + } + _, err := w.Write(buf.Bytes()) + return err +} + +// String renders the block to a string. Useful in tests and for capture. +func (c *ContinuationBlock) String() string { + if c == nil { + return "" + } + var buf bytes.Buffer + _ = c.Render(&buf) + return buf.String() +} + +// renderContinuationTTY builds the ANSI form: coloured message, then each +// continuation line prefixed with a SlotContinuation-coloured │ glyph. +func renderContinuationTTY(buf *bytes.Buffer, msg string, lines []string, theme *Theme) { + // Header message, coloured. + msgCode := theme.CachedMessageFg() + if msgCode != "" { + buf.WriteString(msgCode) + } + buf.WriteString(msg) + if msgCode != "" { + buf.WriteString(theme.ResetStr()) + } + buf.WriteByte('\n') + + // Continuation lines: │ glyph (SlotContinuation), space, line text. + glyphPrefix, glyphSuffix := theme.Wrap(SlotContinuation) + for _, line := range lines { + buf.WriteString(glyphPrefix) + buf.WriteString(continuationGlyphSep) + buf.WriteString(glyphSuffix) + buf.WriteString(line) + buf.WriteByte('\n') + } +} + +// renderContinuationPlain builds the non-ANSI form. The same Unicode │ glyph is +// kept — it is a printable character that renders in any modern terminal, editor, +// or log file viewer. Only the colour wrapper is omitted. This maintains visual +// parity with the TTY form without emitting ANSI control bytes. +func renderContinuationPlain(buf *bytes.Buffer, msg string, lines []string) { + buf.WriteString(msg) + buf.WriteByte('\n') + for _, line := range lines { + buf.WriteString(continuationGlyphSep) + buf.WriteString(line) + buf.WriteByte('\n') + } +} + +// continuationLinesField constructs a Field carrying a []string slice. +// One heap alloc per Logger.Continue call; entries that never call Continue pay nothing. +func continuationLinesField(lines []string) Field { + cp := make([]string, len(lines)) + copy(cp, lines) + return Field{ + Key: continuationKey, + Type: FieldTypeContinuationLines, + value: unsafe.Pointer(&cp), //nolint:gosec // G103: same unsafe.Pointer pattern used throughout field.go + } +} + +// continuationLinesFromField recovers the []string stored in a FieldTypeContinuationLines field. +// Returns nil if f is not of that type. +func continuationLinesFromField(f Field) []string { + if f.Type != FieldTypeContinuationLines || f.value == nil { + return nil + } + return *(*[]string)(f.value) +} + +// Continue logs a primary message at the given level, then emits each of lines +// as a continuation line prefixed with a │ glyph indented to the message column. +// +// On a TTY console the output looks like: +// +// 2006-01-02T15:04:05+10:00 [INFO] HTTP server listening +// │ Available at http://localhost:8080 +// │ Press Ctrl+C to stop +// +// The JSON writer emits a single entry with a "continuation" array. OSC 8 +// hyperlink escape sequences in the lines are stripped from JSON output — +// log aggregators cannot render terminal control sequences. +// +// All standard log-call semantics apply: level filtering, sampling, base fields. +func (l *Logger) Continue(level Level, msg string, lines ...string) { + if l == nil { + fmt.Fprintf(os.Stderr, "[%s] %s\n", level.ConciseLabel(), msg) + for _, line := range lines { + fmt.Fprintf(os.Stderr, " %s %s\n", continuationGlyph, line) + } + return + } + if l.closed.Load() || !l.isEnabled(level) { + return + } + l.logContinue(level, msg, lines) +} + +// logContinue is the internal implementation of Continue. +func (l *Logger) logContinue(level Level, msg string, lines []string) { + if l == nil { + return + } + + if l.sampler != nil && !l.sampler.Sample(level, msg) { + return + } + + entry := GetEntry() + defer entry.Release() + + entry.SetLevel(level) + entry.SetMessage(msg) + entry.SetTime(time.Now()) + entry.forceTreeDisplay = l.forceTreeDisplay + + if l.writers.scanSecure.Load() && strings.IndexByte(msg, '<') >= 0 { + entry.maybeSecure = true + } + + if len(l.baseFields) > 0 { + entry.WithFields(l.baseFields...) + } + + l.captureCaller(entry, 0) + + if l.cfg != nil { + if level >= l.cfg.ConsoleLevel && l.consoleWriter != nil { + if err := l.consoleWriter.WriteContinue(entry, lines); err != nil { //nolint:staticcheck // Silently drop on write errors to prevent logging from blocking + } + } + + if level >= l.cfg.StructuredLevel && l.jsonWriter != nil { + if err := l.jsonWriter.WriteContinue(entry, lines); err != nil { //nolint:staticcheck // Silently drop on write errors to prevent logging from blocking + } + } + + entry.Write() + + l.writers.mu.RLock() + if l.writers.mw != nil { + // Additional writers receive a typed field so they can render the lines + // if they choose. Writers that don't understand FieldTypeContinuationLines + // emit "[N lines]" as a fallback hint (see writeFormatted). + entry.WithFields(continuationLinesField(lines)) + _ = l.writers.mw.Write(entry) + } + l.writers.mu.RUnlock() + return + } + + entry.Write() +} diff --git a/continuation_test.go b/continuation_test.go new file mode 100644 index 0000000..93d9cdf --- /dev/null +++ b/continuation_test.go @@ -0,0 +1,416 @@ +package velocity + +import ( + "bytes" + "io" + "strings" + "testing" +) + +// --- stripOSC8 --- + +func TestStripOSC8_NoSequence(t *testing.T) { + t.Parallel() + + input := "http://localhost:8080" + got := stripOSC8(input) + if got != input { + t.Errorf("no-op string modified: got %q, want %q", got, input) + } +} + +func TestStripOSC8_SingleSequence(t *testing.T) { + t.Parallel() + + // OSC 8: \x1b]8;;\x07\x1b]8;;\x07 + input := "\x1b]8;;http://localhost:8080\x07click here\x1b]8;;\x07" + got := stripOSC8(input) + want := "click here" + if got != want { + t.Errorf("stripOSC8() = %q, want %q", got, want) + } +} + +func TestStripOSC8_TextAroundSequence(t *testing.T) { + t.Parallel() + + input := "Visit " + "\x1b]8;;http://example.com\x07example.com\x1b]8;;\x07" + " for details" + got := stripOSC8(input) + want := "Visit example.com for details" + if got != want { + t.Errorf("stripOSC8() = %q, want %q", got, want) + } +} + +func TestStripOSC8_MultipleSequences(t *testing.T) { + t.Parallel() + + a := "\x1b]8;;http://a.com\x07link-a\x1b]8;;\x07" + b := "\x1b]8;;http://b.com\x07link-b\x1b]8;;\x07" + input := a + " and " + b + got := stripOSC8(input) + want := "link-a and link-b" + if got != want { + t.Errorf("stripOSC8() = %q, want %q", got, want) + } +} + +func TestStripOSC8_Empty(t *testing.T) { + t.Parallel() + + if got := stripOSC8(""); got != "" { + t.Errorf("empty input gave %q", got) + } +} + +// --- ContinuationBlock.Render / String --- + +func TestContinuationBlockNilReceiver(t *testing.T) { + t.Parallel() + + var c *ContinuationBlock + if s := c.String(); s != "" { + t.Errorf("nil.String() = %q, want empty", s) + } + var buf bytes.Buffer + if err := c.Render(&buf); err != nil { + t.Errorf("nil.Render() error: %v", err) + } + if buf.Len() != 0 { + t.Errorf("nil.Render() wrote bytes: %q", buf.String()) + } +} + +func TestContinuationBlockNoLines(t *testing.T) { + t.Parallel() + + c := NewContinuationBlock("HTTP server listening", nil, ThemeMono) + out := c.String() + + // Only the header line should be present. + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(lines) != 1 { + t.Errorf("expected 1 line for no-lines block, got %d: %q", len(lines), out) + } + if !strings.Contains(out, "HTTP server listening") { + t.Errorf("expected message in output, got: %q", out) + } + // No glyph should appear. + if strings.Contains(out, continuationGlyph) { + t.Errorf("expected no glyph for empty lines, got: %q", out) + } +} + +func TestContinuationBlockMultipleLines(t *testing.T) { + t.Parallel() + + lines := []string{ + "Available at http://localhost:8080", + "Press Ctrl+C to stop", + } + c := NewContinuationBlock("HTTP server listening", lines, ThemeMono) + out := c.String() + + // Header + 2 continuation lines. + got := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(got) != 3 { + t.Errorf("expected 3 lines, got %d: %q", len(got), out) + } + + // Each continuation line must start with the glyph+space. + for _, line := range lines { + want := continuationGlyphSep + line + if !strings.Contains(out, want) { + t.Errorf("expected %q in output, got: %q", want, out) + } + } +} + +func TestContinuationBlockEmptyLinePreserved(t *testing.T) { + t.Parallel() + + c := NewContinuationBlock("msg", []string{"first", "", "last"}, ThemeMono) + out := c.String() + + // Empty string still gets the glyph prefix (as an empty continuation). + if !strings.Contains(out, continuationGlyphSep+"\n") { + t.Errorf("expected empty continuation line with glyph, got: %q", out) + } +} + +// TestContinuationBlockTTYUsesGlyph exercises the TTY path via renderContinuationTTY +// directly, since bytes.Buffer is not a terminal and c.String() uses the plain path. +func TestContinuationBlockTTYUsesGlyph(t *testing.T) { + t.Parallel() + + c := NewContinuationBlock("msg", []string{"line one"}, ThemeMono) + + var buf bytes.Buffer + renderContinuationTTY(&buf, c.msg, c.lines, c.theme) + out := buf.String() + + if !strings.Contains(out, continuationGlyph) { + t.Errorf("TTY render missing glyph: %q", out) + } + if !strings.Contains(out, "line one") { + t.Errorf("TTY render missing line text: %q", out) + } +} + +// --- TTY indent alignment --- + +// TestLoggerContinueTTYIndent verifies that continuation lines land at the +// message column using tmpl.CachedMessageIndentStr(). The column is determined +// by: RFC3339 timestamp + space + level badge (6 chars "[INFO]") + space. +// We assert that each continuation line has at least that many leading spaces. +func TestLoggerContinueTTYIndent(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(&buf), + WithColour(false), + ) + + log.Continue(LevelInfo, "HTTP server listening", + "Available at http://localhost:8080", + "Press Ctrl+C to stop", + ) + + out := buf.String() + for line := range strings.SplitSeq(out, "\n") { + if strings.Contains(line, continuationGlyph) { + // The glyph line must have leading whitespace (the message-column indent). + if !strings.HasPrefix(line, " ") { + t.Errorf("continuation line missing indent: %q", line) + } + } + } +} + +// --- JSON output --- + +func TestLoggerContinueJSON(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + + log.Continue(LevelInfo, "HTTP server listening", + "Available at http://localhost:8080", + "Press Ctrl+C to stop", + ) + + out := buf.String() + // Single JSON line. + jsonLines := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(jsonLines) != 1 { + t.Errorf("expected one JSON line, got %d: %q", len(jsonLines), out) + } + + if !strings.Contains(out, `"continuation":[`) { + t.Errorf("expected continuation array in JSON: %q", out) + } + if !strings.Contains(out, `"message":"HTTP server listening"`) { + t.Errorf("expected message field in JSON: %q", out) + } + if !strings.Contains(out, `"Available at http://localhost:8080"`) { + t.Errorf("expected first continuation line in JSON: %q", out) + } +} + +// --- OSC 8 sequences stripped from JSON --- + +func TestLoggerContinueJSONStripsOSC8(t *testing.T) { + t.Parallel() + + osc8Link := "\x1b]8;;http://localhost:8080\x07http://localhost:8080\x1b]8;;\x07" + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + + log.Continue(LevelInfo, "Server ready", osc8Link) + + out := buf.String() + // Raw ESC byte must not appear in JSON output. + if strings.ContainsRune(out, '\x1b') { + t.Errorf("OSC 8 escape leaked into JSON: %q", out) + } + // The plain text of the link must be preserved. + if !strings.Contains(out, "http://localhost:8080") { + t.Errorf("expected plain URL text in JSON: %q", out) + } +} + +// --- OSC 8 preserved in console output --- + +func TestLoggerContinueConsolePreservesOSC8(t *testing.T) { + t.Parallel() + + osc8Link := "\x1b]8;;http://localhost:8080\x07http://localhost:8080\x1b]8;;\x07" + + var buf safeBuffer + log := New( + WithConsoleOutput(&buf), + WithColour(false), + ) + + log.Continue(LevelInfo, "Server ready", osc8Link) + + out := buf.String() + // The OSC 8 sequence must be passed through unchanged for the terminal to render. + if !strings.Contains(out, "\x1b]8;;") { + t.Errorf("OSC 8 sequence missing from console output: %q", out) + } +} + +// --- Logger.Continue: nil logger --- + +func TestLoggerContinueNil(t *testing.T) { + t.Parallel() + + // Must not panic. + var log *Logger + log.Continue(LevelInfo, "should not panic", "line one") +} + +// --- Logger.Continue: level filtering --- + +func TestLoggerContinueLevelFilter(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithStructuredOutput(&buf), + WithLevel(LevelError), + WithStructuredLevel(LevelError), + ) + + log.Continue(LevelInfo, "this should be filtered", "line one") + + if out := buf.String(); out != "" { + t.Errorf("expected no output when filtered, got: %q", out) + } +} + +// --- Logger.Continue: routes through console and JSON --- + +func TestLoggerContinueBothWriters(t *testing.T) { + t.Parallel() + + var consoleBuf, jsonBuf safeBuffer + log := New( + WithConsoleOutput(&consoleBuf), + WithColour(false), + WithStructuredOutput(&jsonBuf), + ) + + log.Continue(LevelInfo, "startup complete", + "listening on :8080", + "metrics on :9090", + ) + + // Console should have the header and both continuation lines. + console := consoleBuf.String() + if !strings.Contains(console, "startup complete") { + t.Errorf("console missing message: %q", console) + } + if !strings.Contains(console, "listening on :8080") { + t.Errorf("console missing first line: %q", console) + } + + // JSON should have the continuation array. + json := jsonBuf.String() + if !strings.Contains(json, `"continuation":[`) { + t.Errorf("JSON missing continuation array: %q", json) + } + if !strings.Contains(json, `"metrics on :9090"`) { + t.Errorf("JSON missing second continuation line: %q", json) + } +} + +// --- continuationLinesField / continuationLinesFromField roundtrip --- + +func TestContinuationLinesFieldRoundtrip(t *testing.T) { + t.Parallel() + + lines := []string{"alpha", "beta", ""} + + f := continuationLinesField(lines) + if f.Type != FieldTypeContinuationLines { + t.Fatalf("expected FieldTypeContinuationLines, got %v", f.Type) + } + + got := continuationLinesFromField(f) + if len(got) != len(lines) { + t.Fatalf("roundtrip length mismatch: got %d, want %d", len(got), len(lines)) + } + for i, line := range lines { + if got[i] != line { + t.Errorf("lines[%d] = %q, want %q", i, got[i], line) + } + } +} + +func TestContinuationLinesFromFieldWrongType(t *testing.T) { + t.Parallel() + + f := String("key", "val") + if got := continuationLinesFromField(f); got != nil { + t.Errorf("expected nil for non-ContinuationLines field, got %v", got) + } +} + +// --- Render parity: TTY and non-TTY both have all lines --- + +func TestContinuationBlockRenderParity(t *testing.T) { + t.Parallel() + + lines := []string{"alpha", "beta", "gamma"} + + // Both render paths must include all line text. + c := NewContinuationBlock("msg", lines, ThemeMono) + + // Plain path (bytes.Buffer is not a terminal). + plainOut := c.String() + for _, line := range lines { + if !strings.Contains(plainOut, line) { + t.Errorf("plain: missing line %q in output: %q", line, plainOut) + } + } + + // TTY path via internal helper. + var ttyBuf bytes.Buffer + renderContinuationTTY(&ttyBuf, c.msg, c.lines, c.theme) + ttyOut := ttyBuf.String() + for _, line := range lines { + if !strings.Contains(ttyOut, line) { + t.Errorf("tty: missing line %q in output: %q", line, ttyOut) + } + } +} + +// --- JSON: empty continuation array --- + +func TestLoggerContinueJSONEmpty(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + + log.Continue(LevelInfo, "no lines") + + out := buf.String() + if !strings.Contains(out, `"continuation":[]`) { + t.Errorf("expected empty continuation array in JSON: %q", out) + } +} diff --git a/doc.go b/doc.go index 4376bcf..6eb4163 100644 --- a/doc.go +++ b/doc.go @@ -1,31 +1,24 @@ // Package velocity is a high-performance structured logging library for Go CLI applications. // // Velocity provides rich terminal output with themed colours, structured fields, -// tree displays, tables, progress indicators, and JSON output — all with -// zero-allocation field encoding on hot paths. +// tables, status items, group blocks, continuation output, hyperlinks, and JSON — +// all with zero-allocation field encoding on hot paths. // // Quick start: // -// log := velocity.New(os.Stdout) +// log := velocity.New(velocity.WithDevelopment()) // log.Info("server started", velocity.String("addr", ":8080")) // -// For more control, use the builder or functional options: +// Preset options cover the common scenarios: // -// log := velocity.NewWithOptions( -// velocity.WithLevel(velocity.LevelDebug), -// velocity.WithTheme(velocity.ThemeDracula), -// ) +// 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; cleans up on exit +// log := velocity.New(velocity.WithNop()) // discards all output // -// Velocity includes five presets for common scenarios: -// -// - PresetDevelopment: verbose, coloured console output -// - PresetProduction: structured JSON, info level and above -// - PresetContainer: JSON with container-friendly defaults -// - PresetTesting: minimal output for test harnesses -// - PresetHighPerformance: sampling and ring-buffer batching -// -// Rich terminal output from velocity/pretty can be mixed with log lines via -// the Renderable interface. Logger.Render writes indented output aligned with -// the message column; Logger.RenderRaw writes flush-left. Logger.Newline inserts -// a blank line under the same mutex as log calls to prevent interleaving. +// Renderables (Box, Table, Tree, Banner, KeyValue, SystemInfo) live in the root +// package and are rendered via Logger convenience methods or the standalone Pretty facade. +// Stateful animated types (Spinner, ProgressBar) live in velocity/live. +// The slog bridge lives in velocity/slogbridge. package velocity diff --git a/docs/bench-baseline.txt b/docs/bench-baseline.txt new file mode 100644 index 0000000..2376d22 --- /dev/null +++ b/docs/bench-baseline.txt @@ -0,0 +1,5652 @@ +2026-05-09 18:27:11 [!DBG] Debug message +2026-05-09 18:27:11 [INFO] Info message +2026-05-09 18:27:11 [WARN] Warning message +2026-05-09 18:27:11 [ERR!] Error message +2026-05-09 18:27:11 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:11+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:11+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:11+10:00 [INFO] endpoint: [REDACTED] +2026-05-09T18:27:11+10:00 [INFO] request api_key: [REDACTED] +[OK] should not panic +[INFO] should not panic + │ line one +[INFO] should not panic (1) +2026-05-09 18:27:11 [!DBG] Debug message +2026-05-09 18:27:11 [INFO] Info message +2026-05-09 18:27:11 [WARN] Warning message +2026-05-09 18:27:11 [ERR!] Error message +2026-05-09 18:27:11 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +[INFO] should not panic (1) +k: v +s +──────────────────────────────────────── +[OK] should not panic +✅ ok +[INFO] should not panic + │ line one +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +[OK] should not panic +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +[INFO] should not panic + │ line one +[INFO] should not panic (1) +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +[OK] should not panic +k: v +s +──────────────────────────────────────── +[INFO] should not panic + │ line one +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +[INFO] should not panic (1) +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +[OK] should not panic +[INFO] should not panic (1) +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +[INFO] should not panic + │ line one +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +[INFO] should not panic + │ line one +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +[OK] should not panic +[INFO] should not panic (1) +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +[INFO] should not panic (1) +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +[OK] should not panic +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +[INFO] should not panic + │ line one +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:13+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:13+10:00 [ERR!] Error + └ key: value +[OK] should not panic +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:13+10:00 [INFO] endpoint: [REDACTED] +2026-05-09T18:27:13+10:00 [INFO] request api_key: [REDACTED] +[INFO] should not panic (1) +[INFO] should not panic + │ line one +2026-05-09 18:27:13 [!DBG] Debug message +2026-05-09 18:27:13 [INFO] Info message +2026-05-09 18:27:13 [WARN] Warning message +2026-05-09 18:27:13 [ERR!] Error message +2026-05-09 18:27:13 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:13+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:13+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:13+10:00 [INFO] request api_key: [REDACTED] +2026-05-09T18:27:13+10:00 [INFO] endpoint: [REDACTED] +[OK] should not panic +[INFO] should not panic + │ line one +[INFO] should not panic (1) +2026-05-09 18:27:13 [!DBG] Debug message +2026-05-09 18:27:13 [INFO] Info message +2026-05-09 18:27:13 [WARN] Warning message +2026-05-09 18:27:13 [ERR!] Error message +2026-05-09 18:27:13 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:13+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:13+10:00 [ERR!] Error + └ key: value +2026-05-09T18:27:13+10:00 [INFO] request api_key: [REDACTED] +2026-05-09T18:27:13+10:00 [INFO] endpoint: [REDACTED] +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +[INFO] should not panic + │ line one +[INFO] should not panic (1) +[OK] should not panic +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkInfo_NoFields-32 44352126 26.77 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43763676 27.56 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43433410 27.70 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 36962654 28.43 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43525412 28.11 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43598313 28.01 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43312074 27.69 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43858526 27.58 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 44440328 27.82 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43512944 27.86 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_OneString-32 21317983 55.59 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 18802705 57.03 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22641210 54.91 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21374143 55.76 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21960445 58.35 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21733224 54.88 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22151582 54.98 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22434183 54.89 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 19358522 55.31 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21821276 56.60 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_FiveFields-32 36582800 32.99 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 35797065 33.25 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 36184030 33.21 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 35520928 35.13 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 37214494 33.21 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 37248687 33.44 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 33482048 33.70 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 37037036 34.32 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 36373665 34.04 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 36611149 33.29 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 34490786 37.26 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 32056932 35.34 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36603778 34.40 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 31899961 34.68 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 34499908 34.65 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 35640882 34.10 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 35811594 34.44 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 34454343 36.22 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 34881996 34.16 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36438168 33.88 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 551615082 2.155 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 557414636 2.203 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 547067149 2.182 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 560484968 2.156 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 553510130 2.191 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 561179036 2.149 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 554638860 2.163 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 560369544 2.161 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 560591012 2.165 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 554305032 2.159 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 210319280 5.763 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 206193998 5.802 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 209276640 5.775 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 208837189 5.728 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 207866175 5.790 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 205396765 5.784 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 207785155 5.801 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 207468914 5.793 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 208435705 5.759 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 206980878 5.795 ns/op 0 B/op 0 allocs/op +BenchmarkString-32 54639334 21.10 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 58259497 20.93 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 56287026 20.71 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 58986898 21.04 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 56750467 21.27 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 59407999 20.80 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 58112505 21.01 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 53852712 21.20 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 59395648 20.58 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 58062454 21.79 ns/op 16 B/op 1 allocs/op +BenchmarkIntField-32 852088148 1.378 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 872369442 1.382 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 835531244 1.379 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 883288186 1.378 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 871885189 1.374 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 877067319 1.387 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 871457796 1.389 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 868222617 1.403 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 875392284 1.389 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 869902461 1.379 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 873770077 1.379 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 874136698 1.382 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 863889214 1.379 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 866152075 1.381 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 835450387 1.376 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 880402460 1.373 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 880505174 1.380 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 862856197 1.370 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 875139475 1.375 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 868272874 1.384 ns/op 0 B/op 0 allocs/op +BenchmarkAny_String-32 56689609 20.89 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 59335442 20.79 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 59236441 21.20 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 55032996 20.81 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 56926092 21.09 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 59211013 21.25 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 56554154 21.20 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 58305920 21.81 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 59346886 21.51 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 56414875 21.19 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 57824636 21.32 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 58886745 20.88 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 58827277 20.97 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 59714266 21.97 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 58549437 20.89 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 55986231 21.28 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 61034224 21.49 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 57929316 20.86 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 58586598 22.22 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 57656271 21.70 ns/op 16 B/op 1 allocs/op +BenchmarkJSONWriter_FiveFields-32 1862457 650.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1694307 765.2 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1843269 650.0 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1843238 657.7 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1886767 636.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1882567 636.3 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1904815 639.7 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1858472 639.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1854525 643.8 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1882137 637.3 ns/op 0 B/op 0 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2428482 487.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2427080 488.9 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2505165 479.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2469199 482.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2497644 480.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2468355 480.5 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2413554 485.6 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2418259 484.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2465970 482.9 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2490676 480.5 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2583015 460.2 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2624818 460.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2591403 458.1 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2631049 458.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2539778 460.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2612949 462.8 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2631529 459.3 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2622278 461.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2629603 466.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2570008 461.0 ns/op 32 B/op 3 allocs/op +BenchmarkGetEntry_Release-32 83917256 14.62 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 83963643 14.37 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84893246 14.37 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 85243012 14.64 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 85530394 14.44 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84093679 14.32 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84190437 14.34 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84180988 14.36 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 87506288 14.26 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 81971132 14.45 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 60545182 19.84 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 62145897 20.31 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 62211622 20.49 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 60852852 19.98 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 55321972 21.17 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 62149437 19.78 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 51219012 20.73 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 57194059 19.99 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 62852892 19.85 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 57371797 19.71 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23335724 52.71 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22663188 53.14 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22429026 54.00 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22417461 53.62 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22708219 53.51 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23694248 51.75 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22982605 53.15 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22541475 51.81 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22586661 52.73 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22743424 53.42 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6538258 192.0 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6213756 183.1 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6415173 196.1 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6519481 192.4 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6295546 192.6 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6315147 192.3 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6311583 189.7 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6363758 190.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6523380 192.0 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6654584 200.2 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 35800910 33.58 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 36597080 33.09 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 35936643 33.36 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 32266304 33.63 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 35608730 33.33 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 36222368 33.29 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 37429935 33.11 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 36390984 33.08 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 36927734 33.85 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 35704615 33.42 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23384336 52.61 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22939638 52.53 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23385931 51.87 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22945558 52.21 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23578788 52.16 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 24091741 50.93 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23077587 52.26 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22939111 53.02 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23066143 52.64 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23386660 52.67 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 34769506 35.26 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 37374910 33.21 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 37206532 33.39 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36957304 33.22 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 35696649 33.78 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36947518 33.36 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36409202 33.24 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36579231 33.24 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36236368 33.33 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36918759 33.25 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 73901181 16.50 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 73894809 16.44 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 74135853 16.46 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 72731679 17.16 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 73475385 16.54 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 72013250 16.51 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 74219763 16.26 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 72685863 18.01 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 70488721 16.35 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 69237692 16.45 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 629671704 1.919 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 630240846 1.927 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 625791691 1.933 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 623104399 1.944 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 625711420 1.920 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 630268982 1.900 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 640332289 1.910 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 638958242 1.931 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 628717290 1.913 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 621646024 1.928 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 620366956 1.966 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 611302369 1.974 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 580688337 1.968 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 602937207 1.954 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 604020662 1.968 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 617695111 1.970 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 622985679 1.949 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 617402095 1.979 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 619888182 1.998 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 607536182 1.970 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 847560578 1.418 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 844400253 1.418 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 850312418 1.419 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 841520037 1.416 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 850153381 1.424 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 852481003 1.412 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 850609567 1.420 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 851141096 1.414 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 834704799 1.434 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 847786323 1.425 ns/op 0 B/op 0 allocs/op +BenchmarkWithComponent_Equivalent-32 7619928 154.4 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7324839 155.5 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7965016 163.2 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7155694 152.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7777762 160.0 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7513566 157.6 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7619042 167.8 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7270620 169.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7069177 174.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7791964 143.0 ns/op 192 B/op 3 allocs/op +BenchmarkSecureScan_NoMatch-32 35330174 34.38 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 35825600 34.94 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 33160804 36.36 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 35489517 34.72 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 35815869 36.02 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34867502 34.55 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 36154705 35.35 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34187936 34.54 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34443366 34.13 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34520552 33.85 ns/op 0 B/op 0 allocs/op +BenchmarkSecureField_UntrustedWriter-32 5313596 250.2 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 5303074 251.4 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4986844 256.7 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4602740 253.6 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4832725 251.5 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4492659 274.9 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 5230868 252.9 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 5181300 250.0 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4428880 259.9 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4350729 260.1 ns/op 32 B/op 1 allocs/op +BenchmarkEntry_GetRelease-32 79002981 14.34 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 81618771 14.69 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 84739179 14.32 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 84479674 15.69 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 79974940 14.39 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 79328877 14.32 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 85278148 14.61 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 82279848 14.36 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 85323625 14.46 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 82018754 14.58 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 72449330 17.09 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 71870058 17.12 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 72815533 17.91 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 69967173 16.95 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 68717502 17.21 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 72944780 17.12 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 73781226 17.32 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 69303270 17.04 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 68361655 17.00 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 71581960 17.06 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7970529 155.1 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7801381 155.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7683249 155.5 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7628205 155.6 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7640308 154.8 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7653422 155.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7721793 154.4 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7635996 154.7 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7657743 155.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7532350 155.3 ns/op 88 B/op 4 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1250840 957.2 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1257446 945.6 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1264429 945.6 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1234862 961.4 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1237035 965.7 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1246051 958.8 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1277065 951.2 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1244830 955.0 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1286775 941.8 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1263585 958.7 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_New_Table-32 1318341 908.1 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1319314 904.5 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1325317 905.1 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1346742 904.9 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1280238 927.3 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1291928 923.4 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1300635 917.3 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1332217 899.4 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1274778 938.3 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1364247 896.1 ns/op 288 B/op 9 allocs/op +PASS +ok github.com/tensorfoundrylabs/velocity 407.770s +? github.com/tensorfoundrylabs/velocity/examples/basic [no test files] +? github.com/tensorfoundrylabs/velocity/examples/continuation [no test files] +? github.com/tensorfoundrylabs/velocity/examples/custom-theme [no test files] +? github.com/tensorfoundrylabs/velocity/examples/groups [no test files] +? github.com/tensorfoundrylabs/velocity/examples/hyperlinks [no test files] +? github.com/tensorfoundrylabs/velocity/examples/json-logging [no test files] +? github.com/tensorfoundrylabs/velocity/examples/multi-writer [no test files] +? github.com/tensorfoundrylabs/velocity/examples/notify [no test files] +? github.com/tensorfoundrylabs/velocity/examples/pretty-output [no test files] +? github.com/tensorfoundrylabs/velocity/examples/progress [no test files] +? github.com/tensorfoundrylabs/velocity/examples/ring-buffer [no test files] +? github.com/tensorfoundrylabs/velocity/examples/sampling [no test files] +? github.com/tensorfoundrylabs/velocity/examples/secure [no test files] +? github.com/tensorfoundrylabs/velocity/examples/slog-bridge [no test files] +? github.com/tensorfoundrylabs/velocity/examples/status-items [no test files] +? github.com/tensorfoundrylabs/velocity/examples/tables [no test files] +? github.com/tensorfoundrylabs/velocity/examples/terminal-velocity [no test files] +? github.com/tensorfoundrylabs/velocity/examples/themes [no test files] +PASS +ok github.com/tensorfoundrylabs/velocity/live 0.153s +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity/slogbridge +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkSlogHandler_Info-32 14779926 81.92 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 14913148 87.48 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 15207043 97.02 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 12420199 102.6 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 10408714 100.3 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 12626793 98.79 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 11285216 98.15 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 12077574 99.89 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 9972384 102.4 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 11869494 100.3 ns/op 144 B/op 3 allocs/op +PASS +ok github.com/tensorfoundrylabs/velocity/slogbridge 13.264s diff --git a/docs/bench-comparative-v2.0.0.txt b/docs/bench-comparative-v2.0.0.txt new file mode 100644 index 0000000..1f3b32d --- /dev/null +++ b/docs/bench-comparative-v2.0.0.txt @@ -0,0 +1,201 @@ +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity/benchmarks +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkLibraries/velocity/Info_NoFields-32 85594147 29.85 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/Info_NoFields-32 80456725 29.40 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/Info_NoFields-32 86362621 29.19 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/Info_ThreeFields-32 34839208 63.27 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/Info_ThreeFields-32 36943423 64.32 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/Info_ThreeFields-32 36561844 63.26 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/With_TwoFields-32 14805208 157.1 ns/op 240 B/op 4 allocs/op +BenchmarkLibraries/velocity/With_TwoFields-32 15579883 154.1 ns/op 240 B/op 4 allocs/op +BenchmarkLibraries/velocity/With_TwoFields-32 15539491 157.5 ns/op 240 B/op 4 allocs/op +BenchmarkLibraries/velocity/Info_TenFields-32 14773030 162.7 ns/op 96 B/op 6 allocs/op +BenchmarkLibraries/velocity/Info_TenFields-32 14853267 166.6 ns/op 96 B/op 6 allocs/op +BenchmarkLibraries/velocity/Info_TenFields-32 15082424 165.7 ns/op 96 B/op 6 allocs/op +BenchmarkLibraries/velocity/Accumulated_10Fields-32 66499862 36.95 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/Accumulated_10Fields-32 64296448 36.25 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/Accumulated_10Fields-32 67570230 36.26 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/MixedFieldTypes-32 17910687 137.5 ns/op 72 B/op 4 allocs/op +BenchmarkLibraries/velocity/MixedFieldTypes-32 18080661 137.2 ns/op 72 B/op 4 allocs/op +BenchmarkLibraries/velocity/MixedFieldTypes-32 17889300 137.5 ns/op 72 B/op 4 allocs/op +BenchmarkLibraries/velocity/ErrorField-32 35768150 67.72 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/ErrorField-32 35847111 67.27 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/ErrorField-32 34443118 67.36 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/LargeMessage-32 67576128 36.11 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/LargeMessage-32 62650262 37.03 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/LargeMessage-32 66627244 37.10 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/velocity/Parallel_4-32 41544054 58.88 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/Parallel_4-32 41392797 59.55 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/Parallel_4-32 41421016 59.07 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/Parallel_16-32 41088009 56.25 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/Parallel_16-32 42436866 56.03 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/velocity/Parallel_16-32 38637823 56.39 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/zap/Info_NoFields-32 10592943 225.7 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/Info_NoFields-32 10737481 221.8 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/Info_NoFields-32 10986098 220.1 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/Info_ThreeFields-32 5262182 457.8 ns/op 193 B/op 1 allocs/op +BenchmarkLibraries/zap/Info_ThreeFields-32 5288566 463.5 ns/op 193 B/op 1 allocs/op +BenchmarkLibraries/zap/Info_ThreeFields-32 5196961 476.9 ns/op 193 B/op 1 allocs/op +BenchmarkLibraries/zap/With_TwoFields-32 1480431 1554 ns/op 1432 B/op 6 allocs/op +BenchmarkLibraries/zap/With_TwoFields-32 1716304 1395 ns/op 1432 B/op 6 allocs/op +BenchmarkLibraries/zap/With_TwoFields-32 1772455 1369 ns/op 1432 B/op 6 allocs/op +BenchmarkLibraries/zap/Info_TenFields-32 2567792 1019 ns/op 708 B/op 1 allocs/op +BenchmarkLibraries/zap/Info_TenFields-32 2184574 1084 ns/op 708 B/op 1 allocs/op +BenchmarkLibraries/zap/Info_TenFields-32 2255178 1028 ns/op 708 B/op 1 allocs/op +BenchmarkLibraries/zap/Accumulated_10Fields-32 10894990 223.2 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/Accumulated_10Fields-32 10843056 225.1 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/Accumulated_10Fields-32 10646192 222.4 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/MixedFieldTypes-32 2635112 934.8 ns/op 515 B/op 1 allocs/op +BenchmarkLibraries/zap/MixedFieldTypes-32 2716516 884.3 ns/op 515 B/op 1 allocs/op +BenchmarkLibraries/zap/MixedFieldTypes-32 2759426 880.6 ns/op 515 B/op 1 allocs/op +BenchmarkLibraries/zap/ErrorField-32 7007662 328.3 ns/op 64 B/op 1 allocs/op +BenchmarkLibraries/zap/ErrorField-32 7491758 335.3 ns/op 64 B/op 1 allocs/op +BenchmarkLibraries/zap/ErrorField-32 7250038 339.0 ns/op 64 B/op 1 allocs/op +BenchmarkLibraries/zap/LargeMessage-32 2473371 936.2 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/LargeMessage-32 2355520 960.8 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/LargeMessage-32 2331948 1030 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zap/Parallel_4-32 16484682 123.4 ns/op 194 B/op 1 allocs/op +BenchmarkLibraries/zap/Parallel_4-32 18956971 126.2 ns/op 194 B/op 1 allocs/op +BenchmarkLibraries/zap/Parallel_4-32 18954546 137.1 ns/op 194 B/op 1 allocs/op +BenchmarkLibraries/zap/Parallel_16-32 19922286 116.2 ns/op 193 B/op 1 allocs/op +BenchmarkLibraries/zap/Parallel_16-32 21007814 116.4 ns/op 193 B/op 1 allocs/op +BenchmarkLibraries/zap/Parallel_16-32 19250481 117.1 ns/op 193 B/op 1 allocs/op +BenchmarkLibraries/zerolog/Info_NoFields-32 36039174 67.50 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Info_NoFields-32 37172647 65.34 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Info_NoFields-32 36714200 64.03 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Info_ThreeFields-32 15663219 154.3 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Info_ThreeFields-32 15367522 155.9 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Info_ThreeFields-32 15101528 155.6 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/With_TwoFields-32 5173915 480.2 ns/op 625 B/op 2 allocs/op +BenchmarkLibraries/zerolog/With_TwoFields-32 5265963 502.9 ns/op 625 B/op 2 allocs/op +BenchmarkLibraries/zerolog/With_TwoFields-32 4643358 501.0 ns/op 625 B/op 2 allocs/op +BenchmarkLibraries/zerolog/Info_TenFields-32 11446995 211.4 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Info_TenFields-32 10785396 219.3 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Info_TenFields-32 11043921 217.1 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Accumulated_10Fields-32 31241701 71.80 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Accumulated_10Fields-32 34748112 71.95 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Accumulated_10Fields-32 34153831 72.96 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/MixedFieldTypes-32 4555592 529.5 ns/op 112 B/op 2 allocs/op +BenchmarkLibraries/zerolog/MixedFieldTypes-32 4606450 518.8 ns/op 112 B/op 2 allocs/op +BenchmarkLibraries/zerolog/MixedFieldTypes-32 4634920 515.8 ns/op 112 B/op 2 allocs/op +BenchmarkLibraries/zerolog/ErrorField-32 26316770 93.85 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/ErrorField-32 26425809 93.49 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/ErrorField-32 25515356 93.73 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/LargeMessage-32 4584663 523.5 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/LargeMessage-32 4574478 527.2 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/LargeMessage-32 4437867 531.3 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Parallel_4-32 225983422 10.87 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Parallel_4-32 230511655 10.60 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Parallel_4-32 223023039 10.48 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Parallel_16-32 231278776 10.37 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Parallel_16-32 235626674 10.34 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/zerolog/Parallel_16-32 225649206 10.35 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/Info_NoFields-32 5861346 406.8 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/Info_NoFields-32 5800585 415.9 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/Info_NoFields-32 5951456 419.2 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/Info_ThreeFields-32 2363150 1025 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/slog/Info_ThreeFields-32 2316096 1041 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/slog/Info_ThreeFields-32 2305591 1037 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/slog/With_TwoFields-32 2046997 1182 ns/op 513 B/op 11 allocs/op +BenchmarkLibraries/slog/With_TwoFields-32 2002716 1222 ns/op 513 B/op 11 allocs/op +BenchmarkLibraries/slog/With_TwoFields-32 1995553 1211 ns/op 513 B/op 11 allocs/op +BenchmarkLibraries/slog/Info_TenFields-32 1318846 1822 ns/op 701 B/op 12 allocs/op +BenchmarkLibraries/slog/Info_TenFields-32 1307176 1839 ns/op 701 B/op 12 allocs/op +BenchmarkLibraries/slog/Info_TenFields-32 1334419 1795 ns/op 701 B/op 12 allocs/op +BenchmarkLibraries/slog/Accumulated_10Fields-32 5803212 413.5 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/Accumulated_10Fields-32 5658340 422.1 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/Accumulated_10Fields-32 5827998 416.2 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/MixedFieldTypes-32 1387392 1740 ns/op 523 B/op 10 allocs/op +BenchmarkLibraries/slog/MixedFieldTypes-32 1403094 1708 ns/op 523 B/op 10 allocs/op +BenchmarkLibraries/slog/MixedFieldTypes-32 1384588 1721 ns/op 523 B/op 10 allocs/op +BenchmarkLibraries/slog/ErrorField-32 3927990 612.8 ns/op 48 B/op 1 allocs/op +BenchmarkLibraries/slog/ErrorField-32 3851331 739.1 ns/op 48 B/op 1 allocs/op +BenchmarkLibraries/slog/ErrorField-32 1849608 1281 ns/op 48 B/op 1 allocs/op +BenchmarkLibraries/slog/LargeMessage-32 1000000 2275 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/LargeMessage-32 1206583 1973 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/LargeMessage-32 1231752 1998 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/slog/Parallel_4-32 9763510 228.7 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/slog/Parallel_4-32 10678868 191.5 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/slog/Parallel_4-32 12482926 189.9 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/slog/Parallel_16-32 13349374 574.6 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/slog/Parallel_16-32 13131882 179.4 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/slog/Parallel_16-32 13677972 182.9 ns/op 153 B/op 4 allocs/op +BenchmarkLibraries/charmbracelet/Info_NoFields-32 742884558 3.219 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Info_NoFields-32 753398296 3.187 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Info_NoFields-32 756908922 3.207 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Info_ThreeFields-32 603850299 3.915 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Info_ThreeFields-32 611957028 3.924 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Info_ThreeFields-32 606543236 3.889 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/With_TwoFields-32 1000000 2148 ns/op 4424 B/op 5 allocs/op +BenchmarkLibraries/charmbracelet/With_TwoFields-32 1000000 2345 ns/op 4424 B/op 5 allocs/op +BenchmarkLibraries/charmbracelet/With_TwoFields-32 1000000 2246 ns/op 4424 B/op 5 allocs/op +BenchmarkLibraries/charmbracelet/Info_TenFields-32 260717725 9.277 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Info_TenFields-32 261404762 9.291 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Info_TenFields-32 253054738 9.509 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Accumulated_10Fields-32 743197420 3.232 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Accumulated_10Fields-32 746786019 3.165 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Accumulated_10Fields-32 757820948 3.176 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/MixedFieldTypes-32 74857303 33.10 ns/op 24 B/op 1 allocs/op +BenchmarkLibraries/charmbracelet/MixedFieldTypes-32 70872976 33.56 ns/op 24 B/op 1 allocs/op +BenchmarkLibraries/charmbracelet/MixedFieldTypes-32 71105094 33.93 ns/op 24 B/op 1 allocs/op +BenchmarkLibraries/charmbracelet/ErrorField-32 685899674 3.461 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/ErrorField-32 681079249 3.480 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/ErrorField-32 682537172 3.495 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/LargeMessage-32 100000000 23.47 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/charmbracelet/LargeMessage-32 100000000 24.14 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/charmbracelet/LargeMessage-32 70761808 34.14 ns/op 16 B/op 1 allocs/op +BenchmarkLibraries/charmbracelet/Parallel_4-32 1000000000 0.3122 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Parallel_4-32 1000000000 0.2872 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Parallel_4-32 1000000000 0.4352 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Parallel_16-32 1000000000 0.3740 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Parallel_16-32 1000000000 0.3106 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/charmbracelet/Parallel_16-32 1000000000 0.2945 ns/op 0 B/op 0 allocs/op +BenchmarkLibraries/pterm/Info_NoFields-32 162535 14880 ns/op 1971 B/op 65 allocs/op +BenchmarkLibraries/pterm/Info_NoFields-32 155116 13191 ns/op 1965 B/op 65 allocs/op +BenchmarkLibraries/pterm/Info_NoFields-32 275943 8462 ns/op 1966 B/op 65 allocs/op +BenchmarkLibraries/pterm/Info_ThreeFields-32 148449 16401 ns/op 5872 B/op 144 allocs/op +BenchmarkLibraries/pterm/Info_ThreeFields-32 152223 16356 ns/op 5882 B/op 144 allocs/op +BenchmarkLibraries/pterm/Info_ThreeFields-32 146930 16331 ns/op 5884 B/op 144 allocs/op +BenchmarkLibraries/pterm/With_TwoFields-32 294430 8307 ns/op 1966 B/op 65 allocs/op +BenchmarkLibraries/pterm/With_TwoFields-32 288728 8514 ns/op 1965 B/op 65 allocs/op +BenchmarkLibraries/pterm/With_TwoFields-32 274354 8333 ns/op 1968 B/op 65 allocs/op +BenchmarkLibraries/pterm/Info_TenFields-32 75117 32715 ns/op 15318 B/op 311 allocs/op +BenchmarkLibraries/pterm/Info_TenFields-32 75734 32021 ns/op 15342 B/op 311 allocs/op +BenchmarkLibraries/pterm/Info_TenFields-32 71997 32625 ns/op 15323 B/op 311 allocs/op +BenchmarkLibraries/pterm/MixedFieldTypes-32 101408 23568 ns/op 9796 B/op 218 allocs/op +BenchmarkLibraries/pterm/MixedFieldTypes-32 102452 22969 ns/op 9762 B/op 218 allocs/op +BenchmarkLibraries/pterm/MixedFieldTypes-32 106873 22841 ns/op 9759 B/op 218 allocs/op +BenchmarkLibraries/pterm/ErrorField-32 228798 10863 ns/op 2888 B/op 90 allocs/op +BenchmarkLibraries/pterm/ErrorField-32 221966 10877 ns/op 2889 B/op 90 allocs/op +BenchmarkLibraries/pterm/ErrorField-32 230924 10576 ns/op 2886 B/op 90 allocs/op +BenchmarkLibraries/pterm/LargeMessage-32 65600 37295 ns/op 18439 B/op 71 allocs/op +BenchmarkLibraries/pterm/LargeMessage-32 64062 37624 ns/op 18461 B/op 71 allocs/op +BenchmarkLibraries/pterm/LargeMessage-32 64140 37641 ns/op 18436 B/op 71 allocs/op +BenchmarkLibraries/pterm/Parallel_4-32 132450 17211 ns/op 10877 B/op 145 allocs/op +BenchmarkLibraries/pterm/Parallel_4-32 130560 16247 ns/op 9836 B/op 145 allocs/op +BenchmarkLibraries/pterm/Parallel_4-32 159892 15710 ns/op 9042 B/op 144 allocs/op +BenchmarkLibraries/pterm/Parallel_16-32 153490 15071 ns/op 7943 B/op 144 allocs/op +BenchmarkLibraries/pterm/Parallel_16-32 134539 15991 ns/op 8161 B/op 144 allocs/op +BenchmarkLibraries/pterm/Parallel_16-32 150066 14518 ns/op 7787 B/op 144 allocs/op +BenchmarkDisabledLevel/velocity-32 819742962 3.046 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/velocity-32 776753074 3.027 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/velocity-32 795937006 3.017 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/zap-32 391210732 6.195 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/zap-32 388966951 6.175 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/zap-32 384696885 6.256 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/zerolog-32 322555888 7.394 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/zerolog-32 323848878 7.378 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/zerolog-32 328579158 7.304 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/slog-32 355185636 6.846 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/slog-32 351021655 6.866 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/slog-32 353163201 6.784 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/charmbracelet-32 749317184 3.199 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/charmbracelet-32 746608994 3.183 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/charmbracelet-32 754277696 3.222 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/pterm-32 137797828 17.42 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/pterm-32 137590268 17.49 ns/op 0 B/op 0 allocs/op +BenchmarkDisabledLevel/pterm-32 137238664 17.48 ns/op 0 B/op 0 allocs/op +PASS +ok github.com/tensorfoundrylabs/velocity/benchmarks 471.021s diff --git a/docs/bench-v1-to-v2.txt b/docs/bench-v1-to-v2.txt new file mode 100644 index 0000000..7fcba95 --- /dev/null +++ b/docs/bench-v1-to-v2.txt @@ -0,0 +1,269 @@ +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity +cpu: AMD Ryzen 9 5950X 16-Core Processor +k: + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ sec/op │ +Info_NoFields-32 25.66n ± 2% +Info_OneString-32 52.54n ± 3% +Info_FiveFields-32 35.42n ± 28% +Info_TenFields-32 34.68n ± 53% +Info_Disabled-32 2.046n ± 2% +Info_WithSampler-32 5.320n ± 1% +String-32 19.50n ± 4% +IntField-32 1.332n ± 1% +Float64Field-32 1.308n ± 2% +F_String-32 22.45n ± 1% +F_Int-32 8.640n ± 2% +JSONWriter_FiveFields-32 594.0n ± 2% +ConsoleWriter_FiveFields-32 433.0n ± 1% +ConsoleWriter_NoTemplate-32 407.2n ± 3% +GetEntry_Release-32 13.89n ± 1% +Entry_WithFields-32 18.74n ± 31% +Info_Parallel-32 52.72n ± 2% +JSONWriter_Parallel-32 170.3n ± 5% +Info_TreeMode-32 36.20n ± 7% +Info_TreeMode_Parallel-32 53.48n ± 2% +InfoDetailed_TreeMode-32 36.35n ± 25% +BufferPool_GetPut-32 24.67n ± 13% +Logger_Render-32 2.597n ± 24% +Logger_RenderRaw-32 3.762n ± 32% +Logger_Newline-32 2.591n ± 57% +WithComponent_Equivalent-32 189.8n ± 139% +SecureScan_NoMatch-32 66.65n ± 42% +Entry_GetRelease-32 20.84n ± 6% +Entry_GetReleaseWithRetain-32 17.34n ± 26% +Entry_ConcurrentRetainRelease-32 145.0n ± 1% +geomean 24.98n + + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ B/op │ +Info_NoFields-32 0.000 ± 0% +Info_OneString-32 16.00 ± 0% +Info_FiveFields-32 0.000 ± 0% +Info_TenFields-32 0.000 ± 0% +Info_Disabled-32 0.000 ± 0% +Info_WithSampler-32 0.000 ± 0% +String-32 16.00 ± 0% +IntField-32 0.000 ± 0% +Float64Field-32 0.000 ± 0% +F_String-32 16.00 ± 0% +F_Int-32 0.000 ± 0% +JSONWriter_FiveFields-32 0.000 ± 0% +ConsoleWriter_FiveFields-32 32.00 ± 0% +ConsoleWriter_NoTemplate-32 32.00 ± 0% +GetEntry_Release-32 0.000 ± 0% +Entry_WithFields-32 0.000 ± 0% +Info_Parallel-32 0.000 ± 0% +JSONWriter_Parallel-32 0.000 ± 0% +Info_TreeMode-32 0.000 ± 0% +Info_TreeMode_Parallel-32 0.000 ± 0% +InfoDetailed_TreeMode-32 0.000 ± 0% +BufferPool_GetPut-32 0.000 ± 0% +Logger_Render-32 0.000 ± 0% +Logger_RenderRaw-32 0.000 ± 0% +Logger_Newline-32 0.000 ± 0% +WithComponent_Equivalent-32 192.0 ± 0% +SecureScan_NoMatch-32 0.000 ± 0% +Entry_GetRelease-32 0.000 ± 0% +Entry_GetReleaseWithRetain-32 0.000 ± 0% +Entry_ConcurrentRetainRelease-32 88.00 ± 0% +geomean ¹ +¹ summaries must be >0 to compute geomean + + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ allocs/op │ +Info_NoFields-32 0.000 ± 0% +Info_OneString-32 1.000 ± 0% +Info_FiveFields-32 0.000 ± 0% +Info_TenFields-32 0.000 ± 0% +Info_Disabled-32 0.000 ± 0% +Info_WithSampler-32 0.000 ± 0% +String-32 1.000 ± 0% +IntField-32 0.000 ± 0% +Float64Field-32 0.000 ± 0% +F_String-32 1.000 ± 0% +F_Int-32 0.000 ± 0% +JSONWriter_FiveFields-32 0.000 ± 0% +ConsoleWriter_FiveFields-32 3.000 ± 0% +ConsoleWriter_NoTemplate-32 3.000 ± 0% +GetEntry_Release-32 0.000 ± 0% +Entry_WithFields-32 0.000 ± 0% +Info_Parallel-32 0.000 ± 0% +JSONWriter_Parallel-32 0.000 ± 0% +Info_TreeMode-32 0.000 ± 0% +Info_TreeMode_Parallel-32 0.000 ± 0% +InfoDetailed_TreeMode-32 0.000 ± 0% +BufferPool_GetPut-32 0.000 ± 0% +Logger_Render-32 0.000 ± 0% +Logger_RenderRaw-32 0.000 ± 0% +Logger_Newline-32 0.000 ± 0% +WithComponent_Equivalent-32 3.000 ± 0% +SecureScan_NoMatch-32 0.000 ± 0% +Entry_GetRelease-32 0.000 ± 0% +Entry_GetReleaseWithRetain-32 0.000 ± 0% +Entry_ConcurrentRetainRelease-32 4.000 ± 0% +geomean ¹ +¹ summaries must be >0 to compute geomean + +k: v + │ D:/projects/tensorfoundry/velocity/docs/bench-v2.0.0.txt │ + │ sec/op │ +Info_NoFields-32 27.76n ± 1% +Info_OneString-32 55.45n ± 3% +Info_FiveFields-32 33.37n ± 3% +Info_TenFields-32 34.55n ± 5% +Info_Disabled-32 2.162n ± 1% +Info_WithSampler-32 5.787n ± 0% +String-32 21.03n ± 1% +IntField-32 1.380n ± 1% +Float64Field-32 1.379n ± 0% +JSONWriter_FiveFields-32 641.9n ± 2% +ConsoleWriter_FiveFields-32 482.6n ± 1% +ConsoleWriter_NoTemplate-32 460.6n ± 0% +GetEntry_Release-32 14.37n ± 2% +Entry_WithFields-32 19.98n ± 4% +Info_Parallel-32 53.15n ± 3% +JSONWriter_Parallel-32 192.2n ± 2% +Info_TreeMode-32 33.34n ± 1% +Info_TreeMode_Parallel-32 52.39n ± 1% +BufferPool_GetPut-32 16.48n ± 4% +Logger_Render-32 1.923n ± 1% +Logger_RenderRaw-32 1.969n ± 1% +Logger_Newline-32 1.418n ± 0% +WithComponent_Equivalent-32 158.8n ± 7% +SecureScan_NoMatch-32 34.64n ± 4% +Entry_GetRelease-32 14.43n ± 2% +Entry_GetReleaseWithRetain-32 17.11n ± 1% +Entry_ConcurrentRetainRelease-32 155.0n ± 0% +Pretty_NewFromLogger_Table-32 956.1n ± 1% +Pretty_New_Table-32 906.6n ± 2% +Any_String-32 21.20n ± 2% +Any_Int-32 21.30n ± 3% +Detailed_TreeMode-32 33.29n ± 1% +SecureField_UntrustedWriter-32 253.2n ± 3% +geomean 31.85n + + │ D:/projects/tensorfoundry/velocity/docs/bench-v2.0.0.txt │ + │ B/op │ +Info_NoFields-32 0.000 ± 0% +Info_OneString-32 16.00 ± 0% +Info_FiveFields-32 0.000 ± 0% +Info_TenFields-32 0.000 ± 0% +Info_Disabled-32 0.000 ± 0% +Info_WithSampler-32 0.000 ± 0% +String-32 16.00 ± 0% +IntField-32 0.000 ± 0% +Float64Field-32 0.000 ± 0% +JSONWriter_FiveFields-32 0.000 ± 0% +ConsoleWriter_FiveFields-32 32.00 ± 0% +ConsoleWriter_NoTemplate-32 32.00 ± 0% +GetEntry_Release-32 0.000 ± 0% +Entry_WithFields-32 0.000 ± 0% +Info_Parallel-32 0.000 ± 0% +JSONWriter_Parallel-32 0.000 ± 0% +Info_TreeMode-32 0.000 ± 0% +Info_TreeMode_Parallel-32 0.000 ± 0% +BufferPool_GetPut-32 0.000 ± 0% +Logger_Render-32 0.000 ± 0% +Logger_RenderRaw-32 0.000 ± 0% +Logger_Newline-32 0.000 ± 0% +WithComponent_Equivalent-32 192.0 ± 0% +SecureScan_NoMatch-32 0.000 ± 0% +Entry_GetRelease-32 0.000 ± 0% +Entry_GetReleaseWithRetain-32 0.000 ± 0% +Entry_ConcurrentRetainRelease-32 88.00 ± 0% +Pretty_NewFromLogger_Table-32 312.0 ± 0% +Pretty_New_Table-32 288.0 ± 0% +Any_String-32 16.00 ± 0% +Any_Int-32 16.00 ± 0% +Detailed_TreeMode-32 0.000 ± 0% +SecureField_UntrustedWriter-32 32.00 ± 0% +geomean ¹ +¹ summaries must be >0 to compute geomean + + │ D:/projects/tensorfoundry/velocity/docs/bench-v2.0.0.txt │ + │ allocs/op │ +Info_NoFields-32 0.000 ± 0% +Info_OneString-32 1.000 ± 0% +Info_FiveFields-32 0.000 ± 0% +Info_TenFields-32 0.000 ± 0% +Info_Disabled-32 0.000 ± 0% +Info_WithSampler-32 0.000 ± 0% +String-32 1.000 ± 0% +IntField-32 0.000 ± 0% +Float64Field-32 0.000 ± 0% +JSONWriter_FiveFields-32 0.000 ± 0% +ConsoleWriter_FiveFields-32 3.000 ± 0% +ConsoleWriter_NoTemplate-32 3.000 ± 0% +GetEntry_Release-32 0.000 ± 0% +Entry_WithFields-32 0.000 ± 0% +Info_Parallel-32 0.000 ± 0% +JSONWriter_Parallel-32 0.000 ± 0% +Info_TreeMode-32 0.000 ± 0% +Info_TreeMode_Parallel-32 0.000 ± 0% +BufferPool_GetPut-32 0.000 ± 0% +Logger_Render-32 0.000 ± 0% +Logger_RenderRaw-32 0.000 ± 0% +Logger_Newline-32 0.000 ± 0% +WithComponent_Equivalent-32 3.000 ± 0% +SecureScan_NoMatch-32 0.000 ± 0% +Entry_GetRelease-32 0.000 ± 0% +Entry_GetReleaseWithRetain-32 0.000 ± 0% +Entry_ConcurrentRetainRelease-32 4.000 ± 0% +Pretty_NewFromLogger_Table-32 10.00 ± 0% +Pretty_New_Table-32 9.000 ± 0% +Any_String-32 1.000 ± 0% +Any_Int-32 1.000 ± 0% +Detailed_TreeMode-32 0.000 ± 0% +SecureField_UntrustedWriter-32 1.000 ± 0% +geomean ¹ +¹ summaries must be >0 to compute geomean + +pkg: github.com/tensorfoundrylabs/velocity/pretty +k: + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ sec/op │ +Pretty_NewFromLogger_Table-32 939.4n ± 6% +Pretty_New_Table-32 929.9n ± 5% +geomean 934.6n + + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ B/op │ +Pretty_NewFromLogger_Table-32 312.0 ± 0% +Pretty_New_Table-32 288.0 ± 0% +geomean 299.8 + + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ allocs/op │ +Pretty_NewFromLogger_Table-32 10.00 ± 0% +Pretty_New_Table-32 9.000 ± 0% +geomean 9.487 + +pkg: github.com/tensorfoundrylabs/velocity/slog + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ sec/op │ +SlogHandler_Info-32 468.3n ± 11% + + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ B/op │ +SlogHandler_Info-32 192.0 ± 0% + + │ D:/projects/tensorfoundry/velocity/docs/bench-baseline.txt │ + │ allocs/op │ +SlogHandler_Info-32 6.000 ± 0% + +pkg: github.com/tensorfoundrylabs/velocity/slogbridge +k: v + │ D:/projects/tensorfoundry/velocity/docs/bench-v2.0.0.txt │ + │ sec/op │ +SlogHandler_Info-32 99.34n ± 12% + + │ D:/projects/tensorfoundry/velocity/docs/bench-v2.0.0.txt │ + │ B/op │ +SlogHandler_Info-32 144.0 ± 0% + + │ D:/projects/tensorfoundry/velocity/docs/bench-v2.0.0.txt │ + │ allocs/op │ +SlogHandler_Info-32 3.000 ± 0% diff --git a/docs/bench-v1.1.3.txt b/docs/bench-v1.1.3.txt new file mode 100644 index 0000000..bf3e960 --- /dev/null +++ b/docs/bench-v1.1.3.txt @@ -0,0 +1,5669 @@ +2026-05-09 13:35:37 [!DBG] Debug message +2026-05-09 13:35:37 [INFO] Info message +2026-05-09 13:35:37 [WARN] Warning message +2026-05-09 13:35:37 [ERR!] Error message +2026-05-09 13:35:37 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:37+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:37+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:37+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:37+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:38+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:38+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:38+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:38+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:38+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:38+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:38+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:38 [!DBG] Debug message +2026-05-09 13:35:38 [INFO] Info message +2026-05-09 13:35:38 [WARN] Warning message +2026-05-09 13:35:38 [ERR!] Error message +2026-05-09 13:35:38 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:38+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:38+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:38+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:38+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:38+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:38+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:38+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:38 [!DBG] Debug message +2026-05-09 13:35:38 [INFO] Info message +2026-05-09 13:35:38 [WARN] Warning message +2026-05-09 13:35:38 [ERR!] Error message +2026-05-09 13:35:38 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:38+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:38+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:38+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:38+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:38+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:38+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:38+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:38 [!DBG] Debug message +2026-05-09 13:35:38 [INFO] Info message +2026-05-09 13:35:38 [WARN] Warning message +2026-05-09 13:35:38 [ERR!] Error message +2026-05-09 13:35:38 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:38+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:38+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:38+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:38+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:38+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 99 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:38+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:38+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:38 [!DBG] Debug message +2026-05-09 13:35:38 [INFO] Info message +2026-05-09 13:35:38 [WARN] Warning message +2026-05-09 13:35:38 [ERR!] Error message +2026-05-09 13:35:38 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:38+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:38+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:38+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:38+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:38+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:38+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:38+10:00 [INFO] Normal log iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:38+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:38+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:38 [!DBG] Debug message +2026-05-09 13:35:38 [INFO] Info message +2026-05-09 13:35:38 [WARN] Warning message +2026-05-09 13:35:38 [ERR!] Error message +2026-05-09 13:35:38 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:38+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:39+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:39+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:39+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:39+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:39+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:39+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:39+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:39 [!DBG] Debug message +2026-05-09 13:35:39 [INFO] Info message +2026-05-09 13:35:39 [WARN] Warning message +2026-05-09 13:35:39 [ERR!] Error message +2026-05-09 13:35:39 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:39+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:39+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:39+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:39+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:39+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 99 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:39+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:39+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:39 [!DBG] Debug message +2026-05-09 13:35:39 [INFO] Info message +2026-05-09 13:35:39 [WARN] Warning message +2026-05-09 13:35:39 [ERR!] Error message +2026-05-09 13:35:39 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:39+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:39+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:39+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:39+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:39+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 99 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:39+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:39+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:39 [!DBG] Debug message +2026-05-09 13:35:39 [INFO] Info message +2026-05-09 13:35:39 [WARN] Warning message +2026-05-09 13:35:39 [ERR!] Error message +2026-05-09 13:35:39 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:39+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:39+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:39+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:39+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:39+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 99 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:39+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:39+10:00 [ERR!] Error + └ key: value +2026-05-09 13:35:39 [!DBG] Debug message +2026-05-09 13:35:39 [INFO] Info message +2026-05-09 13:35:39 [WARN] Warning message +2026-05-09 13:35:39 [ERR!] Error message +2026-05-09 13:35:39 [INFO] Server started addr: :8080 pid: 43532 tls: true +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T13:35:39+10:00 [INFO] Testing theme theme: Nord +2026-05-09T13:35:39+10:00 [INFO] Test message + ├ key1: value1 + ├ key2: 42 + └ key3: true +2026-05-09T13:35:39+10:00 [ERR!] Error occurred + ├ error: connection timeout + └ retry: 3 +2026-05-09T13:35:39+10:00 [WARN] Warning message + ├ warning: high memory usage + └ usage_percent: 89.5 +2026-05-09T13:35:39+10:00 [!DBG] Debug info + ├ module: auth + └ action: token_refresh +2026-05-09T13:35:39+10:00 [INFO] Regular message key: value number: 123 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 1 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 11 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 16 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 20 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 22 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 29 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 32 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 34 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 35 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 36 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 43 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 46 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 51 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 52 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 55 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 58 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 59 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 62 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 73 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 79 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 88 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 89 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 91 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 92 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 93 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 94 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 95 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 96 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Normal log iteration: 99 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T13:35:39+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T13:35:39+10:00 [ERR!] Detailed error + └ iteration: 99 +[INFO] Test +[ERR!] Test +[WARN] Test +[DEBU] Test +2026-05-09T13:35:39+10:00 [WARN] Warn + └ key: value +2026-05-09T13:35:39+10:00 [ERR!] Error + └ key: value +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkInfo_NoFields-32 46534172 26.21 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 44834169 25.54 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 47893101 25.55 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 47042035 25.76 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 47432142 26.51 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 40309712 26.03 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 47289672 25.41 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 49328109 25.11 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 49616095 26.25 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 49570184 24.53 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_OneString-32 22727487 52.90 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 23883115 51.24 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22701775 52.42 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21549517 52.52 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22648089 52.56 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 20984119 53.81 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22910076 51.35 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 23863215 52.40 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21954739 54.19 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22884865 59.49 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_FiveFields-32 36959239 45.18 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 33247443 35.84 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 37506446 35.00 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 35897296 36.38 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 36638760 31.52 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 38001019 31.93 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 38711425 31.75 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 38513259 33.36 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 39490180 38.10 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 19228087 62.44 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 21363069 52.92 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 35616763 56.39 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 31026190 33.60 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36548148 35.82 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36143488 33.98 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 35651788 33.34 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36770337 33.23 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36408871 33.10 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 35662278 35.39 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36021108 39.85 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 523107853 2.123 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 588154855 2.046 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 601319595 2.014 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 587491713 1.992 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 594613101 2.033 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 607726944 2.043 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 558563782 2.079 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 589059399 2.066 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 584171294 2.066 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 574364394 2.047 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 221348836 5.389 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 224370360 5.330 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 223785028 5.431 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 225672885 5.324 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 226707846 5.283 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 225877207 5.299 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 225613315 5.298 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 225907867 5.316 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 226203998 5.303 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 225657268 5.353 ns/op 0 B/op 0 allocs/op +BenchmarkString-32 60292114 19.53 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 62192922 19.34 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 62366820 19.53 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 53367724 19.55 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 63483666 19.31 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 62685771 19.47 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 63066944 20.26 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 60525664 21.10 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 64133396 19.32 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 65353780 19.44 ns/op 16 B/op 1 allocs/op +BenchmarkIntField-32 889087780 1.330 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 914613232 1.326 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 887227472 1.349 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 867276987 1.334 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 929136320 1.321 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 906906775 1.316 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 928331995 1.310 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 902637732 1.340 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 894235089 1.336 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 917647274 1.334 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 880337872 1.372 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 875989137 1.340 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 891518538 1.307 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 928887470 1.312 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 925737349 1.301 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 928487145 1.320 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 929347875 1.285 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 914943078 1.307 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 912793252 1.309 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 940123531 1.296 ns/op 0 B/op 0 allocs/op +BenchmarkF_String-32 51276134 22.53 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 55677219 22.52 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 54432135 22.27 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 56043078 22.46 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 54536034 22.40 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 55209219 22.31 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 55589269 22.48 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 53670142 22.57 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 52740992 22.45 ns/op 16 B/op 1 allocs/op +BenchmarkF_String-32 54411157 22.37 ns/op 16 B/op 1 allocs/op +BenchmarkF_Int-32 138832455 8.643 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 139179651 8.638 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 140220103 8.589 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 138188732 8.704 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 135630690 8.862 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 136040631 8.795 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 137560159 8.740 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 138958898 8.628 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 139567308 8.581 ns/op 0 B/op 0 allocs/op +BenchmarkF_Int-32 139542769 8.622 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2054896 583.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2056406 583.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2043144 588.5 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2058644 585.7 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2008596 596.8 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2010388 596.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2033709 591.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2005344 596.1 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 2000546 604.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1950410 615.9 ns/op 0 B/op 0 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2666564 438.3 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2744241 443.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2777757 432.6 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2750884 429.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2798041 430.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2810049 431.6 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2772429 433.2 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2810482 432.8 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2757390 438.9 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2783545 436.3 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2844162 420.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2863263 418.3 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2968209 407.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2932875 408.5 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2982391 401.8 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2945971 407.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2977428 401.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2971867 403.5 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 3002991 402.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2961387 414.9 ns/op 32 B/op 3 allocs/op +BenchmarkGetEntry_Release-32 89229282 13.92 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 85426885 13.90 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84865628 13.70 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 86142536 13.78 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 88369797 13.92 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 89197444 13.91 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 85000282 13.91 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84901054 13.87 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84672810 13.83 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 81802935 13.74 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 65155068 18.52 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 65513268 21.47 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 41783744 26.35 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 63704410 18.79 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 66168928 18.60 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 65109111 18.94 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 48072588 24.59 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 67697931 18.35 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 64695258 18.65 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 65457519 18.70 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 21827269 53.27 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22497229 52.74 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22784016 53.32 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23183791 52.69 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23243380 52.54 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22420016 53.28 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23466741 51.56 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23280176 51.74 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23335543 51.58 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22540375 52.87 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 7374512 162.2 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 7571030 159.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 7371124 176.6 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 7216724 173.2 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 7158409 165.3 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 7737856 164.3 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6961912 167.8 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 7561536 175.8 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 7003896 182.2 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6791524 172.7 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 32759317 34.23 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 34180534 33.27 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 33069876 33.76 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 39683064 34.61 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 31353450 36.83 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 33050930 35.60 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 37852501 36.93 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 30849860 37.67 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 28615168 36.80 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 27032445 37.88 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 21946388 54.75 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 21454316 53.27 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 20810282 53.70 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23108696 52.89 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22881592 54.32 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 21783763 55.23 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 21905644 51.94 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23082604 52.67 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22422739 52.98 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22548337 53.83 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 34021609 39.46 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 33065958 35.38 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 32344489 37.31 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 36972448 33.38 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 35330798 40.31 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 30548731 52.30 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 30183591 45.49 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 33703340 34.92 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 38819121 32.62 ns/op 0 B/op 0 allocs/op +BenchmarkInfoDetailed_TreeMode-32 37789489 31.59 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 72041350 18.19 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 59724966 22.60 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 71740299 25.10 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 42306105 31.81 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 38949909 26.86 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 51659785 27.84 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 45437850 25.55 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 50053598 21.69 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 44469807 24.23 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 50364934 21.77 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 510460831 2.538 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 405677318 2.872 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 451578888 2.394 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 663255322 1.872 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 626236816 1.979 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 610141776 2.262 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 460588054 2.663 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 431286375 2.656 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 399269602 3.004 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 432009763 2.728 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 469295372 2.432 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 513603648 2.667 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 407669208 2.546 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 372617460 3.700 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 310429258 3.824 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 295176229 4.557 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 315543266 3.853 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 314009205 3.923 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 333745972 3.597 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 287148386 5.266 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 431140873 2.566 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 489042792 2.499 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 459458878 2.540 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 465599383 2.616 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 455865910 2.324 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 567560510 2.286 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 450831727 2.957 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 310754428 3.720 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 351642374 4.077 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 224678330 4.644 ns/op 0 B/op 0 allocs/op +BenchmarkWithComponent_Equivalent-32 2251190 533.8 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 4148746 444.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 2259657 454.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 6550794 187.2 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 6137835 196.6 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 6062402 192.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 6639778 183.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 5879551 179.2 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 8823801 159.4 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7701781 166.8 ns/op 192 B/op 3 allocs/op +BenchmarkSecureScan_NoMatch-32 21951687 48.01 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34512609 47.30 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 26425605 47.94 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 24361969 57.88 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 17411137 65.90 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 16897981 67.40 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 23521663 103.1 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 16863464 70.63 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 18601680 78.57 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 11080924 94.57 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 46869324 21.94 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 46702783 22.44 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 72752846 21.60 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 49321014 22.02 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 63734822 19.38 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 55814472 19.59 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 68084719 21.09 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 73100305 20.42 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 53315797 19.95 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 62032982 20.58 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 70698441 21.77 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 54269420 20.19 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 54386004 19.78 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 65618242 17.06 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 61749040 16.76 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 75540586 16.35 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 44848579 23.53 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 69555651 17.20 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 70410964 17.49 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 64120032 16.66 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8325481 146.4 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8116290 143.1 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8274457 145.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8123058 143.4 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8480810 142.8 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8118085 143.7 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8587443 145.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8316878 146.2 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8307584 146.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 8368065 146.2 ns/op 88 B/op 4 allocs/op +PASS +ok github.com/tensorfoundrylabs/velocity 379.032s +? github.com/tensorfoundrylabs/velocity/examples/basic [no test files] +? github.com/tensorfoundrylabs/velocity/examples/custom-theme [no test files] +? github.com/tensorfoundrylabs/velocity/examples/json-logging [no test files] +? github.com/tensorfoundrylabs/velocity/examples/multi-writer [no test files] +? github.com/tensorfoundrylabs/velocity/examples/pretty-output [no test files] +? github.com/tensorfoundrylabs/velocity/examples/progress [no test files] +? github.com/tensorfoundrylabs/velocity/examples/sampling [no test files] +? github.com/tensorfoundrylabs/velocity/examples/slog-bridge [no test files] +? github.com/tensorfoundrylabs/velocity/examples/tables [no test files] +? github.com/tensorfoundrylabs/velocity/examples/terminal-velocity [no test files] +? github.com/tensorfoundrylabs/velocity/examples/themes [no test files] +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity/pretty +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkPretty_NewFromLogger_Table-32 1373402 884.3 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1311885 906.1 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1279467 953.7 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1000000 1023 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1328244 894.4 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1371415 875.9 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1310115 925.0 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1233704 972.2 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1227282 995.2 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1210576 970.8 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_New_Table-32 1338716 886.7 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1302410 931.7 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1250655 939.3 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1221268 975.5 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1231671 974.5 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1336176 915.8 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1000000 1013 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1282807 928.0 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1348998 887.6 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1366118 881.3 ns/op 288 B/op 9 allocs/op +PASS +ok github.com/tensorfoundrylabs/velocity/pretty 23.845s +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity/slog +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkSlogHandler_Info-32 2777419 440.1 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2740453 448.5 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2270419 458.1 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2487800 462.6 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2398492 499.8 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2533945 518.9 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2387862 455.1 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2639852 474.0 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2179902 545.1 ns/op 192 B/op 6 allocs/op +BenchmarkSlogHandler_Info-32 2220135 481.6 ns/op 192 B/op 6 allocs/op +PASS +ok github.com/tensorfoundrylabs/velocity/slog 16.901s diff --git a/docs/bench-v2.0.0.txt b/docs/bench-v2.0.0.txt new file mode 100644 index 0000000..2376d22 --- /dev/null +++ b/docs/bench-v2.0.0.txt @@ -0,0 +1,5652 @@ +2026-05-09 18:27:11 [!DBG] Debug message +2026-05-09 18:27:11 [INFO] Info message +2026-05-09 18:27:11 [WARN] Warning message +2026-05-09 18:27:11 [ERR!] Error message +2026-05-09 18:27:11 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:11+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:11+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:11+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:11+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:11+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:11+10:00 [INFO] endpoint: [REDACTED] +2026-05-09T18:27:11+10:00 [INFO] request api_key: [REDACTED] +[OK] should not panic +[INFO] should not panic + │ line one +[INFO] should not panic (1) +2026-05-09 18:27:11 [!DBG] Debug message +2026-05-09 18:27:11 [INFO] Info message +2026-05-09 18:27:11 [WARN] Warning message +2026-05-09 18:27:11 [ERR!] Error message +2026-05-09 18:27:11 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:11+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +[INFO] should not panic (1) +k: v +s +──────────────────────────────────────── +[OK] should not panic +✅ ok +[INFO] should not panic + │ line one +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +[OK] should not panic +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +[INFO] should not panic + │ line one +[INFO] should not panic (1) +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +[OK] should not panic +k: v +s +──────────────────────────────────────── +[INFO] should not panic + │ line one +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +[INFO] should not panic (1) +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +[OK] should not panic +[INFO] should not panic (1) +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +[INFO] should not panic + │ line one +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +[INFO] should not panic + │ line one +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +[OK] should not panic +[INFO] should not panic (1) +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:12+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:12+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:12+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:12+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +[INFO] should not panic (1) +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +[OK] should not panic +2026-05-09T18:27:12+10:00 [INFO] endpoint: [REDACTED] +[INFO] should not panic + │ line one +2026-05-09T18:27:12+10:00 [INFO] request api_key: [REDACTED] +2026-05-09 18:27:12 [!DBG] Debug message +2026-05-09 18:27:12 [INFO] Info message +2026-05-09 18:27:12 [WARN] Warning message +2026-05-09 18:27:12 [ERR!] Error message +2026-05-09 18:27:12 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:12+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:13+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:13+10:00 [ERR!] Error + └ key: value +[OK] should not panic +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:13+10:00 [INFO] endpoint: [REDACTED] +2026-05-09T18:27:13+10:00 [INFO] request api_key: [REDACTED] +[INFO] should not panic (1) +[INFO] should not panic + │ line one +2026-05-09 18:27:13 [!DBG] Debug message +2026-05-09 18:27:13 [INFO] Info message +2026-05-09 18:27:13 [WARN] Warning message +2026-05-09 18:27:13 [ERR!] Error message +2026-05-09 18:27:13 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:13+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:13+10:00 [ERR!] Error + └ key: value +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +2026-05-09T18:27:13+10:00 [INFO] request api_key: [REDACTED] +2026-05-09T18:27:13+10:00 [INFO] endpoint: [REDACTED] +[OK] should not panic +[INFO] should not panic + │ line one +[INFO] should not panic (1) +2026-05-09 18:27:13 [!DBG] Debug message +2026-05-09 18:27:13 [INFO] Info message +2026-05-09 18:27:13 [WARN] Warning message +2026-05-09 18:27:13 [ERR!] Error message +2026-05-09 18:27:13 [INFO] Server started addr: :8080 pid: 4404 tls: true +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Night Owl +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Solarized +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Dracula +2026-05-09T18:27:13+10:00 [INFO] Testing theme theme: Nord +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 0 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 2 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 3 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 4 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 5 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 6 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 7 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 8 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 9 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 0 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 1 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 2 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 3 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 4 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 16 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 11 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 12 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 13 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 14 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 21 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 16 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 17 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 18 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 5 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 6 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 7 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 8 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 25 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 20 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 21 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 22 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 9 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 10 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 11 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 12 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 13 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 14 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 15 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 16 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 17 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 18 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 19 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 20 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 21 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 22 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 23 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 24 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 25 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 26 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 24 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 25 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 26 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 26 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 28 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 29 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 30 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 31 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 32 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 33 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 27 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 28 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 29 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 30 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 31 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 32 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 33 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 34 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 35 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 45 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 35 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 36 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 37 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 36 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 37 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 38 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 39 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 48 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 39 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 40 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 41 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 40 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 41 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 42 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 43 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 44 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 50 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 52 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 43 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 44 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 45 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 46 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 45 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 46 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 47 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 48 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 55 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 48 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 49 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 50 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 51 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 52 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 49 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 50 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 51 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 52 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 53 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 54 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 55 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 54 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 55 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 56 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 57 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 59 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 60 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 61 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 62 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 63 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 56 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 57 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 58 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 59 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 73 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 65 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 66 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 67 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 68 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 69 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 70 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 71 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 72 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 73 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 74 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 75 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 76 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 77 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 78 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 60 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 61 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 62 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 63 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 64 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 75 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 77 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 80 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 81 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 82 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 83 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 84 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 85 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 86 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 87 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 88 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 89 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 90 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 91 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 65 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 66 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 67 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 68 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 69 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 70 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 71 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 72 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 73 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 74 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 75 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 93 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 76 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 77 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 78 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 79 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 80 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 81 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 82 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 93 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 95 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 83 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 84 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 85 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 86 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 87 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Normal log iteration: 99 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 97 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 98 +2026-05-09T18:27:13+10:00 [ERR!] Detailed error + └ iteration: 99 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 88 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 89 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 90 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 91 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 92 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 93 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 94 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 95 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 96 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 97 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 98 +2026-05-09T18:27:13+10:00 [INFO] Detailed log + └ iteration: 99 +2026-05-09T18:27:13+10:00 [WARN] Warn + └ key: value +2026-05-09T18:27:13+10:00 [ERR!] Error + └ key: value +2026-05-09T18:27:13+10:00 [INFO] request api_key: [REDACTED] +2026-05-09T18:27:13+10:00 [INFO] endpoint: [REDACTED] +k: v +s +──────────────────────────────────────── +✅ ok +⚠️ warn +❌ err +ℹ️ info +muted +🐛 debug +[INFO] should not panic + │ line one +[INFO] should not panic (1) +[OK] should not panic +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkInfo_NoFields-32 44352126 26.77 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43763676 27.56 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43433410 27.70 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 36962654 28.43 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43525412 28.11 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43598313 28.01 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43312074 27.69 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43858526 27.58 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 44440328 27.82 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_NoFields-32 43512944 27.86 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_OneString-32 21317983 55.59 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 18802705 57.03 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22641210 54.91 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21374143 55.76 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21960445 58.35 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21733224 54.88 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22151582 54.98 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 22434183 54.89 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 19358522 55.31 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_OneString-32 21821276 56.60 ns/op 16 B/op 1 allocs/op +BenchmarkInfo_FiveFields-32 36582800 32.99 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 35797065 33.25 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 36184030 33.21 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 35520928 35.13 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 37214494 33.21 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 37248687 33.44 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 33482048 33.70 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 37037036 34.32 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 36373665 34.04 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_FiveFields-32 36611149 33.29 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 34490786 37.26 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 32056932 35.34 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36603778 34.40 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 31899961 34.68 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 34499908 34.65 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 35640882 34.10 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 35811594 34.44 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 34454343 36.22 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 34881996 34.16 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TenFields-32 36438168 33.88 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 551615082 2.155 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 557414636 2.203 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 547067149 2.182 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 560484968 2.156 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 553510130 2.191 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 561179036 2.149 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 554638860 2.163 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 560369544 2.161 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 560591012 2.165 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Disabled-32 554305032 2.159 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 210319280 5.763 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 206193998 5.802 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 209276640 5.775 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 208837189 5.728 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 207866175 5.790 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 205396765 5.784 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 207785155 5.801 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 207468914 5.793 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 208435705 5.759 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_WithSampler-32 206980878 5.795 ns/op 0 B/op 0 allocs/op +BenchmarkString-32 54639334 21.10 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 58259497 20.93 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 56287026 20.71 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 58986898 21.04 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 56750467 21.27 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 59407999 20.80 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 58112505 21.01 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 53852712 21.20 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 59395648 20.58 ns/op 16 B/op 1 allocs/op +BenchmarkString-32 58062454 21.79 ns/op 16 B/op 1 allocs/op +BenchmarkIntField-32 852088148 1.378 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 872369442 1.382 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 835531244 1.379 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 883288186 1.378 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 871885189 1.374 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 877067319 1.387 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 871457796 1.389 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 868222617 1.403 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 875392284 1.389 ns/op 0 B/op 0 allocs/op +BenchmarkIntField-32 869902461 1.379 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 873770077 1.379 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 874136698 1.382 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 863889214 1.379 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 866152075 1.381 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 835450387 1.376 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 880402460 1.373 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 880505174 1.380 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 862856197 1.370 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 875139475 1.375 ns/op 0 B/op 0 allocs/op +BenchmarkFloat64Field-32 868272874 1.384 ns/op 0 B/op 0 allocs/op +BenchmarkAny_String-32 56689609 20.89 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 59335442 20.79 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 59236441 21.20 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 55032996 20.81 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 56926092 21.09 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 59211013 21.25 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 56554154 21.20 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 58305920 21.81 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 59346886 21.51 ns/op 16 B/op 1 allocs/op +BenchmarkAny_String-32 56414875 21.19 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 57824636 21.32 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 58886745 20.88 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 58827277 20.97 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 59714266 21.97 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 58549437 20.89 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 55986231 21.28 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 61034224 21.49 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 57929316 20.86 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 58586598 22.22 ns/op 16 B/op 1 allocs/op +BenchmarkAny_Int-32 57656271 21.70 ns/op 16 B/op 1 allocs/op +BenchmarkJSONWriter_FiveFields-32 1862457 650.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1694307 765.2 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1843269 650.0 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1843238 657.7 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1886767 636.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1882567 636.3 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1904815 639.7 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1858472 639.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1854525 643.8 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_FiveFields-32 1882137 637.3 ns/op 0 B/op 0 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2428482 487.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2427080 488.9 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2505165 479.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2469199 482.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2497644 480.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2468355 480.5 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2413554 485.6 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2418259 484.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2465970 482.9 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_FiveFields-32 2490676 480.5 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2583015 460.2 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2624818 460.4 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2591403 458.1 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2631049 458.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2539778 460.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2612949 462.8 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2631529 459.3 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2622278 461.0 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2629603 466.7 ns/op 32 B/op 3 allocs/op +BenchmarkConsoleWriter_NoTemplate-32 2570008 461.0 ns/op 32 B/op 3 allocs/op +BenchmarkGetEntry_Release-32 83917256 14.62 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 83963643 14.37 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84893246 14.37 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 85243012 14.64 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 85530394 14.44 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84093679 14.32 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84190437 14.34 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 84180988 14.36 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 87506288 14.26 ns/op 0 B/op 0 allocs/op +BenchmarkGetEntry_Release-32 81971132 14.45 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 60545182 19.84 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 62145897 20.31 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 62211622 20.49 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 60852852 19.98 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 55321972 21.17 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 62149437 19.78 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 51219012 20.73 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 57194059 19.99 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 62852892 19.85 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_WithFields-32 57371797 19.71 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23335724 52.71 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22663188 53.14 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22429026 54.00 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22417461 53.62 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22708219 53.51 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 23694248 51.75 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22982605 53.15 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22541475 51.81 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22586661 52.73 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_Parallel-32 22743424 53.42 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6538258 192.0 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6213756 183.1 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6415173 196.1 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6519481 192.4 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6295546 192.6 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6315147 192.3 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6311583 189.7 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6363758 190.9 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6523380 192.0 ns/op 0 B/op 0 allocs/op +BenchmarkJSONWriter_Parallel-32 6654584 200.2 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 35800910 33.58 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 36597080 33.09 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 35936643 33.36 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 32266304 33.63 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 35608730 33.33 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 36222368 33.29 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 37429935 33.11 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 36390984 33.08 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 36927734 33.85 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode-32 35704615 33.42 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23384336 52.61 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22939638 52.53 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23385931 51.87 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22945558 52.21 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23578788 52.16 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 24091741 50.93 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23077587 52.26 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 22939111 53.02 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23066143 52.64 ns/op 0 B/op 0 allocs/op +BenchmarkInfo_TreeMode_Parallel-32 23386660 52.67 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 34769506 35.26 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 37374910 33.21 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 37206532 33.39 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36957304 33.22 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 35696649 33.78 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36947518 33.36 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36409202 33.24 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36579231 33.24 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36236368 33.33 ns/op 0 B/op 0 allocs/op +BenchmarkDetailed_TreeMode-32 36918759 33.25 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 73901181 16.50 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 73894809 16.44 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 74135853 16.46 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 72731679 17.16 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 73475385 16.54 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 72013250 16.51 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 74219763 16.26 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 72685863 18.01 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 70488721 16.35 ns/op 0 B/op 0 allocs/op +BenchmarkBufferPool_GetPut-32 69237692 16.45 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 629671704 1.919 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 630240846 1.927 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 625791691 1.933 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 623104399 1.944 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 625711420 1.920 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 630268982 1.900 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 640332289 1.910 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 638958242 1.931 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 628717290 1.913 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Render-32 621646024 1.928 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 620366956 1.966 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 611302369 1.974 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 580688337 1.968 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 602937207 1.954 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 604020662 1.968 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 617695111 1.970 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 622985679 1.949 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 617402095 1.979 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 619888182 1.998 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_RenderRaw-32 607536182 1.970 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 847560578 1.418 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 844400253 1.418 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 850312418 1.419 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 841520037 1.416 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 850153381 1.424 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 852481003 1.412 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 850609567 1.420 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 851141096 1.414 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 834704799 1.434 ns/op 0 B/op 0 allocs/op +BenchmarkLogger_Newline-32 847786323 1.425 ns/op 0 B/op 0 allocs/op +BenchmarkWithComponent_Equivalent-32 7619928 154.4 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7324839 155.5 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7965016 163.2 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7155694 152.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7777762 160.0 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7513566 157.6 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7619042 167.8 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7270620 169.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7069177 174.3 ns/op 192 B/op 3 allocs/op +BenchmarkWithComponent_Equivalent-32 7791964 143.0 ns/op 192 B/op 3 allocs/op +BenchmarkSecureScan_NoMatch-32 35330174 34.38 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 35825600 34.94 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 33160804 36.36 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 35489517 34.72 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 35815869 36.02 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34867502 34.55 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 36154705 35.35 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34187936 34.54 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34443366 34.13 ns/op 0 B/op 0 allocs/op +BenchmarkSecureScan_NoMatch-32 34520552 33.85 ns/op 0 B/op 0 allocs/op +BenchmarkSecureField_UntrustedWriter-32 5313596 250.2 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 5303074 251.4 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4986844 256.7 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4602740 253.6 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4832725 251.5 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4492659 274.9 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 5230868 252.9 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 5181300 250.0 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4428880 259.9 ns/op 32 B/op 1 allocs/op +BenchmarkSecureField_UntrustedWriter-32 4350729 260.1 ns/op 32 B/op 1 allocs/op +BenchmarkEntry_GetRelease-32 79002981 14.34 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 81618771 14.69 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 84739179 14.32 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 84479674 15.69 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 79974940 14.39 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 79328877 14.32 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 85278148 14.61 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 82279848 14.36 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 85323625 14.46 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetRelease-32 82018754 14.58 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 72449330 17.09 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 71870058 17.12 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 72815533 17.91 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 69967173 16.95 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 68717502 17.21 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 72944780 17.12 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 73781226 17.32 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 69303270 17.04 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 68361655 17.00 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_GetReleaseWithRetain-32 71581960 17.06 ns/op 0 B/op 0 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7970529 155.1 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7801381 155.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7683249 155.5 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7628205 155.6 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7640308 154.8 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7653422 155.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7721793 154.4 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7635996 154.7 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7657743 155.0 ns/op 88 B/op 4 allocs/op +BenchmarkEntry_ConcurrentRetainRelease-32 7532350 155.3 ns/op 88 B/op 4 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1250840 957.2 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1257446 945.6 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1264429 945.6 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1234862 961.4 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1237035 965.7 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1246051 958.8 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1277065 951.2 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1244830 955.0 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1286775 941.8 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_NewFromLogger_Table-32 1263585 958.7 ns/op 312 B/op 10 allocs/op +BenchmarkPretty_New_Table-32 1318341 908.1 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1319314 904.5 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1325317 905.1 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1346742 904.9 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1280238 927.3 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1291928 923.4 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1300635 917.3 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1332217 899.4 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1274778 938.3 ns/op 288 B/op 9 allocs/op +BenchmarkPretty_New_Table-32 1364247 896.1 ns/op 288 B/op 9 allocs/op +PASS +ok github.com/tensorfoundrylabs/velocity 407.770s +? github.com/tensorfoundrylabs/velocity/examples/basic [no test files] +? github.com/tensorfoundrylabs/velocity/examples/continuation [no test files] +? github.com/tensorfoundrylabs/velocity/examples/custom-theme [no test files] +? github.com/tensorfoundrylabs/velocity/examples/groups [no test files] +? github.com/tensorfoundrylabs/velocity/examples/hyperlinks [no test files] +? github.com/tensorfoundrylabs/velocity/examples/json-logging [no test files] +? github.com/tensorfoundrylabs/velocity/examples/multi-writer [no test files] +? github.com/tensorfoundrylabs/velocity/examples/notify [no test files] +? github.com/tensorfoundrylabs/velocity/examples/pretty-output [no test files] +? github.com/tensorfoundrylabs/velocity/examples/progress [no test files] +? github.com/tensorfoundrylabs/velocity/examples/ring-buffer [no test files] +? github.com/tensorfoundrylabs/velocity/examples/sampling [no test files] +? github.com/tensorfoundrylabs/velocity/examples/secure [no test files] +? github.com/tensorfoundrylabs/velocity/examples/slog-bridge [no test files] +? github.com/tensorfoundrylabs/velocity/examples/status-items [no test files] +? github.com/tensorfoundrylabs/velocity/examples/tables [no test files] +? github.com/tensorfoundrylabs/velocity/examples/terminal-velocity [no test files] +? github.com/tensorfoundrylabs/velocity/examples/themes [no test files] +PASS +ok github.com/tensorfoundrylabs/velocity/live 0.153s +goos: windows +goarch: amd64 +pkg: github.com/tensorfoundrylabs/velocity/slogbridge +cpu: AMD Ryzen 9 5950X 16-Core Processor +BenchmarkSlogHandler_Info-32 14779926 81.92 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 14913148 87.48 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 15207043 97.02 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 12420199 102.6 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 10408714 100.3 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 12626793 98.79 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 11285216 98.15 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 12077574 99.89 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 9972384 102.4 ns/op 144 B/op 3 allocs/op +BenchmarkSlogHandler_Info-32 11869494 100.3 ns/op 144 B/op 3 allocs/op +PASS +ok github.com/tensorfoundrylabs/velocity/slogbridge 13.264s diff --git a/entry.go b/entry.go index 96ef898..92a4767 100644 --- a/entry.go +++ b/entry.go @@ -34,6 +34,17 @@ type Entry struct { // forceTreeDisplay indicates that fields should always be displayed in tree format forceTreeDisplay bool + // maybeSecure is set when the message contains '<' and scanSecure is active. + // Writers check this to decide whether to run the ... redaction pass. + // Kept on Entry (not inlined into every Field) because the common case is false. + maybeSecure bool + + // statusKind carries the StatusKind for Logger.Status calls. + // statusKindNone (0xFF) means no status was set; this allows StatusOK (0) to be + // a valid value without ambiguity. One byte — measured to have zero hot-path cost + // on entries that never call Logger.Status. + statusKind StatusKind + // Reference count for pool safety // Starts at 1 when acquired, decremented on Release // Only returned to pool when count reaches 0 @@ -86,6 +97,8 @@ func (e *Entry) Reset() { e.written.Store(0) e.forceTreeDisplay = false + e.maybeSecure = false + e.statusKind = statusKindNone e.refCount.Store(0) } @@ -140,7 +153,7 @@ func (e *Entry) Release() { } func (e *Entry) WithField(key string, value any) *Entry { - e.Fields = append(e.Fields, F(key, value)) + e.Fields = append(e.Fields, Any(key, value)) return e } diff --git a/examples/Makefile b/examples/Makefile index d0aab02..83bdd66 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,4 +1,6 @@ -.PHONY: run-basic run-json run-themes run-custom-theme run-pretty run-tables run-progress run-slog run-multi-writer run-sampling run-terminal-velocity run-all build-all clean help +.DEFAULT_GOAL := help + +.PHONY: run-basic run-json run-themes run-custom-theme run-pretty run-tables run-progress run-slog run-multi-writer run-sampling run-terminal-velocity run-notify run-secure run-ring-buffer run-status-items run-groups run-continuation run-hyperlinks run-all build-all clean help SEPARATOR = @echo "" && echo "============================================================" && echo "" @@ -46,6 +48,34 @@ run-sampling: run-terminal-velocity: go run ./terminal-velocity +## run-notify: Run the Notify channel example +run-notify: + go run ./notify + +## run-secure: Run the field-level redaction and tag example +run-secure: + go run ./secure + +## run-ring-buffer: Run the in-process log capture example +run-ring-buffer: + go run ./ring-buffer + +## run-status-items: Run the StatusItem and Logger.Status example +run-status-items: + go run ./status-items + +## run-groups: Run the Group count-headed block example +run-groups: + go run ./groups + +## run-continuation: Run the ContinuationBlock example +run-continuation: + go run ./continuation + +## run-hyperlinks: Run the OSC 8 hyperlink example +run-hyperlinks: + go run ./hyperlinks + ## run-all: Run all examples sequentially with a separator between each run-all: @echo "Running all velocity examples..." @@ -104,6 +134,41 @@ run-all: @echo " EXAMPLE 11: terminal-velocity (interactive)" @echo "============================================================" go run ./terminal-velocity + $(SEPARATOR) + @echo "============================================================" + @echo " EXAMPLE 12: notify" + @echo "============================================================" + go run ./notify + $(SEPARATOR) + @echo "============================================================" + @echo " EXAMPLE 13: secure" + @echo "============================================================" + go run ./secure + $(SEPARATOR) + @echo "============================================================" + @echo " EXAMPLE 14: ring-buffer" + @echo "============================================================" + go run ./ring-buffer + $(SEPARATOR) + @echo "============================================================" + @echo " EXAMPLE 15: status-items" + @echo "============================================================" + go run ./status-items + $(SEPARATOR) + @echo "============================================================" + @echo " EXAMPLE 16: groups" + @echo "============================================================" + go run ./groups + $(SEPARATOR) + @echo "============================================================" + @echo " EXAMPLE 17: continuation" + @echo "============================================================" + go run ./continuation + $(SEPARATOR) + @echo "============================================================" + @echo " EXAMPLE 18: hyperlinks" + @echo "============================================================" + go run ./hyperlinks ## build-all: Compile all examples to binaries in examples/bin/ build-all: @@ -118,12 +183,20 @@ build-all: go build -o bin/multi-writer ./multi-writer go build -o bin/sampling ./sampling go build -o bin/terminal-velocity ./terminal-velocity + go build -o bin/notify ./notify + go build -o bin/secure ./secure + go build -o bin/ring-buffer ./ring-buffer + go build -o bin/status-items ./status-items + go build -o bin/groups ./groups + go build -o bin/continuation ./continuation + go build -o bin/hyperlinks ./hyperlinks ## clean: Remove compiled example binaries clean: go clean ./basic ./json-logging ./themes ./custom-theme ./pretty-output \ ./tables ./progress ./slog-bridge ./multi-writer ./sampling \ - ./terminal-velocity + ./terminal-velocity ./notify ./secure ./ring-buffer ./status-items \ + ./groups ./continuation ./hyperlinks -rm -rf bin ## help: Show this help message diff --git a/examples/README.md b/examples/README.md index fde3d5b..16d65e5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,36 +21,48 @@ go run ./examples/terminal-velocity ## Examples -### Getting Started +### Foundation | Example | What it shows | Run | |---------|--------------|-----| -| [basic](basic/) | Logger creation, log levels, typed fields, child loggers, `SetLevel`, `InfoDetailed`, presets | `make run-basic` | -| [json-logging](json-logging/) | Dual output (console + JSON file), `WithCaller`, custom time format, structured JSON | `make run-json` | -| [themes](themes/) | All four built-in colour themes (Night Owl, Solarized, Dracula, Nord) with the same log entries | `make run-themes` | -| [custom-theme](custom-theme/) | Define your own colour palette (Cyberpunk neon). Shows how custom themes flow through loggers, pretty output, tables, and status indicators | `make run-custom-theme` | +| [basic](basic/) | Logger creation, log levels, typed fields, child loggers, `SetLevel`, `Detailed()`, `WithDevelopment()` | `make run-basic` | +| [themes](themes/) | All five built-in themes (Night Owl, Solarized, Dracula, Nord, Mono) with `Theme.Format` and `Theme.Wrap` for semantic style slots | `make run-themes` | +| [custom-theme](custom-theme/) | Define your own colour palette with `NewTheme` + `ThemeOption`, custom `StyleSlot` values, theme flowing through loggers, tables, and status output | `make run-custom-theme` | -### Output and Display +### Output Formats | Example | What it shows | Run | |---------|--------------|-----| -| [pretty-output](pretty-output/) | Section, Box, Panel, Banner, Bullet, KeyValue, Table, Tree, SystemInfo, StatusFormatter | `make run-pretty` | -| [tables](tables/) | Table rendering with headers, rows, and ANSI-coloured cells (StatusFormatter) | `make run-tables` | -| [progress](progress/) | ProgressBar, all 5 Spinner styles, label changes, success/error stop messages | `make run-progress` | +| [json-logging](json-logging/) | Dual output: coloured console for humans, newline-delimited JSON for aggregators; `WithCaller`, custom time format | `make run-json` | +| [multi-writer](multi-writer/) | `AddWriter`/`RemoveWriter`, `FilteredWriter`, `WriterFunc`, `WriterTrusted()` for the Phase 4 trust model | `make run-multi-writer` | -### Integration +### Pretty Output | Example | What it shows | Run | |---------|--------------|-----| -| [slog-bridge](slog-bridge/) | `NewSlogLogger`, `slog.SetDefault`, `WithGroup`, `WithAttrs`, level filtering | `make run-slog` | -| [multi-writer](multi-writer/) | `AddWriter`/`RemoveWriter`, `FilteredWriter`, `WriterFunc` adapter | `make run-multi-writer` | -| [sampling](sampling/) | `CountSampler` for high-volume log reduction, before/after stats | `make run-sampling` | +| [pretty-output](pretty-output/) | Section, Box, Panel, Banner, Bullet, KeyValue, Table, Tree, SystemInfo via the `Pretty` facade | `make run-pretty` | +| [tables](tables/) | `NewTable` with ANSI-coloured cells, `log.Table()` convenience for indented rendering, auto-sized columns | `make run-tables` | +| [progress](progress/) | `live.NewProgressBar`, all five `SpinnerStyle` variants, label changes, success and error stop messages | `make run-progress` | +| [terminal-velocity](terminal-velocity/) | Hero example: GPU cluster deployment simulator using banners, spinners, progress bars, trees, tables, child loggers, error recovery | `make run-terminal-velocity` | -### Showcase +### Structured Features | Example | What it shows | Run | |---------|--------------|-----| -| [terminal-velocity](terminal-velocity/) | Full GPU cluster deployment simulator using banner, spinners, progress bars, trees, tables, child loggers, structured fields, error recovery | `make run-terminal-velocity` | +| [sampling](sampling/) | `CountSampler` for high-volume log reduction; first-N pass-through then every-Mth sampling | `make run-sampling` | +| [slog-bridge](slog-bridge/) | `slogbridge.NewLogger`, `slog.SetDefault`, `WithGroup`, level filtering; incremental adoption path | `make run-slog` | + +### v2 New + +| Example | What it shows | Run | +|---------|--------------|-----| +| [notify](notify/) | `Logger.Notify`, `NotifyLines`, `NotifyBox` for ephemeral operator output bypassing the structured pipeline | `make run-notify` | +| [secure](secure/) | `Secure`, `SecureURL`, `Redacted`, `Truncated` field constructors; `` tag scanning; TTY vs JSON divergence; trusted writers for audit logs | `make run-secure` | +| [ring-buffer](ring-buffer/) | `RingBufferWriter` for in-process log capture: `Snapshot` (HTTP debug endpoint pattern) and `Subscribe` (live tail pattern) | `make run-ring-buffer` | +| [status-items](status-items/) | `Logger.Status` with all six `StatusKind` values (`OK`, `Fail`, `Warn`, `Info`, `Pending`, `Skipped`); standalone `NewStatusItem` with `log.Render` | `make run-status-items` | +| [groups](groups/) | `Logger.Group` for count-headed indented blocks; explicit markers; empty group | `make run-groups` | +| [continuation](continuation/) | `Logger.Continue` for multi-line output anchored to one structured entry; hyperlinks inside continuation lines | `make run-continuation` | +| [hyperlinks](hyperlinks/) | `Hyperlink` OSC 8 helper; `HyperlinksSupported` detection; all three fallback modes; composing with `Theme.Format`; embedding in Box, Table, and ContinuationBlock | `make run-hyperlinks` | ## Building diff --git a/examples/basic/main.go b/examples/basic/main.go index 61b0d70..19b9ed9 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -7,13 +7,16 @@ import ( "os" "time" - "github.com/tensorfoundrylabs/velocity" + "github.com/tensorfoundrylabs/velocity/v2" ) func main() { - // The simplest way to get a logger. It writes coloured output to stdout - // using the default Night Owl theme and debug level. - log := velocity.New(os.Stdout) + // The simplest way to get a logger. WithDevelopment resets to sensible + // development defaults: debug level, coloured output, local timezone. + log := velocity.New( + velocity.WithDevelopment(), + velocity.WithConsoleOutput(os.Stdout), + ) log.Info("velocity logging library - basic example") @@ -25,8 +28,7 @@ func main() { log.Error("failed to connect to cache", velocity.String("host", "redis:6379")) // Typed field constructors keep allocations off the heap on hot paths. - // Use the specific constructor when you know the type; F() is fine for - // less critical code where convenience matters more. + // Use the specific constructor when you know the type. log.Info("request processed", velocity.String("method", "GET"), velocity.String("path", "/api/users"), @@ -37,14 +39,6 @@ func main() { velocity.Error("err", nil), ) - // F() detects the type automatically. Handy for quick instrumentation - // but the typed constructors are faster in tight loops. - log.Info("generic field constructor", - velocity.F("user_id", 42), - velocity.F("role", "admin"), - velocity.F("active", true), - ) - // With() returns a child logger that stamps every subsequent entry with // the given fields. Great for scoping a logger to a request or component. reqLog := log.With( @@ -70,12 +64,12 @@ func main() { log.Info("this info message won't appear either") log.Warn("this warning still gets through") - // Drop back to debug so we can show InfoDetailed. + // Drop back to debug so we can show Detailed(). log.SetLevel(velocity.LevelDebug) - // InfoDetailed forces a tree-format display for the fields, which is much - // easier to read when there are many fields or values are long. - log.InfoDetailed("deployment summary", + // Detailed() returns a child logger that forces tree-format for every call. + // Easier to read when there are many fields or values are long. + log.Detailed().Info("deployment summary", velocity.String("environment", "staging"), velocity.String("version", "2.4.1"), velocity.Int("replicas", 3), @@ -83,9 +77,8 @@ func main() { velocity.Bool("health_checks_passed", true), ) - // NewDevelopment() is a convenience preset with sensible defaults for - // local development: debug level, local timezone, coloured output. - devLog := velocity.NewDevelopment() + // WithDevelopment() as the only option keeps it simple. + devLog := velocity.New(velocity.WithDevelopment()) devLog.Info("development preset logger is ready", velocity.String("preset", "development"), ) diff --git a/examples/continuation/main.go b/examples/continuation/main.go new file mode 100644 index 0000000..0f4a2b4 --- /dev/null +++ b/examples/continuation/main.go @@ -0,0 +1,120 @@ +// continuation demonstrates Logger.Continue for multi-line output anchored to +// a single structured log entry. +// +// The canonical use case is a server startup block where the listening address, +// dashboard URL, and stop instruction should all be grouped under one timestamped +// INFO entry rather than scattered across three separate log lines. +// +// Run directly for a TTY console with coloured │ glyph: +// +// go run ./examples/continuation +// +// Pipe to see the non-TTY plain form (same glyph, no colour): +// +// go run ./examples/continuation | cat +// +// Add -json to write structured output alongside the console output: +// +// go run ./examples/continuation -json +package main + +import ( + "flag" + "os" + "time" + + velocity "github.com/tensorfoundrylabs/velocity/v2" +) + +func main() { + jsonOut := flag.Bool("json", false, "also write JSON to continuation.log") + flag.Parse() + + opts := []velocity.Option{ + velocity.WithDevelopment(), + } + if *jsonOut { + f, err := os.Create("continuation.log") + if err != nil { + panic(err) + } + defer func() { + if err := f.Close(); err != nil { + panic(err) + } + }() + opts = append(opts, + velocity.WithStructuredOutput(f), + velocity.WithStructuredLevel(velocity.LevelDebug), + ) + } + + log := velocity.New(opts...) + defer func() { + if err := log.Close(); err != nil { + panic(err) + } + }() + + // Gate OSC 8 hyperlinks on whether stdout is a real terminal. HyperlinksSupported() + // is per-process and does not detect pipe/redirect — IsTerminalWriter fills that gap. + stdoutIsTTY := velocity.IsTerminalWriter(os.Stdout) + + // hyperlink returns an OSC 8 link when stdout is a TTY, otherwise a plain URL. + hyperlink := func(url string) string { + if stdoutIsTTY { + return velocity.Hyperlink(url, url) + } + return url + } + + // --- Server startup block --- + // The primary INFO line records the event in the structured pipeline. + // Continuation lines carry the human-readable context (URL, keybind) without + // polluting the structured log with ad-hoc fields. + log.Continue(velocity.LevelInfo, "HTTP server listening", + "Available at: "+hyperlink("http://localhost:8080"), + "Metrics: "+hyperlink("http://localhost:9090/metrics"), + "Press Ctrl+C to stop", + ) + + log.Newline() + + // --- Error context block --- + // Failed operations often benefit from inline context: the query that failed, + // the connection details, and a suggested action — all tied to one ERROR entry. + log.Continue(velocity.LevelError, "Database query failed", + "Query: SELECT * FROM users WHERE active = true", + "Connection: db.internal:5432", + "Try: check DB connectivity with `pg_isready -h db.internal`", + ) + + log.Newline() + + // --- MOTD-style startup block --- + // A service that displays its version and environment at launch. Continuation + // lets this be a proper log entry (timestamped, levelled) rather than a + // fmt.Println block that bypasses the structured pipeline. + buildTime := time.Date(2026, 5, 9, 10, 0, 0, 0, time.UTC) + log.Continue(velocity.LevelInfo, "Velocity example service starting", + "Version: v2.0.0-dev", + "Built: "+buildTime.Format(time.RFC3339), + "Go: 1.24.3", + "Environment: development", + ) + + log.Newline() + + // --- Single continuation line --- + // Works with just one additional line; no special case needed by the caller. + log.Continue(velocity.LevelWarn, "Rate limit approaching", + "Current: 950/1000 requests per minute", + ) + + log.Newline() + + // --- No continuation lines --- + // When called with no lines, Continue behaves like a normal log call. + // Useful when continuation lines are conditionally computed. + log.Continue(velocity.LevelDebug, "Cache warm-up complete") +} diff --git a/examples/custom-theme/main.go b/examples/custom-theme/main.go index e217be2..e96d5b7 100644 --- a/examples/custom-theme/main.go +++ b/examples/custom-theme/main.go @@ -1,6 +1,5 @@ -// Custom theme example. Shows how to define your own colour palette -// and have it flow through the entire velocity stack: log lines, -// pretty output, status indicators, and tables. +// Custom theme example. Shows how to define your own colour palette using NewTheme +// and ThemeOption, then use Theme.Format(slot, s) to colour arbitrary output. // // This one is a cyberpunk theme. Hot pinks, electric blues, neon greens. package main @@ -10,51 +9,42 @@ import ( "os" "time" - "github.com/tensorfoundrylabs/velocity" - "github.com/tensorfoundrylabs/velocity/pretty" + "github.com/tensorfoundrylabs/velocity/v2" ) // ThemeCyberpunk is a neon-on-dark palette inspired by Night City. -var ThemeCyberpunk = cyberpunkTheme() - -func cyberpunkTheme() *velocity.Theme { - t := &velocity.Theme{ - Name: "Cyberpunk", - - // Log levels: each gets a distinct neon tone. - DebugColour: velocity.RGB(0x8B, 0x5C, 0xF6), // purple - InfoColour: velocity.RGB(0x00, 0xD4, 0xFF), // electric blue - WarnColour: velocity.RGB(0xFF, 0xE6, 0x00), // neon yellow - ErrorColour: velocity.RGB(0xFF, 0x00, 0x6E), // hot pink - FatalColour: velocity.RGB(0xFF, 0x00, 0x00), // red - - // Chrome: the structural bits around your log messages. - TimestampColour: velocity.RGB(0x5A, 0x5A, 0x7A), // dim steel - MessageColour: velocity.RGB(0xE0, 0xE0, 0xFF), // cool white - FieldKeyColour: velocity.RGB(0x00, 0xFF, 0xAA), // neon green - FieldValColour: velocity.RGB(0xCC, 0xCC, 0xEE), // soft lavender - ErrorValColour: velocity.RGB(0xFF, 0x00, 0x6E), // hot pink (matches error) - - // Status indicators for tables and operation results. - StatusOKColour: velocity.RGB(0x00, 0xFF, 0xAA), // neon green - StatusFailColour: velocity.RGB(0xFF, 0x00, 0x6E), // hot pink - StatusWarnColour: velocity.RGB(0xFF, 0xE6, 0x00), // neon yellow - StatusInfoColour: velocity.RGB(0x00, 0xD4, 0xFF), // electric blue - - // Table headers. - TableHeader: velocity.RGB(0xBB, 0x86, 0xFC), // bright purple - } - - // Pre-compute ANSI escape sequences so they aren't generated per log line. - t.Cache() - - return t -} +var ThemeCyberpunk = velocity.NewTheme("Cyberpunk", + // Log levels: each gets a distinct neon tone. + velocity.WithLevelColours( + velocity.RGB(0x8B, 0x5C, 0xF6), // debug: purple + velocity.RGB(0x00, 0xD4, 0xFF), // info: electric blue + velocity.RGB(0xFF, 0xE6, 0x00), // warn: neon yellow + velocity.RGB(0xFF, 0x00, 0x6E), // error: hot pink + velocity.RGB(0xFF, 0x00, 0x00), // fatal: red + ), + // Chrome: the structural bits around your log messages. + velocity.WithTimestampColour(velocity.RGB(0x5A, 0x5A, 0x7A)), // dim steel + velocity.WithMessageColour(velocity.RGB(0xE0, 0xE0, 0xFF)), // cool white + velocity.WithFieldColours( + velocity.RGB(0x00, 0xFF, 0xAA), // key: neon green + velocity.RGB(0xCC, 0xCC, 0xEE), // value: soft lavender + velocity.RGB(0xFF, 0x00, 0x6E), // error value: hot pink + ), + // Status and semantic slots for use with Theme.Format. + velocity.WithStyleSlot(velocity.SlotStatusOK, velocity.RGB(0x00, 0xFF, 0xAA)), + velocity.WithStyleSlot(velocity.SlotStatusFail, velocity.RGB(0xFF, 0x00, 0x6E)), + velocity.WithStyleSlot(velocity.SlotStatusWarn, velocity.RGB(0xFF, 0xE6, 0x00)), + velocity.WithStyleSlot(velocity.SlotStatusInfo, velocity.RGB(0x00, 0xD4, 0xFF)), + velocity.WithStyleSlot(velocity.SlotTableHeader, velocity.RGB(0xBB, 0x86, 0xFC)), + velocity.WithStyleSlot(velocity.SlotGood, velocity.RGB(0x00, 0xFF, 0xAA)), + velocity.WithStyleSlot(velocity.SlotBad, velocity.RGB(0xFF, 0x00, 0x6E)), + velocity.WithStyleSlot(velocity.SlotMuted, velocity.RGB(0x5A, 0x5A, 0x7A)), +) func main() { // Wire up the theme through the logger. Every writer and formatter // inherits it automatically. - log := velocity.NewWithOptions( + log := velocity.New( velocity.WithConsoleOutput(os.Stdout), velocity.WithTheme(ThemeCyberpunk), velocity.WithLevel(velocity.LevelDebug), @@ -71,8 +61,8 @@ func main() { log.Newline() - // Detailed mode shows fields as a tree, same colours. - log.InfoDetailed("system status", + // Detailed() child forces tree mode, same colours as the parent theme. + log.Detailed().Info("system status", velocity.String("cpu", "Arasaka X9-R"), velocity.Int("cores", 128), velocity.Float64("clock_ghz", 5.8), @@ -84,7 +74,7 @@ func main() { // Pretty output routes through the logger so it serialises under the same mutex // and inherits the theme automatically. - p := pretty.NewFromLogger(log) + p := velocity.NewPrettyFromLogger(log) p.Section("Mission Briefing") @@ -99,28 +89,29 @@ func main() { log.Newline() - // Status formatter picks up the theme for coloured OK/FAIL/WARN. - sf := velocity.NewStatusFormatter(ThemeCyberpunk, true) - + // Theme.Format(slot, s) is the v2 way to colour cell content. + // No raw ANSI construction needed; the theme handles escape codes. + style := log.Style() p.Table( []string{"Implant", "Status", "Integrity"}, [][]string{ - {"Kiroshi Optics Mk.3", sf.Okay("ONLINE"), "98%"}, - {"Mantis Blades", sf.Okay("ONLINE"), "100%"}, - {"Sandevistan Mk.4", sf.Warn("DEGRADED"), "67%"}, - {"Monowire", sf.Fail("OFFLINE"), "12%"}, + {"Kiroshi Optics Mk.3", style.Format(velocity.SlotStatusOK, "ONLINE"), "98%"}, + {"Mantis Blades", style.Format(velocity.SlotStatusOK, "ONLINE"), "100%"}, + {"Sandevistan Mk.4", style.Format(velocity.SlotStatusWarn, "DEGRADED"), "67%"}, + {"Monowire", style.Format(velocity.SlotStatusFail, "OFFLINE"), "12%"}, }, ) log.Newline() - // Tree display with the theme. - p.Tree([]pretty.TreeItem{ + // Tree display with the theme. velocity.NewTree is the canonical constructor; + // p.Tree is sugar that calls Render immediately. + p.Tree([]velocity.TreeItem{ { Key: "netrunner-loadout", - Children: []pretty.TreeItem{ + Children: []velocity.TreeItem{ {Key: "deck", Value: "Tetratronic Rippler Mk.4"}, - {Key: "quickhacks", Children: []pretty.TreeItem{ + {Key: "quickhacks", Children: []velocity.TreeItem{ {Key: "contagion", Value: "legendary"}, {Key: "short circuit", Value: "epic"}, {Key: "system reset", Value: "rare"}, diff --git a/examples/groups/main.go b/examples/groups/main.go new file mode 100644 index 0000000..cc41786 --- /dev/null +++ b/examples/groups/main.go @@ -0,0 +1,86 @@ +// groups demonstrates Logger.Group for count-headed indented blocks. +// +// This pattern comes from olla's translator-route registration, where each +// registered route needs to be visible at startup but kept out of the hot-log path. +// +// Run directly for a TTY console with coloured count token: +// +// go run ./examples/groups +// +// Pipe to see the non-TTY plain form: +// +// go run ./examples/groups | cat +// +// Add -json to write structured output alongside the console output: +// +// go run ./examples/groups -json +package main + +import ( + "flag" + "os" + + velocity "github.com/tensorfoundrylabs/velocity/v2" +) + +func main() { + jsonOut := flag.Bool("json", false, "also write JSON to groups.log") + flag.Parse() + + opts := []velocity.Option{ + velocity.WithDevelopment(), + } + if *jsonOut { + f, err := os.Create("groups.log") + if err != nil { + panic(err) + } + defer func() { + if err := f.Close(); err != nil { + panic(err) + } + }() + opts = append(opts, + velocity.WithStructuredOutput(f), + velocity.WithStructuredLevel(velocity.LevelDebug), + ) + } + + log := velocity.New(opts...) + defer func() { + if err := log.Close(); err != nil { + panic(err) + } + }() + + log.Info("translator service starting") + log.Newline() + + // Olla's route-registration pattern: show which routes were bound. + log.Group(velocity.LevelInfo, "Registering translator routes", + velocity.GroupItem{Text: "GET /translate"}, + velocity.GroupItem{Text: "POST /translate"}, + velocity.GroupItem{Text: "GET /languages"}, + velocity.GroupItem{Text: "GET /health"}, + ) + + log.Newline() + + // Explicit markers: useful for check-list style output (pass / fail). + log.Group(velocity.LevelInfo, "Config validation", + velocity.GroupItem{Marker: "✓", Text: "API key present"}, + velocity.GroupItem{Marker: "✓", Text: "Rate limit configured"}, + velocity.GroupItem{Marker: "✓", Text: "Target language list loaded"}, + velocity.GroupItem{Marker: "~", Text: "Cache warm (optional, skipped)"}, + ) + + log.Newline() + + // Empty group: count shows (0), no item lines emitted. + log.Group(velocity.LevelInfo, "Pending background tasks") + + log.Newline() + + // The follow-up call is a normal Info — Group does not include a footer. + log.Info("finished registering translator routes") +} diff --git a/examples/hyperlinks/main.go b/examples/hyperlinks/main.go new file mode 100644 index 0000000..98b00ca --- /dev/null +++ b/examples/hyperlinks/main.go @@ -0,0 +1,185 @@ +// Hyperlinks example demonstrates OSC 8 terminal hyperlinks. +// +// OSC 8 is the escape sequence that makes terminal text into a clickable link, +// like an in a browser. Terminals that support it (iTerm2, WezTerm, +// Windows Terminal, kitty, VSCode integrated terminal) show the display text +// underlined; clicking it opens the URI. +// +// Detection is automatic. Override with: +// +// VELOCITY_HYPERLINKS=1 force enable (always render OSC 8) +// VELOCITY_HYPERLINKS=0 force disable (always use fallback) +// +// Run to see the default detection result: +// +// go run ./examples/hyperlinks +// +// Pipe to confirm no OSC 8 control sequences appear in non-TTY output: +// +// go run ./examples/hyperlinks | cat +// +// Force-enable to see OSC 8 sequences in a non-supporting terminal: +// +// VELOCITY_HYPERLINKS=1 go run ./examples/hyperlinks +// +// Note: HyperlinksSupported() is per-process, not per-fd. When stdout is a pipe +// but the parent terminal supports OSC 8, the env-based detection may still +// return true. Use WithHyperlinkFallback(HyperlinkFallbackNone) or gate on +// IsTerminalWriter(os.Stdout) when writing to potentially non-TTY destinations. +package main + +import ( + "fmt" + "os" + + "github.com/tensorfoundrylabs/velocity/v2" +) + +func main() { + // Gate OSC 8 on whether stdout is actually a terminal. HyperlinksSupported() + // checks the env var and terminal type but does not know whether stdout has + // been redirected — IsTerminalWriter catches the pipe/redirect case. + stdoutIsTTY := velocity.IsTerminalWriter(os.Stdout) + + log := velocity.New( + velocity.WithDevelopment(), + velocity.WithConsoleOutput(os.Stdout), + ) + + supported := velocity.HyperlinksSupported() && stdoutIsTTY + fmt.Printf("OSC 8 support detected: %v\n", velocity.HyperlinksSupported()) + fmt.Printf("stdout is a terminal: %v\n", stdoutIsTTY) + fmt.Printf("hyperlinks active: %v\n", supported) + fmt.Printf("(override with VELOCITY_HYPERLINKS=1 or =0)\n") + fmt.Println() + + // --- Plain vs hyperlinked text --- + // + // On a supporting terminal the second line is clickable; both lines read + // the same in a non-supporting terminal (Parens fallback appends the URL). + plain := "https://tensorfoundry.io/docs" + + // Only emit OSC 8 when stdout is a terminal. HyperlinksSupported() checks env + // vars and TERM_PROGRAM but does not know whether stdout has been redirected — + // the stdoutIsTTY guard catches the pipe/redirect case. + var linked string + if stdoutIsTTY && velocity.HyperlinksSupported() { + linked = velocity.Hyperlink("https://tensorfoundry.io/docs", "velocity docs") + } else { + linked = "velocity docs (https://tensorfoundry.io/docs)" + } + + fmt.Println("Plain URL:", plain) + fmt.Println("Hyperlink:", linked) + fmt.Println() + + // --- Fallback modes --- + // + // When OSC 8 is not supported (or force-disabled via VELOCITY_HYPERLINKS=0), + // Hyperlink returns plain text in one of three forms. + // These are shown with their literal output — independent of terminal support. + uri := "https://tensorfoundry.io/setup" + text := "complete setup" + + fmt.Println("Fallback modes (seen when VELOCITY_HYPERLINKS=0 or no OSC 8 support):") + fmt.Printf(" Parens : %s\n", text+" ("+uri+")") + fmt.Printf(" Brackets : %s\n", text+" ["+uri+"]") + fmt.Printf(" None : %s\n", text) + fmt.Println() + + // The three fallback variants are only meaningfully different when OSC 8 is + // disabled; when it is active all three emit the same OSC 8 sequence and look + // identical on a supporting terminal. Show them using plain string construction + // so the output is predictable regardless of terminal support. + fmt.Println("Fallback variant formats (shown as plain text for illustration):") + fmt.Printf(" Parens : %s\n", text+" ("+uri+")") + fmt.Printf(" Brackets : %s\n", text+" ["+uri+"]") + fmt.Printf(" None : %s\n", text) + fmt.Println() + + // --- Combining with Theme.Format --- + // + // Hyperlink returns a plain string; wrap it with Theme.Format to apply + // colour. The OSC 8 sequence and ANSI colour codes compose correctly in + // all supporting terminals. When stdout is not a TTY, Hyperlink returns + // plain text so no escape sequences reach the pipe. + style := log.Style() + var setupLink string + if stdoutIsTTY && velocity.HyperlinksSupported() { + setupLink = velocity.Hyperlink(uri, "Open setup page") + } else { + setupLink = "Open setup page (" + uri + ")" + } + coloured := style.Format(velocity.SlotHyperlink, setupLink) + fmt.Println("Coloured hyperlink:", coloured) + fmt.Println() + + // --- Inside a Box --- + // + // Renderable types treat Hyperlink output as ordinary strings, so they + // work inside Box content, Table cells, and ContinuationBlock lines. + // Gate on stdoutIsTTY to keep OSC 8 out of piped output. + var setupURL, docsURL string + if stdoutIsTTY { + setupURL = velocity.Hyperlink("https://tensorfoundry.io/setup?token=abc123", "https://tensorfoundry.io/setup?token=abc123") + docsURL = velocity.Hyperlink("https://tensorfoundry.io/docs", "documentation") + } else { + setupURL = "https://tensorfoundry.io/setup?token=abc123" + docsURL = "documentation (https://tensorfoundry.io/docs)" + } + + // log.Style() returns a mono theme when stdout is not a TTY (NO_COLOR, piped), + // so the box and table are colour-free in that case. + box := velocity.NewBox( + "Setup Required", + "Open the following URL to complete your installation:\n\n"+ + " "+setupURL+"\n\n"+ + "See the "+docsURL+" for details.", + log.Style(), + ) + log.Render(box) + log.Newline() + + // --- Inside a Table cell --- + // + // Column-width calculation strips ANSI codes but the OSC 8 sequences are + // zero-width markup, so cell alignment is preserved. + // When stdout is a pipe, plain text is used to avoid leaking escape sequences. + tableRows := [][]string{ + {"API reference", "https://pkg.go.dev/github.com/tensorfoundrylabs/velocity/v2"}, + {"Source code", "https://github.com/tensorfoundrylabs/velocity"}, + {"Changelog", "https://github.com/tensorfoundrylabs/velocity/releases"}, + } + if stdoutIsTTY && velocity.HyperlinksSupported() { + tableRows = [][]string{ + {"API reference", velocity.Hyperlink("https://pkg.go.dev/github.com/tensorfoundrylabs/velocity/v2", "pkg.go.dev")}, + {"Source code", velocity.Hyperlink("https://github.com/tensorfoundrylabs/velocity", "github.com")}, + {"Changelog", velocity.Hyperlink("https://github.com/tensorfoundrylabs/velocity/releases", "releases")}, + } + } + log.RenderRaw(velocity.NewTable( + []string{"Resource", "URL"}, + tableRows, + log.Style(), + )) + log.Newline() + + // --- Inside a ContinuationBlock --- + // + // The canonical server-startup pattern: listening address and dashboard as + // clickable links, all grouped under one timestamped INFO entry. + // Plain URLs are used when stdout is piped, so no OSC 8 sequences reach the file. + apiURL := "http://localhost:8080" + metricsURL := "http://localhost:9090/metrics" + dashURL := "http://localhost:3000" + if stdoutIsTTY { + apiURL = velocity.Hyperlink(apiURL, apiURL) + metricsURL = velocity.Hyperlink(metricsURL, metricsURL) + dashURL = velocity.Hyperlink(dashURL, dashURL) + } + log.Continue(velocity.LevelInfo, "Server listening", + "API: "+apiURL, + "Metrics: "+metricsURL, + "Dashboard: "+dashURL, + ) +} diff --git a/examples/json-logging/main.go b/examples/json-logging/main.go index ae414ec..4e3a4a2 100644 --- a/examples/json-logging/main.go +++ b/examples/json-logging/main.go @@ -10,7 +10,7 @@ import ( "os" "time" - "github.com/tensorfoundrylabs/velocity" + "github.com/tensorfoundrylabs/velocity/v2" ) func main() { @@ -26,7 +26,7 @@ func main() { // Build a logger with console output to stdout and JSON to the temp file. // WithCaller adds the source file and line number to every JSON entry, // which is invaluable when tailing logs in production. - log := velocity.NewWithOptions( + log := velocity.New( velocity.WithConsoleOutput(os.Stdout), velocity.WithLevel(velocity.LevelDebug), velocity.WithStructuredOutput(jsonFile), diff --git a/examples/multi-writer/main.go b/examples/multi-writer/main.go index 7be4b09..2b359a7 100644 --- a/examples/multi-writer/main.go +++ b/examples/multi-writer/main.go @@ -1,7 +1,8 @@ // Multi-writer example. Shows how to route log entries to different // destinations at runtime: a human-readable console, a JSON stream, and a // filtered sink that only captures errors. We also demonstrate WriterFunc -// as a lightweight adapter for custom processing. +// as a lightweight adapter for custom processing, and WriterTrusted() to +// opt a writer into receiving unredacted Secure fields. package main import ( @@ -11,23 +12,25 @@ import ( "sync/atomic" "time" - "github.com/tensorfoundrylabs/velocity" + "github.com/tensorfoundrylabs/velocity/v2" ) func main() { // Main logger writes to stdout for human-readable output. - log := velocity.New(os.Stdout) + log := velocity.New(velocity.WithDevelopment(), velocity.WithConsoleOutput(os.Stdout)) log.Info("Logger ready, adding dynamic writers") // A JSON buffer lets us inspect what the JSON writer received after logging. // In a real service this would be a file or a network socket. + // Marked trusted: this writer receives unredacted Secure fields. jsonBuf := &bytes.Buffer{} jsonWriter := velocity.NewJSONWriter(jsonBuf) - log.AddWriter("json", jsonWriter) + log.AddWriter("json", jsonWriter, velocity.WriterTrusted()) // FilteredWriter wraps another writer and only forwards entries that pass // the predicate. Here we capture anything at Error or above into a separate // buffer so we can ship it to an alerting system later. + // Not trusted: this sink is for alerting, Secure values should be redacted. errorBuf := &bytes.Buffer{} errorSink := velocity.NewFilteredWriter( velocity.NewJSONWriter(errorBuf), @@ -52,10 +55,20 @@ func main() { log.Error("Payment gateway timeout", velocity.String("gateway", "stripe"), velocity.Int("attempt", 3)) log.Info("Background job completed", velocity.String("job", "email-digest")) - // Remove the counter writer now that we've logged what we need. - log.RemoveWriter("counter") + // RemoveWriter returns the writer so the caller can flush or close it. + removed := log.RemoveWriter("counter") + if removed != nil { + _ = removed.Close() + } log.Info("Counter writer removed, this line won't be counted") + // Logger.Writer lets you inspect a registered writer without removing it. + if w := log.Writer("json"); w != nil { + if fw, ok := w.(velocity.FlushableWriter); ok { + _ = fw.Flush() + } + } + // Flush async writers before reading the buffers. if err := log.Close(); err != nil { fmt.Fprintf(os.Stderr, "close error: %v\n", err) diff --git a/examples/notify/main.go b/examples/notify/main.go new file mode 100644 index 0000000..52c836f --- /dev/null +++ b/examples/notify/main.go @@ -0,0 +1,61 @@ +// Notify channel example. Demonstrates the ephemeral operator output pattern +// used by alloy for onboarding URLs — messages that must reach the operator +// regardless of log level, writer configuration, or sampling settings. +// +// Notify bypasses the structured pipeline entirely: no level check, no sampler, +// no JSON writer, no MultiWriter fan-out. The console writer mutex is shared +// so log lines and Notify output cannot interleave on a shared terminal. +package main + +import ( + "fmt" + "os" + "time" + + "github.com/tensorfoundrylabs/velocity/v2" +) + +func main() { + log := velocity.New( + velocity.WithDevelopment(), + velocity.WithConsoleOutput(os.Stdout), + ) + defer func() { _ = log.Close() }() + + // Simulate a bootstrap URL that the operator must open to complete setup. + // In alloy this came from 5+ raw fmt.Fprintf(os.Stderr, ...) calls scattered + // across server.go. NotifyBox collapses that into one intentional call site. + token := "abc123xyz" + setupURL := "https://example.tensorfoundry.io/setup?token=" + token + + // NotifyBox renders to stderr (default) with a visible border so the URL + // stands out even when the terminal is flooded with log output. + // log.Style() returns a mono theme when colour is disabled (NO_COLOR, piped). + log.NotifyBox(velocity.NewBox( + "Setup not complete", + fmt.Sprintf("Open this URL to finish configuring your instance:\n\n %s\n\nThe URL expires in 15 minutes.", setupURL), + log.Style(), + )) + + // Regular structured log — goes through the normal pipeline (console stdout + // in development mode), not to the notify destination. + log.Info("server starting", velocity.String("version", "2.0.0")) + + // Simulate some work. + time.Sleep(10 * time.Millisecond) + log.Info("listening", velocity.String("addr", ":8080")) + + // NotifyLines is the lighter form — useful for simple multi-line operator + // messages without the bordered box treatment. + log.NotifyLines( + "", + " Reminder: setup URL expires soon.", + " "+setupURL, + "", + ) + + // Notify with format string — the most direct form for a single line. + log.Notify("\n Setup complete? Run: tensorfoundry validate --token %s\n\n", token) + + log.Info("example complete") +} diff --git a/examples/pretty-output/main.go b/examples/pretty-output/main.go index 4229d4a..d7e5327 100644 --- a/examples/pretty-output/main.go +++ b/examples/pretty-output/main.go @@ -7,18 +7,19 @@ import ( "fmt" "os" - "github.com/tensorfoundrylabs/velocity" - "github.com/tensorfoundrylabs/velocity/pretty" + "github.com/tensorfoundrylabs/velocity/v2" ) func main() { - log := velocity.NewWithOptions( + log := velocity.New( velocity.WithConsoleOutput(os.Stdout), velocity.WithLevel(velocity.LevelDebug), velocity.WithTheme(velocity.ThemeNightOwl), ) - p := pretty.New(os.Stdout, velocity.ThemeNightOwl) + // NewPrettyFromLogger routes Pretty writes through the logger's console writer + // mutex, so log lines and pretty output cannot interleave. + p := velocity.NewPrettyFromLogger(log) // Banner shows the tool name using the double-border box built into the logger. // Great for the splash screen at startup. @@ -30,29 +31,32 @@ func main() { " |____/ \\___| .__/|_|\\___/ \\__, |", " |_| |___/ ", } - banner := pretty.CreateBanner("Deploy", "4.2.0", "https://deploy.example.com", ascii) + banner := velocity.CreateBanner("Deploy", "4.2.0", "https://deploy.example.com", ascii) fmt.Print(banner) // Section headers make it easy to scan a long run's output. p.Section("Pre-flight Checks") - // StatusFormatter gives you coloured OK/FAIL/WARN/INFO tokens. - // Useful for checklist-style output where the status is the key signal. - sf := log.Status() - fmt.Printf(" %-30s %s\n", "Docker daemon reachable:", sf.Okay("OK")) - fmt.Printf(" %-30s %s\n", "Registry credentials:", sf.Okay("OK")) - fmt.Printf(" %-30s %s\n", "Kubernetes context:", sf.Warn("WARN (non-prod)")) - fmt.Printf(" %-30s %s\n", "Staging namespace exists:", sf.Okay("OK")) - fmt.Printf(" %-30s %s\n", "Production namespace:", sf.Fail("FAIL")) + // Theme.Format(slot, s) colours cell content using semantic slots. + // The theme handles all ANSI construction; call sites stay readable. + style := log.Style() + statusOK := style.Format(velocity.SlotStatusOK, "OK") + statusWarn := style.Format(velocity.SlotStatusWarn, "WARN (non-prod)") + statusFail := style.Format(velocity.SlotStatusFail, "FAIL") + fmt.Printf(" %-30s %s\n", "Docker daemon reachable:", statusOK) + fmt.Printf(" %-30s %s\n", "Registry credentials:", statusOK) + fmt.Printf(" %-30s %s\n", "Kubernetes context:", statusWarn) + fmt.Printf(" %-30s %s\n", "Staging namespace exists:", statusOK) + fmt.Printf(" %-30s %s\n", "Production namespace:", statusFail) p.Section("Environment Info") // SystemInfo is a compact block for key-value pairs under a title. // Perfect for printing build metadata or runtime configuration at startup. - p.SystemInfo(&pretty.SystemInfo{ + p.SystemInfo(&velocity.SystemInfoData{ Title: "Deploy Tool", Version: "4.2.0", - Fields: []pretty.KeyValuePair{ + Fields: []velocity.KeyValuePair{ {Key: "Target cluster", Value: "k8s-staging-au-east-1"}, {Key: "Namespace", Value: "app-staging"}, {Key: "Image", Value: "registry.example.com/app:v4.2.0"}, @@ -112,27 +116,27 @@ func main() { // Tree shows hierarchical relationships. Each TreeItem can have children, // and velocity draws the connecting lines automatically. - p.Tree([]pretty.TreeItem{ + p.Tree([]velocity.TreeItem{ { Key: "app (v4.2.0)", - Children: []pretty.TreeItem{ + Children: []velocity.TreeItem{ { Key: "postgres (primary)", - Children: []pretty.TreeItem{ + Children: []velocity.TreeItem{ {Key: "max_connections", Value: 200}, {Key: "pool_size", Value: 20}, }, }, { Key: "redis (cache)", - Children: []pretty.TreeItem{ + Children: []velocity.TreeItem{ {Key: "eviction_policy", Value: "allkeys-lru"}, {Key: "max_memory", Value: "256mb"}, }, }, { Key: "payments-api (external)", - Children: []pretty.TreeItem{ + Children: []velocity.TreeItem{ {Key: "timeout", Value: "5s"}, {Key: "retries", Value: 3}, }, diff --git a/examples/progress/main.go b/examples/progress/main.go index d1add89..2744bda 100644 --- a/examples/progress/main.go +++ b/examples/progress/main.go @@ -7,17 +7,17 @@ import ( "os" "time" - "github.com/tensorfoundrylabs/velocity" - "github.com/tensorfoundrylabs/velocity/pretty" + "github.com/tensorfoundrylabs/velocity/v2" + "github.com/tensorfoundrylabs/velocity/v2/live" ) func main() { - log := velocity.New(os.Stdout) + log := velocity.New(velocity.WithDevelopment(), velocity.WithConsoleOutput(os.Stdout)) log.Info("Starting deployment pipeline") // Show a progress bar simulating a dependency download. // Total is the number of packages we're pretending to fetch. - pb := pretty.NewProgressBar(os.Stdout, 10, "Downloading deps") + pb := live.NewProgressBar(os.Stdout, 10, "Downloading deps") for i := range int64(10) { time.Sleep(80 * time.Millisecond) pb.Increment(1) @@ -32,19 +32,19 @@ func main() { // Cycle through all five spinner styles so you can see what each looks like. // Each one runs for about half a second, which is enough to see a few frames. spinners := []struct { - style pretty.SpinnerStyle + style live.SpinnerStyle label string success string }{ - {pretty.SpinnerStyleBraille, "Compiling (braille)...", "Compiled"}, - {pretty.SpinnerStyleDots, "Linking (dots)...", "Linked"}, - {pretty.SpinnerStyleArrows, "Packaging (arrows)...", "Packaged"}, - {pretty.SpinnerStyleBounce, "Pushing image (bounce)...", "Image pushed"}, - {pretty.SpinnerStyleBar, "Health check (bar)...", ""}, + {live.SpinnerStyleBraille, "Compiling (braille)...", "Compiled"}, + {live.SpinnerStyleDots, "Linking (dots)...", "Linked"}, + {live.SpinnerStyleArrows, "Packaging (arrows)...", "Packaged"}, + {live.SpinnerStyleBounce, "Pushing image (bounce)...", "Image pushed"}, + {live.SpinnerStyleBar, "Health check (bar)...", ""}, } for i, sp := range spinners { - s := pretty.NewSpinner(os.Stdout, sp.label) + s := live.NewSpinner(os.Stdout, sp.label) s.SetStyle(sp.style) time.Sleep(500 * time.Millisecond) @@ -58,7 +58,7 @@ func main() { // Second progress bar: simulating a rollback after the failed health check. log.Warn("Rolling back to previous version") - rb := pretty.NewProgressBar(os.Stdout, 5, "Rolling back") + rb := live.NewProgressBar(os.Stdout, 5, "Rolling back") for range int64(5) { time.Sleep(100 * time.Millisecond) rb.Increment(1) diff --git a/examples/ring-buffer/main.go b/examples/ring-buffer/main.go new file mode 100644 index 0000000..f2233ee --- /dev/null +++ b/examples/ring-buffer/main.go @@ -0,0 +1,101 @@ +// Ring buffer writer example. Shows how to attach a RingBufferWriter to a +// logger for in-process log capture — the pattern foundryos uses to serve +// recent log entries over an HTTP debug endpoint. +// +// Two access patterns are demonstrated: +// 1. Snapshot — pull the most recent N entries on demand (HTTP handler style) +// 2. Subscribe — push every new entry to a channel (live tail / alerting style) +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/tensorfoundrylabs/velocity/v2" +) + +func main() { + // Attach a ring that holds the last 100 entries. + // Untrusted by default — Secure fields are redacted when read via Snapshot. + ring := velocity.NewRingBufferWriter(100) + + log := velocity.New( + velocity.WithDevelopment(), + velocity.WithConsoleOutput(os.Stdout), + ) + log.AddWriter("ring", ring) + + log.Info("Logger ready, ring attached") + + // Simulate a few requests so the ring has something to show. + routes := []string{"/api/users", "/api/orders", "/api/health"} + for i, route := range routes { + log.Info("Request handled", + velocity.String("route", route), + velocity.Int("status", 200), + velocity.Duration("latency", time.Duration(i+1)*15*time.Millisecond), + ) + } + log.Warn("Slow query detected", velocity.Duration("elapsed", 320*time.Millisecond)) + log.Error("Upstream timeout", velocity.String("service", "payments")) + + fmt.Println() + + // --- Pattern 2: subscribe (live tail) --- + // + // Start the subscriber BEFORE closing the logger so the ring is still open. + // A background goroutine receives every new snapshot as it arrives. + // The channel buffers 16 entries; slow consumers drop, not block. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ch := ring.Subscribe(ctx, 16) + + done := make(chan struct{}) + go func() { + defer close(done) + fmt.Println("=== Subscriber: live tail ===") + for snap := range ch { + fmt.Printf(" -> [%s] %s\n", snap.Level, snap.Message) + } + fmt.Println(" subscriber done") + }() + + // Log a few more entries while the subscriber is active. + for _, msg := range []string{"Cron job started", "Cron job finished"} { + log.Info(msg) + } + + // Close flushes the async MultiWriter so all entries reach the ring. + // This also closes the ring, which closes all subscriber channels. + // The subscriber goroutine exits cleanly when its channel is closed. + if err := log.Close(); err != nil { + fmt.Fprintf(os.Stderr, "close error: %v\n", err) + } + + // Wait for the subscriber goroutine to finish draining. + <-done + + fmt.Println() + + // --- Pattern 1: snapshot (HTTP debug endpoint) --- + // + // Grab the last 3 entries from the now-closed ring. + // In a real service this is called inside an http.HandlerFunc and the + // result is JSON-encoded into the response. + snaps := ring.Snapshot(3) + fmt.Printf("=== Snapshot: last %d entries ===\n", len(snaps)) + for _, s := range snaps { + fmt.Printf(" [%s] %s", s.Level, s.Message) + for _, f := range s.Fields { + fmt.Printf(" %s=%s", f.Key, f.Value) + } + fmt.Println() + } + + s := ring.Stats() + fmt.Printf("\nRing stats: capacity=%d fill=%d total=%d drops=%d\n", + s.Capacity, s.Fill, s.Total, s.Drops) +} diff --git a/examples/sampling/main.go b/examples/sampling/main.go index 11f8725..ab8efcf 100644 --- a/examples/sampling/main.go +++ b/examples/sampling/main.go @@ -8,7 +8,7 @@ import ( "os" "sync/atomic" - "github.com/tensorfoundrylabs/velocity" + "github.com/tensorfoundrylabs/velocity/v2" ) func main() { @@ -24,7 +24,8 @@ func main() { // This is a common pattern: capture the burst at startup, then sample steady-state noise. sampler := velocity.NewCountSampler(5, 100) - log := velocity.NewWithOptions( + log := velocity.New( + velocity.WithDevelopment(), velocity.WithConsoleOutput(os.Stdout), velocity.WithLevel(velocity.LevelInfo), velocity.WithSampler(sampler), diff --git a/examples/secure/main.go b/examples/secure/main.go new file mode 100644 index 0000000..6b87521 --- /dev/null +++ b/examples/secure/main.go @@ -0,0 +1,102 @@ +// Secure example demonstrates field-level redaction and tag scanning. +// +// Run on a TTY: +// +// go run ./examples/secure +// +// Pipe through cat to simulate a non-TTY (redacts automatically): +// +// go run ./examples/secure | cat +package main + +import ( + "fmt" + "os" + + velocity "github.com/tensorfoundrylabs/velocity/v2" +) + +func main() { + // ---- setup --------------------------------------------------------------- + // One JSON log file (always untrusted — secure fields are redacted there). + jsonFile, err := os.CreateTemp("", "velocity-secure-*.json") + if err != nil { + fmt.Fprintf(os.Stderr, "temp file: %v\n", err) + os.Exit(1) + } + defer func() { _ = os.Remove(jsonFile.Name()) }() + defer func() { _ = jsonFile.Close() }() + + // Console logger. TTY gets plaintext; pipe/file gets redacted automatically. + log := velocity.New(velocity.WithDevelopment()) + log.AddWriter("json-file", velocity.NewJSONWriter(jsonFile)) + + // A second JSON writer opted into trust — receives plaintext for audit log. + auditFile, _ := os.CreateTemp("", "velocity-audit-*.json") + defer func() { _ = os.Remove(auditFile.Name()) }() + defer func() { _ = auditFile.Close() }() + log.AddWriter("audit", velocity.NewJSONWriter(auditFile), velocity.WriterTrusted()) + + fmt.Println("--- Secure field constructors ---") + + // Secure(key, value): shows plaintext on TTY console, [REDACTED] in JSON log. + log.Info("user authenticated", + velocity.Secure("session_token", "tok_abc123def456"), + ) + + // SecureURL(key, url): redacts the password portion of the URL everywhere + // except trusted writers. The host and path are preserved in the redacted form. + log.Info("database connected", + velocity.SecureURL("dsn", "postgres://app:s3cretP4ss@db.internal:5432/mydb"), + ) + + // Redacted(key): permanently hidden — no plaintext stored, not even on trusted writers. + log.Info("request received", + velocity.Redacted("api_key"), + ) + + // Truncated(key, val, maxLen): shows a safe prefix, appends '…' when clipped. + // Useful for bearer tokens where the prefix identifies the token type. + log.Info("bearer presented", + velocity.Truncated("token", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig", 20), + ) + + fmt.Println() + fmt.Println("--- tag in message ---") + + // The ... tag form works for unstructured message strings. + // On TTY console: markers stripped, content shown. + // On non-TTY / JSON: markers and content replaced with [REDACTED]. + log.Info("connecting to cache at redis://admin:hunter2@cache.internal:6379") + log.Warn("auth failed for user@domain.com; locking account") + + fmt.Println() + fmt.Println("--- Multi-writer trust divergence ---") + + // Mixed trusted + untrusted writers on the same log call. + log.Info("session created", + velocity.Secure("session", "sess_XyZ789"), + velocity.String("user_id", "u_42"), + ) + + _ = log.Close() + + // ---- print JSON outputs for visual inspection ---------------------------- + fmt.Println() + fmt.Println("--- JSON log (untrusted, should be redacted) ---") + printFile(jsonFile) + + fmt.Println() + fmt.Println("--- Audit log (trusted, should show plaintext) ---") + printFile(auditFile) +} + +func printFile(f *os.File) { + if _, err := f.Seek(0, 0); err != nil { + fmt.Fprintf(os.Stderr, "seek: %v\n", err) + return + } + buf := make([]byte, 8192) + n, _ := f.Read(buf) + _, _ = os.Stdout.Write(buf[:n]) +} diff --git a/examples/slog-bridge/main.go b/examples/slog-bridge/main.go index cee4c6d..a3e0125 100644 --- a/examples/slog-bridge/main.go +++ b/examples/slog-bridge/main.go @@ -8,17 +8,17 @@ import ( "log/slog" "os" - "github.com/tensorfoundrylabs/velocity" - velocityslog "github.com/tensorfoundrylabs/velocity/slog" + "github.com/tensorfoundrylabs/velocity/v2" + slogbridge "github.com/tensorfoundrylabs/velocity/v2/slogbridge" ) func main() { // Start with a standard velocity logger that writes to stdout. - vlog := velocity.New(os.Stdout) + vlog := velocity.New(velocity.WithDevelopment(), velocity.WithConsoleOutput(os.Stdout)) vlog.Info("Velocity logger initialised") // Wrap it so existing slog callers don't need any changes. - sl := velocityslog.NewLogger(vlog) + sl := slogbridge.NewLogger(vlog) slog.SetDefault(sl) // From here on, all slog calls route through velocity's writers. diff --git a/examples/status-items/main.go b/examples/status-items/main.go new file mode 100644 index 0000000..886ec83 --- /dev/null +++ b/examples/status-items/main.go @@ -0,0 +1,100 @@ +// status-items demonstrates StatusItem rendering and Logger.Status routing. +// +// Run it directly for a TTY console with coloured badges: +// +// go run ./examples/status-items +// +// Pipe it to see the plain (non-TTY) badge form: +// +// go run ./examples/status-items | cat +// +// Add -json to write JSON to a file alongside the console output: +// +// go run ./examples/status-items -json +package main + +import ( + "flag" + "os" + "time" + + velocity "github.com/tensorfoundrylabs/velocity/v2" +) + +func main() { + jsonOut := flag.Bool("json", false, "also write JSON to status.log") + flag.Parse() + + opts := []velocity.Option{ + velocity.WithDevelopment(), + } + if *jsonOut { + f, err := os.Create("status.log") + if err != nil { + panic(err) + } + defer func() { + if err := f.Close(); err != nil { + panic(err) + } + }() + opts = append(opts, velocity.WithStructuredOutput(f)) + } + + log := velocity.New(opts...) + defer func() { + if err := log.Close(); err != nil { + panic(err) + } + }() + + log.Info("starting service health check") + log.Newline() + + // Startup checklist: six services, mixed outcomes. + log.Status(velocity.LevelInfo, velocity.StatusOK, "postgres connected", + velocity.String("host", "db.internal"), + velocity.Duration("latency", 4*time.Millisecond), + ) + + log.Status(velocity.LevelInfo, velocity.StatusOK, "redis connected", + velocity.String("host", "cache.internal"), + velocity.Duration("latency", 1*time.Millisecond), + ) + + log.Status(velocity.LevelInfo, velocity.StatusWarn, "object storage degraded", + velocity.String("bucket", "assets-prod"), + velocity.String("region", "ap-southeast-2"), + velocity.Duration("latency", 320*time.Millisecond), + ) + + log.Status(velocity.LevelError, velocity.StatusFail, "payment gateway unreachable", + velocity.String("provider", "stripe"), + velocity.Error("reason", os.ErrDeadlineExceeded), + ) + + log.Status(velocity.LevelInfo, velocity.StatusPending, "feature flags syncing", + velocity.String("remote", "flags.internal"), + ) + + log.Status(velocity.LevelInfo, velocity.StatusSkipped, "telemetry export", + velocity.String("reason", "disabled in config"), + ) + + log.Newline() + + // Standalone StatusItem rendered via Logger.Render for inline display. + // TTY is detected automatically from the console writer at render time. + log.Info("re-checking payment gateway") + item := velocity.NewStatusItem( + velocity.StatusOK, + "payment gateway recovered", + log.Style(), + velocity.String("provider", "stripe"), + velocity.Duration("rtt", 12*time.Millisecond), + ) + log.Render(item) + + log.Newline() + log.Info("health check complete") +} diff --git a/examples/tables/main.go b/examples/tables/main.go index cdca5f8..b0c3c36 100644 --- a/examples/tables/main.go +++ b/examples/tables/main.go @@ -1,60 +1,61 @@ // Package main demonstrates velocity's table rendering for structured // terminal output. Tables auto-size columns and handle ANSI colour codes -// in cell content (e.g. from StatusFormatter) without breaking alignment. +// in cell content without breaking alignment. package main import ( "fmt" "os" - "github.com/tensorfoundrylabs/velocity" - "github.com/tensorfoundrylabs/velocity/pretty" + "github.com/tensorfoundrylabs/velocity/v2" ) func main() { - log := velocity.NewWithOptions( + log := velocity.New( velocity.WithConsoleOutput(os.Stdout), velocity.WithTheme(velocity.ThemeNightOwl), velocity.WithLevel(velocity.LevelDebug), ) - sf := log.Status() - p := pretty.NewFromLogger(log) + // Theme.Format(slot, s) is the canonical way to colour cell content in v2. + // The theme handles all ANSI construction; callers just pick a semantic slot. + // log.Style() returns a mono theme when colour is disabled (piped, NO_COLOR). + style := log.Style() fmt.Println("=== Pretty Table ===") fmt.Println() log.Info("service health check results") - log.RenderRaw(p.NewTable( + log.RenderRaw(velocity.NewTable( []string{"Service", "Status", "Latency", "Region"}, [][]string{ - {"auth-api", sf.Okay("HEALTHY"), "12ms", "us-east-1"}, - {"payments", sf.Okay("HEALTHY"), "45ms", "us-east-1"}, - {"search", sf.Warn("DEGRADED"), "380ms", "eu-west-1"}, - {"notifications", sf.Fail("DOWN"), "-", "ap-southeast-2"}, - {"analytics", sf.Okay("HEALTHY"), "28ms", "us-west-2"}, + {"auth-api", style.Format(velocity.SlotStatusOK, "HEALTHY"), "12ms", "us-east-1"}, + {"payments", style.Format(velocity.SlotStatusOK, "HEALTHY"), "45ms", "us-east-1"}, + {"search", style.Format(velocity.SlotStatusWarn, "DEGRADED"), "380ms", "eu-west-1"}, + {"notifications", style.Format(velocity.SlotStatusFail, "DOWN"), "-", "ap-southeast-2"}, + {"analytics", style.Format(velocity.SlotStatusOK, "HEALTHY"), "28ms", "us-west-2"}, }, + style, )) log.Newline() fmt.Println("=== GPU Node Table ===") fmt.Println() - log.RenderRaw(p.NewTable( + log.RenderRaw(velocity.NewTable( []string{"Node", "GPU", "Memory", "Utilisation", "Temperature"}, [][]string{ - {"node-0", "A100 80GB", "72.3 / 80.0 GB", sf.Okay("89%"), "68C"}, - {"node-1", "A100 80GB", "65.1 / 80.0 GB", sf.Okay("81%"), "65C"}, - {"node-2", "A100 80GB", "78.9 / 80.0 GB", sf.Warn("98%"), "82C"}, - {"node-3", "A100 80GB", "0.0 / 80.0 GB", sf.Fail("0%"), "34C"}, + {"node-0", "A100 80GB", "72.3 / 80.0 GB", style.Format(velocity.SlotStatusOK, "89%"), "68C"}, + {"node-1", "A100 80GB", "65.1 / 80.0 GB", style.Format(velocity.SlotStatusOK, "81%"), "65C"}, + {"node-2", "A100 80GB", "78.9 / 80.0 GB", style.Format(velocity.SlotStatusWarn, "98%"), "82C"}, + {"node-3", "A100 80GB", "0.0 / 80.0 GB", style.Format(velocity.SlotStatusFail, "0%"), "34C"}, }, + style, )) log.Newline() - // Tables work without colour too. pretty.New(os.Stdout, nil) demonstrates - // the standalone constructor without a logger or theme. + // Tables work without colour too — log.Style() is already mono when piped. fmt.Println("=== Plain Table (no theme, no colour) ===") fmt.Println() - plain := pretty.New(os.Stdout, nil) - log.RenderRaw(plain.NewTable( + log.RenderRaw(velocity.NewTable( []string{"Endpoint", "Method", "Calls/sec", "P99"}, [][]string{ {"/v1/chat/completions", "POST", "1,240", "89ms"}, @@ -62,13 +63,14 @@ func main() { {"/v1/models", "GET", "450", "3ms"}, {"/health", "GET", "10,000", "1ms"}, }, + style, )) log.Newline() // Wide table with many columns. Columns auto-size to content. fmt.Println("=== Wide Table (auto-sized columns) ===") fmt.Println() - log.RenderRaw(p.NewTable( + log.RenderRaw(velocity.NewTable( []string{"PID", "User", "CPU%", "Mem%", "VSZ", "RSS", "TTY", "Stat", "Command"}, [][]string{ {"1", "root", "0.0", "0.1", "168k", "12k", "?", "Ss", "/sbin/init"}, @@ -76,21 +78,22 @@ func main() { {"1204", "nginx", "0.3", "0.2", "32M", "8M", "?", "S", "nginx: worker process"}, {"1891", "prometheus", "1.2", "0.8", "256M", "64M", "?", "Sl", "/usr/bin/prometheus"}, }, + style, )) log.Newline() - // Use log.Render to nest a small table under a related log line. The table - // indents to the message column, visually grouping with the entry above. - // Best for narrow tables; wide tables still want RenderRaw to avoid wrapping. - fmt.Println("=== Indented Table (under a log line via log.Render) ===") + // log.Table is the convenience form: it calls log.Style() for the theme and + // routes through Logger.Render, so the table indents to the message column. + // Equivalent to log.Render(velocity.NewTable(..., log.Style())) but shorter. + fmt.Println("=== Indented Table (under a log line via log.Table) ===") fmt.Println() log.Info("migrations applied", velocity.Int("count", 3)) - log.Render(p.NewTable( + log.Table( []string{"Migration", "Duration", "Status"}, [][]string{ - {"001_initial_schema.sql", "5ms", sf.Okay("OK")}, - {"002_webhooks.sql", "2ms", sf.Okay("OK")}, - {"003_model_access.sql", "3ms", sf.Okay("OK")}, + {"001_initial_schema.sql", "5ms", style.Format(velocity.SlotStatusOK, "OK")}, + {"002_webhooks.sql", "2ms", style.Format(velocity.SlotStatusOK, "OK")}, + {"003_model_access.sql", "3ms", style.Format(velocity.SlotStatusOK, "OK")}, }, - )) + ) } diff --git a/examples/terminal-velocity/main.go b/examples/terminal-velocity/main.go index b397dde..8663fb1 100644 --- a/examples/terminal-velocity/main.go +++ b/examples/terminal-velocity/main.go @@ -1,10 +1,20 @@ -// Terminal Velocity - GPU cluster deploy simulator. This is the hero example -// for the velocity logging library. It walks through deploying Llama-3.1-70B -// across a 4-node GPU cluster, exercising banners, spinners, progress bars, -// structured fields, tree views, tables, and pretty output along the way. +// Terminal Velocity — the flagship example for velocity v2. // -// Node-3 has a disk space issue, which causes it to fail and trigger a -// recovery path. Real deployments are rarely clean happy-paths. +// This simulates deploying Llama-3.1-70B across a 4-node GPU cluster and +// exercises every major v2 API in one coherent narrative: +// +// - Branded ASCII banner +// - Spinner (cluster scan), ProgressBar (weight download, container build) +// - Tree (deployment plan), Table (preflight, health), SystemInfo, Box +// - Logger.Status — staged checklist transitions +// - Logger.Group — route registration block +// - Logger.Continue — inline server-listening block with hyperlinks +// - velocity.Secure — config secret field with trust-model demo +// - velocity.Notify / NotifyBox — operator URL callout +// - RingBufferWriter — in-process log capture; snapshot printed at the end +// +// Node-3 has a disk space issue, triggering a recovery path. Real +// deployments are rarely happy-paths. package main import ( @@ -14,37 +24,54 @@ import ( "strings" "time" - "github.com/tensorfoundrylabs/velocity" - "github.com/tensorfoundrylabs/velocity/pretty" + "github.com/tensorfoundrylabs/velocity/v2" + "github.com/tensorfoundrylabs/velocity/v2/live" ) +// link is a TTY-aware wrapper for velocity.Hyperlink. OSC 8 sequences are only +// emitted when stdout is an actual terminal that supports them; plain text is +// returned otherwise so no control sequences reach pipes or log aggregators. +func link(uri, text string) string { + if velocity.IsTerminalWriter(os.Stdout) && velocity.HyperlinksSupported() { + return velocity.Hyperlink(uri, text) + } + return text +} + func main() { startTime := time.Now() - // Night Owl gives us a dark, high-contrast palette that looks excellent on - // any decent terminal. It's the default for a reason. - log := velocity.NewWithOptions( + // Ring buffer captures everything the logger writes for the end-of-run + // diagnostic snapshot — the same pattern used by foundryos debug endpoints. + ring := velocity.NewRingBufferWriter(256) + + log := velocity.New( velocity.WithTheme(velocity.ThemeNightOwl), velocity.WithConsoleOutput(os.Stdout), velocity.WithLevel(velocity.LevelDebug), ) + log.AddWriter("ring", ring) defer func() { _ = log.Close() }() - p := pretty.NewFromLogger(log) + p := velocity.NewPrettyFromLogger(log) stageBanner(log) stageClusterDiscovery(log, p) stageDeploymentConfig(log, p) + stageSecureConfig(log, p) stagePreflightChecks(log, p) + stageRouteRegistration(log) stageModelDistribution(log, p) failed := stageNodeDeployment(log, p) stageRecovery(log, p, failed) stageHealthVerification(log, p) - stageSummary(log, p, startTime) + stageSummary(log, p, startTime, ring) } -// stageBanner prints the title screen. Simple ASCII art keeps it portable -// across terminals that might not handle fancy Unicode block characters. +// --- Banner --------------------------------------------------------------- + +// stageBanner prints the title screen. Plain ASCII keeps it portable +// across terminals that might not handle Unicode block art. func stageBanner(log *velocity.Logger) { ascii := []string{ " ______ _ __ ", @@ -59,23 +86,24 @@ func stageBanner(log *velocity.Logger) { " /____/ ", } - banner := pretty.CreateBanner("Terminal Velocity", "0.1.0", "tensorfoundry.io", ascii) - log.Banner(strings.Split(strings.TrimRight(banner, "\n"), "\n")...) + banner := velocity.CreateBanner("Terminal Velocity", "2.0.0", "tensorfoundry.io", ascii) + log.BannerLines(strings.Split(strings.TrimRight(banner, "\n"), "\n")...) log.Newline() } -// stageClusterDiscovery scans for available GPU nodes and reports what it finds. -func stageClusterDiscovery(log *velocity.Logger, p *pretty.Pretty) { +// --- Cluster discovery ---------------------------------------------------- + +func stageClusterDiscovery(log *velocity.Logger, p *velocity.Pretty) { p.Section("Cluster Discovery") - spinner := pretty.NewSpinner(os.Stdout, "Scanning network for GPU nodes...") + spinner := live.NewSpinner(os.Stdout, "Scanning network for GPU nodes...") time.Sleep(1200 * time.Millisecond) spinner.StopWithSuccess("Found 4 nodes with 16 GPUs total") - p.SystemInfo(&pretty.SystemInfo{ + p.SystemInfo(&velocity.SystemInfoData{ Title: "GPU Cluster", Version: "CUDA 13.0", - Fields: []pretty.KeyValuePair{ + Fields: []velocity.KeyValuePair{ {Key: "Nodes", Value: "4"}, {Key: "GPUs Total", Value: "8 x NVIDIA RTX Pro 6000 96GB"}, {Key: "CUDA Version", Value: "13.0"}, @@ -85,35 +113,34 @@ func stageClusterDiscovery(log *velocity.Logger, p *pretty.Pretty) { }, }) - log.Info("Cluster discovery complete", + log.Info("cluster discovery complete", velocity.Int("nodes", 4), - velocity.Int("gpus", 8), + velocity.Int("gpus", 16), velocity.String("cuda", "13.0"), ) log.Newline() } -// stageDeploymentConfig displays the model deployment configuration as a tree. -func stageDeploymentConfig(log *velocity.Logger, p *pretty.Pretty) { +// --- Deployment plan ------------------------------------------------------ + +func stageDeploymentConfig(log *velocity.Logger, p *velocity.Pretty) { p.Section("Deployment Configuration") - // Render the tree indented under the log line — this is the explicit "nest under - // message column" path, using log.Render with a Renderable result type. - log.Info("Llama-3.1-70B Deployment Plan") - log.Render(p.NewTree([]pretty.TreeItem{ + log.Info("Llama-3.1-70B deployment plan") + log.Render(velocity.NewTree([]velocity.TreeItem{ {Key: "Model", Value: "meta-llama/Llama-3.1-70B-Instruct"}, {Key: "Replicas", Value: 4}, {Key: "GPU Type", Value: "NVIDIA RTX Pro 6000 96GB"}, { Key: "Parallelism", - Children: []pretty.TreeItem{ + Children: []velocity.TreeItem{ {Key: "Tensor Parallelism", Value: 2}, {Key: "Pipeline Parallelism", Value: 1}, }, }, { Key: "Quantisation", - Children: []pretty.TreeItem{ + Children: []velocity.TreeItem{ {Key: "Method", Value: "AWQ"}, {Key: "Bits", Value: "4-bit"}, {Key: "Group Size", Value: 128}, @@ -121,44 +148,77 @@ func stageDeploymentConfig(log *velocity.Logger, p *pretty.Pretty) { }, {Key: "Max Batch Size", Value: 32}, {Key: "Max Sequence Length", Value: 8192}, - })) + }, log.Style())) log.Newline() } -// stagePreflightChecks runs pre-flight validation across all nodes and reports results. -// Node-3 fails the disk space check, which foreshadows the deployment failure. -func stagePreflightChecks(log *velocity.Logger, p *pretty.Pretty) { +// --- Secure config demo --------------------------------------------------- + +// stageSecureConfig demonstrates the Secure field trust model. The console +// writer (TTY) shows plaintext; a JSON writer would redact. We also show +// tag scanning in the message string. +func stageSecureConfig(log *velocity.Logger, p *velocity.Pretty) { + p.Section("Secure Configuration") + + // Secure("key", val) — plaintext on TTY, [REDACTED] on non-TTY / JSON. + // This is the pattern for API keys, session tokens, and similar secrets + // that operators need to see locally but must never reach a log aggregator. + log.Info("loading inference server config", + velocity.Secure("api_key", "sk-live-7f3a9b2c4e1d8f60"), + velocity.SecureURL("registry_dsn", "https://registry:s3cret@models.internal/v2"), + velocity.String("model_path", "/mnt/models/llama-3.1-70b-awq"), + ) + + // tag scanning works in message strings — same TTY vs. non-TTY + // divergence without needing a structured field. + log.Info("mounted model checkpoint at /mnt/models/llama-3.1-70b-awq/shard-0") + + // Redacted is always hidden — not even trusted writers see the value. + // Use it for fields you want present in the schema but never logged. + log.Debug("auth context attached", + velocity.Redacted("bearer_token"), + velocity.String("scope", "inference:read"), + ) + + log.Newline() +} + +// --- Pre-flight checks ---------------------------------------------------- + +// stagePreflightChecks runs validation across all nodes. Node-3 fails the +// disk space check, foreshadowing the deployment failure later. +func stagePreflightChecks(log *velocity.Logger, p *velocity.Pretty) { p.Section("Pre-flight Checks") - sf := log.Status() + style := log.Style() + okCell := style.Format(velocity.SlotStatusOK, "OK") + warnCell := style.Format(velocity.SlotStatusWarn, "WARN") rows := [][]string{ - {"GPU Memory", "node-0", sf.Okay("OK"), "79.8 GB free"}, - {"GPU Memory", "node-1", sf.Okay("OK"), "79.8 GB free"}, - {"GPU Memory", "node-2", sf.Okay("OK"), "79.8 GB free"}, - {"GPU Memory", "node-3", sf.Okay("OK"), "79.8 GB free"}, - {"CUDA Version", "node-0", sf.Okay("OK"), "12.4 / driver 550.54.15"}, - {"CUDA Version", "node-1", sf.Okay("OK"), "12.4 / driver 550.54.15"}, - {"CUDA Version", "node-2", sf.Okay("OK"), "12.4 / driver 550.54.15"}, - {"CUDA Version", "node-3", sf.Okay("OK"), "12.4 / driver 550.54.15"}, - {"Disk Space", "node-0", sf.Okay("OK"), "340 GB free"}, - {"Disk Space", "node-1", sf.Okay("OK"), "280 GB free"}, - {"Disk Space", "node-2", sf.Okay("OK"), "310 GB free"}, - {"Disk Space", "node-3", sf.Warn("WARN"), "18 GB free (need 35 GB)"}, - {"Network", "node-0", sf.Okay("OK"), "IB latency 1.2us"}, - {"Network", "node-1", sf.Okay("OK"), "IB latency 1.1us"}, - {"Network", "node-2", sf.Okay("OK"), "IB latency 1.3us"}, - {"Network", "node-3", sf.Okay("OK"), "IB latency 1.2us"}, + {"GPU Memory", "node-0", okCell, "79.8 GB free"}, + {"GPU Memory", "node-1", okCell, "79.8 GB free"}, + {"GPU Memory", "node-2", okCell, "79.8 GB free"}, + {"GPU Memory", "node-3", okCell, "79.8 GB free"}, + {"CUDA Version", "node-0", okCell, "12.4 / driver 550.54.15"}, + {"CUDA Version", "node-1", okCell, "12.4 / driver 550.54.15"}, + {"CUDA Version", "node-2", okCell, "12.4 / driver 550.54.15"}, + {"CUDA Version", "node-3", okCell, "12.4 / driver 550.54.15"}, + {"Disk Space", "node-0", okCell, "340 GB free"}, + {"Disk Space", "node-1", okCell, "280 GB free"}, + {"Disk Space", "node-2", okCell, "310 GB free"}, + {"Disk Space", "node-3", warnCell, "18 GB free (need 35 GB)"}, + {"Network", "node-0", okCell, "IB latency 1.2us"}, + {"Network", "node-1", okCell, "IB latency 1.1us"}, + {"Network", "node-2", okCell, "IB latency 1.3us"}, + {"Network", "node-3", okCell, "IB latency 1.2us"}, } - p.Table( - []string{"Check", "Node", "Status", "Detail"}, - rows, - ) + p.Table([]string{"Check", "Node", "Status", "Detail"}, rows) - // Flag the disk issue immediately so the operator has a chance to notice. - log.Warn("node-3 disk space is critically low; deploy will attempt but may fail", + // Logger.Status uses StatusKind to produce a coloured badge in the console + // and a structured "status" field in JSON — no raw ANSI needed at the call site. + log.Status(velocity.LevelWarn, velocity.StatusWarn, "node-3 disk space critically low", velocity.String("node", "node-3"), velocity.String("available", "18 GB"), velocity.String("required", "35 GB"), @@ -167,24 +227,49 @@ func stagePreflightChecks(log *velocity.Logger, p *pretty.Pretty) { log.Newline() } +// --- Route registration --------------------------------------------------- + +// stageRouteRegistration shows Logger.Group for count-headed indented blocks. +// This is exactly the pattern used by olla's translator route registration. +func stageRouteRegistration(log *velocity.Logger) { + log.Group(velocity.LevelInfo, "Registering inference API routes", + velocity.GroupItem{Text: "POST /v1/chat/completions"}, + velocity.GroupItem{Text: "POST /v1/completions"}, + velocity.GroupItem{Text: "POST /v1/embeddings"}, + velocity.GroupItem{Text: "GET /v1/models"}, + velocity.GroupItem{Text: "GET /health"}, + velocity.GroupItem{Text: "GET /metrics"}, + ) + + log.Newline() + + // Continue places all lines under one timestamped INFO entry. OSC 8 + // hyperlinks are only emitted when stdout is a TTY that supports them; + // plain URLs are used otherwise so no control sequences reach pipes. + log.Continue(velocity.LevelInfo, "Inference server listening", + "API: "+link("http://10.0.1.10:8080/v1", "http://10.0.1.10:8080/v1"), + "Metrics: "+link("http://10.0.1.10:9090/metrics", "http://10.0.1.10:9090/metrics"), + "Press Ctrl+C to stop", + ) + + log.Newline() +} + +// --- Model distribution --------------------------------------------------- + // stageModelDistribution downloads model weights and builds inference containers. -// This is the longest stage because it moves the most data. -func stageModelDistribution(log *velocity.Logger, p *pretty.Pretty) { +func stageModelDistribution(log *velocity.Logger, p *velocity.Pretty) { p.Section("Model Distribution") - // Child logger carries the stage context on every structured entry without - // us having to repeat it on every log call. distLog := log.With(velocity.String("stage", "distribute")) - const weightBytes int64 = 35_000 // units = MB (35 GB quantised) + const weightBytes int64 = 35_000 // MB - pb := pretty.NewProgressBar(os.Stdout, weightBytes, "Downloading model weights") + pb := live.NewProgressBar(os.Stdout, weightBytes, "Downloading model weights") - // Drive the progress bar without logging mid-loop. Mixing log writes with - // a progress bar on the same writer causes line-overwrite interleaving. var downloaded int64 for downloaded < weightBytes { - chunk := 700 + (downloaded/1000)%400 // speed varies a bit + chunk := 700 + (downloaded/1000)%400 downloaded += chunk if downloaded > weightBytes { downloaded = weightBytes @@ -192,17 +277,14 @@ func stageModelDistribution(log *velocity.Logger, p *pretty.Pretty) { pb.Update(downloaded) time.Sleep(18 * time.Millisecond) } - pb.Complete() - // Log the milestone after the bar has finished and emitted its newline. distLog.Info("model weights verified", velocity.Int64("size_mb", weightBytes), velocity.String("checksum", "sha256:a3f9...d12e"), ) - // Container build is quicker but still worth showing. - cb := pretty.NewProgressBar(os.Stdout, 15, "Building inference containers") + cb := live.NewProgressBar(os.Stdout, 15, "Building inference containers") layers := []string{ "base: nvcr.io/nvidia/pytorch:24.01", "layer: vllm==0.4.2", @@ -211,8 +293,6 @@ func stageModelDistribution(log *velocity.Logger, p *pretty.Pretty) { "layer: serving config", } - // Accumulate completed layers and log them after the bar is done so log - // output does not interleave with the progress bar line. var completedLayers []string for i, layer := range layers { isLastLayer := i == len(layers)-1 @@ -220,8 +300,6 @@ func stageModelDistribution(log *velocity.Logger, p *pretty.Pretty) { cb.Increment(1) isLastStep := isLastLayer && step == 2 if isLastStep { - // Complete immediately after the final increment so the render - // goroutine sees the done signal before the ticker fires again. cb.Complete() } else { time.Sleep(120 * time.Millisecond) @@ -232,7 +310,6 @@ func stageModelDistribution(log *velocity.Logger, p *pretty.Pretty) { } } - // Log the per-layer completions now that the bar has finished its line. for i, layer := range completedLayers { distLog.Debug("container layer complete", velocity.String("layer", layer), @@ -240,13 +317,15 @@ func stageModelDistribution(log *velocity.Logger, p *pretty.Pretty) { ) } - distLog.Info("inference containers ready", velocity.String("image", "velocity/llama3-70b-awq:0.1.0")) + distLog.Info("inference containers ready", velocity.String("image", "velocity/llama3-70b-awq:2.0.0")) log.Newline() } -// stageNodeDeployment pushes the model to each node in turn. -// Returns the name of any node that failed, or an empty string for full success. -func stageNodeDeployment(log *velocity.Logger, p *pretty.Pretty) string { +// --- Node deployment ------------------------------------------------------ + +// stageNodeDeployment pushes the model to each node. Returns the failed node +// name, or an empty string if all nodes succeeded. +func stageNodeDeployment(log *velocity.Logger, p *velocity.Pretty) string { p.Section("Deploying to Nodes") nodes := []struct { @@ -257,35 +336,34 @@ func stageNodeDeployment(log *velocity.Logger, p *pretty.Pretty) string { {name: "node-0", ip: "10.0.1.10", willFail: false}, {name: "node-1", ip: "10.0.1.11", willFail: false}, {name: "node-2", ip: "10.0.1.12", willFail: false}, - {name: "node-3", ip: "10.0.1.13", willFail: true}, // pre-flight warned us + {name: "node-3", ip: "10.0.1.13", willFail: true}, } var failed string for _, node := range nodes { - nodeLog := log.With( - velocity.String("node", node.name), - velocity.String("ip", node.ip), - ) - - spinner := pretty.NewSpinner(os.Stdout, fmt.Sprintf("Deploying to %s (%s)...", node.name, node.ip)) + spinner := live.NewSpinner(os.Stdout, fmt.Sprintf("Deploying to %s (%s)...", node.name, node.ip)) time.Sleep(900 * time.Millisecond) if node.willFail { spinner.StopWithError(fmt.Sprintf("Deployment to %s failed", node.name)) - nodeLog.ErrorDetailed("container failed to start: insufficient disk space", + // StatusFail gives the operator an immediate visual signal without + // the full tree layout of Detailed(). The fields still land in JSON. + log.Status(velocity.LevelError, velocity.StatusFail, "container failed to start: insufficient disk space", + velocity.String("node", node.name), velocity.String("error", "no space left on device"), velocity.String("disk_used", "93%"), velocity.String("disk_free", "18 GB"), velocity.String("required", "35 GB"), - velocity.String("suggestion", "free space or add a volume"), ) failed = node.name } else { spinner.StopWithSuccess(node.name + " ready, inference endpoint active") - nodeLog.Info("node deployment successful", + + // StatusOK produces a green badge on TTY; JSON gets status:"ok". + log.Status(velocity.LevelInfo, velocity.StatusOK, node.name+" deployment successful", velocity.String("endpoint", "http://"+net.JoinHostPort(node.ip, "8080")+"/v1"), velocity.String("model", "llama-3.1-70b-awq"), ) @@ -296,10 +374,9 @@ func stageNodeDeployment(log *velocity.Logger, p *pretty.Pretty) string { return failed } -// stageRecovery handles the node-3 failure by redistributing its load to node-0. -// In a real system you would update the load balancer config; here we just -// log what would happen. -func stageRecovery(log *velocity.Logger, p *pretty.Pretty, failedNode string) { +// --- Recovery ------------------------------------------------------------- + +func stageRecovery(log *velocity.Logger, p *velocity.Pretty, failedNode string) { if failedNode == "" { return } @@ -311,13 +388,13 @@ func stageRecovery(log *velocity.Logger, p *pretty.Pretty, failedNode string) { velocity.String("stage", "recovery"), ) - recoveryLog.Warn("initiating workload reallocation", + log.Status(velocity.LevelWarn, velocity.StatusWarn, "initiating workload reallocation", velocity.String("from", failedNode), velocity.String("to", "node-0"), velocity.String("strategy", "single-node-overflow"), ) - spinner := pretty.NewSpinner(os.Stdout, fmt.Sprintf("Reallocating %s workload to node-0...", failedNode)) + spinner := live.NewSpinner(os.Stdout, fmt.Sprintf("Reallocating %s workload to node-0...", failedNode)) time.Sleep(1400 * time.Millisecond) spinner.StopWithSuccess("Workload reallocated, node-0 running at 2x replicas") @@ -329,30 +406,39 @@ func stageRecovery(log *velocity.Logger, p *pretty.Pretty, failedNode string) { log.Newline() } -// stageHealthVerification pings every endpoint and shows a summary table. -func stageHealthVerification(log *velocity.Logger, p *pretty.Pretty) { +// --- Health verification -------------------------------------------------- + +func stageHealthVerification(log *velocity.Logger, p *velocity.Pretty) { p.Section("Health Verification") - sf := log.Status() + style := log.Style() + healthyCell := style.Format(velocity.SlotStatusOK, "HEALTHY") + relocatedCell := style.Format(velocity.SlotStatusInfo, "RELOCATED") + failedCell := style.Format(velocity.SlotStatusFail, "FAILED") rows := [][]string{ - {"node-0", "llama-3.1-70b-awq", sf.Okay("HEALTHY"), "38 ms", "http://10.0.1.10:8080/v1"}, - {"node-0*", "llama-3.1-70b-awq", sf.Info("RELOCATED"), "41 ms", "http://10.0.1.10:8080/v1 (replica 2)"}, - {"node-1", "llama-3.1-70b-awq", sf.Okay("HEALTHY"), "35 ms", "http://10.0.1.11:8080/v1"}, - {"node-2", "llama-3.1-70b-awq", sf.Okay("HEALTHY"), "37 ms", "http://10.0.1.12:8080/v1"}, - {"node-3", "-", sf.Fail("FAILED"), "-", "disk full, out of service"}, + {"node-0", "llama-3.1-70b-awq", healthyCell, "38 ms", "http://10.0.1.10:8080/v1"}, + {"node-0*", "llama-3.1-70b-awq", relocatedCell, "41 ms", "http://10.0.1.10:8080/v1 (replica 2)"}, + {"node-1", "llama-3.1-70b-awq", healthyCell, "35 ms", "http://10.0.1.11:8080/v1"}, + {"node-2", "llama-3.1-70b-awq", healthyCell, "37 ms", "http://10.0.1.12:8080/v1"}, + {"node-3", "-", failedCell, "-", "disk full, out of service"}, } - p.Table( - []string{"Node", "Model", "Status", "P50 Latency", "Endpoint"}, - rows, - ) + p.Table([]string{"Node", "Model", "Status", "P50 Latency", "Endpoint"}, rows) + + // Status checklist gives the operator a quick scan-able summary of outcomes. + log.Status(velocity.LevelInfo, velocity.StatusOK, "node-0 healthy", velocity.String("replicas", "2")) + log.Status(velocity.LevelInfo, velocity.StatusOK, "node-1 healthy", velocity.String("replicas", "1")) + log.Status(velocity.LevelInfo, velocity.StatusOK, "node-2 healthy", velocity.String("replicas", "1")) + log.Status(velocity.LevelError, velocity.StatusFail, "node-3 out of service", velocity.String("reason", "disk full")) log.Newline() } -// stageSummary prints the final deployment summary box and the completion log line. -func stageSummary(log *velocity.Logger, p *pretty.Pretty, started time.Time) { +// --- Summary -------------------------------------------------------------- + +// stageSummary prints the final deployment summary and ring buffer snapshot. +func stageSummary(log *velocity.Logger, p *velocity.Pretty, started time.Time, ring *velocity.RingBufferWriter) { elapsed := time.Since(started).Round(time.Second) content := fmt.Sprintf( @@ -381,5 +467,36 @@ func stageSummary(log *velocity.Logger, p *pretty.Pretty, started time.Time) { velocity.Duration("elapsed", elapsed), ) - p.Success("3/4 nodes healthy, inference stack operational. Address node-3 disk space to restore full capacity.") + // NotifyBox goes to stderr (bypassing the structured pipeline) so the + // operator sees it even when stdout is redirected to a log aggregator. + // This is the alloy pattern: ephemeral operator messages that must not + // get buried in log volume. + dashURL := link("http://10.0.1.10:8080/v1/models", "http://10.0.1.10:8080/v1/models") + log.NotifyBox(velocity.NewBox( + "Deployment complete", + fmt.Sprintf( + "3/4 nodes operational. Inference stack is live.\n\n"+ + " API: %s\n\n"+ + "Address node-3 disk space to restore full capacity.", + dashURL, + ), + log.Style(), + )) + + // Ring buffer snapshot — the last N entries the logger wrote. In a real + // service this is served from an HTTP debug endpoint; here we print it so + // the operator can see what was captured without re-reading stdout. + snaps := ring.Snapshot(5) + fmt.Printf("\n=== Ring buffer: last %d entries ===\n", len(snaps)) + for _, s := range snaps { + fmt.Printf(" [%-5s] %s", s.Level, s.Message) + for _, f := range s.Fields { + fmt.Printf(" %s=%s", f.Key, f.Value) + } + fmt.Println() + } + + stats := ring.Stats() + fmt.Printf("Ring stats: capacity=%d fill=%d total=%d drops=%d\n", + stats.Capacity, stats.Fill, stats.Total, stats.Drops) } diff --git a/examples/themes/main.go b/examples/themes/main.go index ee31a79..ce86355 100644 --- a/examples/themes/main.go +++ b/examples/themes/main.go @@ -1,6 +1,6 @@ -// Package main cycles through velocity's four built-in themes so you can see -// how each one styles the different log levels. Run this in a terminal that -// supports 256-colour or true-colour output for the full effect. +// Package main cycles through velocity's built-in themes and demonstrates +// Theme.Format(slot, s) for each semantic style slot. Run in a terminal +// with 256-colour or true-colour support for the full effect. package main import ( @@ -8,7 +8,7 @@ import ( "os" "time" - "github.com/tensorfoundrylabs/velocity" + "github.com/tensorfoundrylabs/velocity/v2" ) func main() { @@ -17,6 +17,7 @@ func main() { velocity.ThemeSolarized, velocity.ThemeDracula, velocity.ThemeNord, + velocity.ThemeMono, } for _, theme := range themes { @@ -25,9 +26,9 @@ func main() { } func showTheme(theme *velocity.Theme) { - fmt.Printf("\n--- Theme: %s ---\n\n", theme.Name) + fmt.Printf("\n--- Theme: %s ---\n\n", theme.Name()) - log := velocity.NewWithOptions( + log := velocity.New( velocity.WithConsoleOutput(os.Stdout), velocity.WithLevel(velocity.LevelDebug), velocity.WithTheme(theme), @@ -39,13 +40,40 @@ func showTheme(theme *velocity.Theme) { log.Warn("memory pressure detected", velocity.Int("used_mb", 780)) log.Error("health check failed", velocity.String("target", "db.internal")) - // InfoDetailed forces tree-format for the fields, which shows how each + // Detailed() returns a child that forces tree-format, showing how each // theme colours key names and values separately. - log.InfoDetailed("deployment complete", + log.Detailed().Info("deployment complete", velocity.String("environment", "staging"), velocity.String("version", "3.1.0"), velocity.Int("instances", 5), velocity.Duration("rollout", 32*time.Second), velocity.Bool("canary", false), ) + + // log.Style() returns the active palette when colour is enabled (TTY or + // FORCE_COLOR), or ThemeMono when NO_COLOR / piped — so Format and Wrap + // calls are always colour-aware without manual env-var checks. + style := log.Style() + + // Theme.Format(slot, s) — semantic colouring without raw ANSI. + // Each slot has a well-defined role across all built-in themes. + fmt.Printf("\n Style slots:\n") + fmt.Printf(" %s\n", style.Format(velocity.SlotGood, "SlotGood — success / positive outcome")) + fmt.Printf(" %s\n", style.Format(velocity.SlotBad, "SlotBad — error / failure")) + fmt.Printf(" %s\n", style.Format(velocity.SlotWarn, "SlotWarn — warning / degraded")) + fmt.Printf(" %s\n", style.Format(velocity.SlotInfo, "SlotInfo — informational")) + fmt.Printf(" %s\n", style.Format(velocity.SlotMuted, "SlotMuted — secondary / de-emphasised")) + fmt.Printf(" %s\n", style.Format(velocity.SlotStrong, "SlotStrong — emphasis")) + fmt.Printf(" %s\n", style.Format(velocity.SlotHeading, "SlotHeading — section headings")) + fmt.Printf(" %s\n", style.Format(velocity.SlotEndpoint, "SlotEndpoint — service/URL labels")) + fmt.Printf(" %s\n", style.Format(velocity.SlotTableHeader, "SlotTableHeader — column headers")) + + // Status badge demonstration using Wrap for prefix/suffix embedding. + okPfx, okSfx := style.Wrap(velocity.SlotStatusOK) + warnPfx, warnSfx := style.Wrap(velocity.SlotStatusWarn) + failPfx, failSfx := style.Wrap(velocity.SlotStatusFail) + infoPfx, infoSfx := style.Wrap(velocity.SlotStatusInfo) + fmt.Printf("\n Status slots (via Wrap):\n") + fmt.Printf(" %s[OKAY]%s %s[WARN]%s %s[FAIL]%s %s[INFO]%s\n", + okPfx, okSfx, warnPfx, warnSfx, failPfx, failSfx, infoPfx, infoSfx) } diff --git a/fatal_test.go b/fatal_test.go index d666c30..bb054bb 100644 --- a/fatal_test.go +++ b/fatal_test.go @@ -14,9 +14,13 @@ func TestFatal_CustomHandler_CalledNotOsExit(t *testing.T) { var buf bytes.Buffer called := false - cfg := DefaultTestingConfig(&buf) - cfg.FatalHandler = func() { called = true } - l := NewWithConfig(cfg) + l := New( + WithConsoleOutput(&buf), + WithColour(false), + WithLevel(LevelDebug), + WithTimeFormat("15:04:05.000"), + WithFatalHandler(func() { called = true }), + ) l.Fatal("something went wrong", String("code", "E001")) @@ -29,20 +33,23 @@ func TestFatal_CustomHandler_CalledNotOsExit(t *testing.T) { } } -func TestFatal_WithFatalHandler_Builder(t *testing.T) { +func TestFatal_WithFatalHandler_Options(t *testing.T) { t.Parallel() var buf bytes.Buffer called := false - l := NewWithBuilder( - PresetTesting(&buf).WithFatalHandler(func() { called = true }), + l := New( + WithConsoleOutput(&buf), + WithColour(false), + WithLevel(LevelDebug), + WithFatalHandler(func() { called = true }), ) - l.Fatal("fatal via builder") + l.Fatal("fatal via options") if !called { - t.Error("expected FatalHandler to be called via builder") + t.Error("expected FatalHandler to be called via options") } } diff --git a/field.go b/field.go index b22cc9e..87c8118 100644 --- a/field.go +++ b/field.go @@ -3,6 +3,7 @@ package velocity import ( "fmt" "math" + "net/url" "reflect" "strconv" "time" @@ -25,6 +26,24 @@ const ( FieldTypeStringer FieldTypeBytes FieldTypeAny // Avoid in hot paths + + // Secure field types — hold a *secureValue in the value slot. + // One small heap alloc at constructor call; zero extra cost on the hot path + // for entries that contain no secure fields. + FieldTypeSecure // plaintext + redacted pair, writer decides which to emit + FieldTypeSecureURL // URL with userinfo password redacted + FieldTypeRedacted // permanently redacted; no plaintext stored anywhere + FieldTypeTruncated // value clipped to maxLen; may still be sensitive + + // FieldTypeGroupItems carries a []GroupItem for Logger.Group calls. + // Stored as a typed Field so Entry layout stays unchanged — entries that never + // call Logger.Group pay zero cost. + FieldTypeGroupItems + + // FieldTypeContinuationLines carries a []string for Logger.Continue calls. + // Stored as a typed Field so Entry layout stays unchanged — entries that never + // call Logger.Continue pay zero cost. + FieldTypeContinuationLines ) // Field represents a structured log field optimised for minimal allocations. @@ -44,37 +63,6 @@ type Field struct { Type FieldType } -// F creates a field with automatic type detection. -// For performance-critical code, use typed constructors instead. -func F(key string, value any) Field { - switch v := value.(type) { - case string: - return String(key, v) - case int: - return Int(key, v) - case int64: - return Int64(key, v) - case float64: - return Float64(key, v) - case bool: - return Bool(key, v) - case time.Time: - return Time(key, v) - case time.Duration: - return Duration(key, v) - case error: - // Typed nils satisfy the error interface but panic on .Error(); delegate to Error which handles them. - return Error(key, v) - case fmt.Stringer: - // Typed nils satisfy fmt.Stringer but panic on .String(); delegate to Stringer which handles them. - return Stringer(key, v) - case []byte: - return Bytes(key, v) - default: - return Any(key, v) - } -} - func String(key, val string) Field { return Field{ Key: key, @@ -137,13 +125,6 @@ func Duration(key string, val time.Duration) Field { } } -// Milliseconds creates a float field with duration in milliseconds. -// Useful for showing precise timing for fast operations (e.g., "0.5ms" instead of "0s"). -func Milliseconds(key string, val time.Duration) Field { - ms := float64(val) / float64(time.Millisecond) - return Float64(key, ms) -} - func Error(key string, val error) Field { if val == nil { return String(key, "") @@ -192,6 +173,123 @@ func Any(key string, val any) Field { } } +// parseURL wraps url.Parse so it can be swapped in tests. +// Kept unexported — callers outside this file should not need it. +var parseURL = url.Parse + +// userInfoRedacted builds a url.Userinfo with username preserved and the +// password sentinel "REDACTED". Brackets are intentionally omitted because +// url.String() URL-encodes '[' and ']', producing ugly %5BREDACTED%5D output. +func userInfoRedacted(username string) *url.Userinfo { + return url.UserPassword(username, "REDACTED") +} + +// secureValue pairs a redacted display string with the original plaintext. +// Stored behind an unsafe.Pointer so Field width stays unchanged. +// One alloc per Secure/SecureURL constructor call; zero per log call. +type secureValue struct { + plain string + redacted string +} + +// Secure creates a field that renders as redacted on untrusted writers. +// On TTY console writers (trusted by context) the plaintext is shown. +// One heap alloc at the call site; zero per log call thereafter. +func Secure(key, val string) Field { + sv := &secureValue{plain: val, redacted: redactedMark} + return Field{ + Key: key, + Type: FieldTypeSecure, + value: unsafe.Pointer(sv), + } +} + +// SecureURL creates a field from a URL string, redacting any userinfo password. +// The plaintext form retains the full URL; the redacted form replaces the +// password with "[REDACTED]". Falls back to treating the raw string as plaintext +// when parsing fails. One heap alloc at the call site. +func SecureURL(key, rawURL string) Field { + plain := rawURL + redacted := rawURL + + if u, err := parseURL(rawURL); err == nil && u.User != nil { + if _, hasPass := u.User.Password(); hasPass { + safe := *u + safe.User = userInfoRedacted(u.User.Username()) + redacted = safe.String() + } + } + + sv := &secureValue{plain: plain, redacted: redacted} + return Field{ + Key: key, + Type: FieldTypeSecureURL, + value: unsafe.Pointer(sv), + } +} + +// Redacted creates a field with no plaintext — the value is permanently hidden +// regardless of writer trust level. Use when the value must never appear in any +// log output, not even on trusted writers. +func Redacted(key string) Field { + return Field{ + Key: key, + Type: FieldTypeRedacted, + // No value stored. Writers emit the redaction mark unconditionally. + } +} + +// Truncated clips val to maxLen runes, appending '…' when trimmed. +// Zero-alloc when val fits within maxLen (stored as FieldTypeString). +// One alloc when trimming occurs; returns FieldTypeTruncated so writers can +// distinguish truncated fields from plain strings if needed. +func Truncated(key, val string, maxLen int) Field { + if maxLen <= 0 { + return Field{Key: key, Type: FieldTypeTruncated} + } + // Count runes to handle multi-byte sequences correctly. + n := 0 + for i := range val { + if n == maxLen { + // val exceeds maxLen — clip and append ellipsis. + clipped := val[:i] + "…" + return Field{ + Key: key, + Type: FieldTypeTruncated, + value: unsafe.Pointer(&clipped), + } + } + _ = i + n++ + } + // val fits; equivalent alloc profile to String() since &val forces escape. + return Field{ + Key: key, + Type: FieldTypeTruncated, + value: unsafe.Pointer(&val), + } +} + +// redactedMark is the default redaction sentinel used across the package. +const redactedMark = "[REDACTED]" + +// SecurePlain returns the plaintext value of a Secure or SecureURL field. +// Returns empty string for all other types or when no plaintext is stored. +func SecurePlain(f Field) string { + if (f.Type == FieldTypeSecure || f.Type == FieldTypeSecureURL) && f.value != nil { + return (*secureValue)(f.value).plain + } + return "" +} + +// SecureRedacted returns the redacted form of a Secure or SecureURL field. +func SecureRedacted(f Field) string { + if (f.Type == FieldTypeSecure || f.Type == FieldTypeSecureURL) && f.value != nil { + return (*secureValue)(f.value).redacted + } + return redactedMark +} + // Value returns the field's value based on its type. // This method allocates and should be avoided in hot paths. func (f Field) Value() any { @@ -219,40 +317,31 @@ func (f Field) Value() any { return *(*[]byte)(f.value) case FieldTypeAny: return *(*any)(f.value) + case FieldTypeSecure, FieldTypeSecureURL: + if f.value != nil { + return (*secureValue)(f.value).plain + } + return "" + case FieldTypeRedacted: + return redactedMark + case FieldTypeTruncated: + if f.value != nil { + return *(*string)(f.value) + } + return "" + case FieldTypeGroupItems: + // Items are handled directly by group-aware writers. + // Returning nil here prevents fmt fallback from trying to dereference the slice pointer. + return nil + case FieldTypeContinuationLines: + // Lines are handled directly by continuation-aware writers. + return nil case FieldTypeUnknown: return nil } return nil } -type Fields struct { - fields []Field -} - -func NewFields(capacity int) *Fields { - return &Fields{ - fields: make([]Field, 0, capacity), - } -} - -func (fs *Fields) Add(key string, value any) *Fields { - fs.fields = append(fs.fields, F(key, value)) - return fs -} - -func (fs *Fields) AddField(f Field) *Fields { - fs.fields = append(fs.fields, f) - return fs -} - -func (fs *Fields) Reset() { - fs.fields = fs.fields[:0] -} - -func (fs *Fields) Slice() []Field { - return fs.fields -} - func (f Field) writeFormatted(buf interface { WriteString(string) (int, error) WriteRune(rune) (int, error) @@ -310,11 +399,92 @@ func (f Field) writeFormatted(buf interface { case FieldTypeAny: val := *(*any)(f.value) _, _ = fmt.Fprintf(buf, "%v", val) + case FieldTypeSecure, FieldTypeSecureURL: + // Default: emit redacted form. Trusted writers call writeFormattedTrusted instead. + if f.value != nil { + _, _ = buf.WriteString((*secureValue)(f.value).redacted) + } else { + _, _ = buf.WriteString(redactedMark) + } + case FieldTypeRedacted: + _, _ = buf.WriteString(redactedMark) + case FieldTypeTruncated: + // Truncated values are not sensitive by definition; always emit as-is. + if f.value != nil { + _, _ = buf.WriteString(*(*string)(f.value)) + } + case FieldTypeGroupItems: + // Group items are rendered by the console/JSON writers directly. + // In generic contexts (e.g. additional writers) write the item count as a hint. + if f.value != nil { + items := *(*[]GroupItem)(f.value) + var tmp [20]byte + n := formatInt(tmp[:], int64(len(items))) + _, _ = buf.WriteString("[") + _, _ = buf.Write(tmp[:n]) + _, _ = buf.WriteString(" items]") + } + case FieldTypeContinuationLines: + // Continuation lines are rendered by WriteContinue directly. + // In generic contexts emit the line count as a hint. + if f.value != nil { + lines := *(*[]string)(f.value) + var tmp [20]byte + n := formatInt(tmp[:], int64(len(lines))) + _, _ = buf.WriteString("[") + _, _ = buf.Write(tmp[:n]) + _, _ = buf.WriteString(" lines]") + } case FieldTypeUnknown: // Unknown field type - write nothing } } +// writeFormattedTrusted writes the field to buf, using plaintext for Secure/SecureURL. +// Call this only from writers that have been explicitly opted into trust. +func (f Field) writeFormattedTrusted(buf interface { + WriteString(string) (int, error) + WriteRune(rune) (int, error) + Write([]byte) (int, error) +}, +) { + switch f.Type { + case FieldTypeSecure, FieldTypeSecureURL: + if f.value != nil { + _, _ = buf.WriteString((*secureValue)(f.value).plain) + } + case FieldTypeRedacted: + // Redacted is unconditional — trust has no effect. + _, _ = buf.WriteString(redactedMark) + default: + f.writeFormatted(buf) + } +} + +// writeFormattedWithMark writes the field to buf, replacing Secure/SecureURL values +// with the given redactionMark. Use on untrusted writer paths. +func (f Field) writeFormattedWithMark(buf interface { + WriteString(string) (int, error) + WriteRune(rune) (int, error) + Write([]byte) (int, error) +}, redactionMark string, +) { + switch f.Type { + case FieldTypeSecure, FieldTypeSecureURL: + if f.value != nil { + // Emit the field-level redacted form rather than the writer-level mark + // so URL fields still show the host/path portion. + _, _ = buf.WriteString((*secureValue)(f.value).redacted) + } else { + _, _ = buf.WriteString(redactionMark) + } + case FieldTypeRedacted: + _, _ = buf.WriteString(redactionMark) + default: + f.writeFormatted(buf) + } +} + func itoa(i int) string { if i == 0 { return "0" diff --git a/field_convert.go b/field_convert.go index baef042..c213974 100644 --- a/field_convert.go +++ b/field_convert.go @@ -76,6 +76,37 @@ func FieldValueToString(f Field) string { return nilValueString } return fmt.Sprintf("%v", val) + case FieldTypeSecure, FieldTypeSecureURL: + // Return redacted form; callers that need plaintext use SecurePlain(). + if f.value == nil { + return redactedMark + } + return (*secureValue)(f.value).redacted + case FieldTypeRedacted: + return redactedMark + case FieldTypeTruncated: + if f.value == nil { + return "" + } + return *(*string)(f.value) + case FieldTypeGroupItems: + // Return a human-readable hint; group-aware writers handle items directly. + if f.value == nil { + return "[0 items]" + } + items := *(*[]GroupItem)(f.value) + var tmp [20]byte + n := formatInt(tmp[:], int64(len(items))) + return "[" + UnsafeString(tmp[:n]) + " items]" + case FieldTypeContinuationLines: + // Return a human-readable hint; continuation-aware writers handle lines directly. + if f.value == nil { + return "[0 lines]" + } + lines := *(*[]string)(f.value) + var tmp [20]byte + n := formatInt(tmp[:], int64(len(lines))) + return "[" + UnsafeString(tmp[:n]) + " lines]" case FieldTypeUnknown: return "" } diff --git a/field_test.go b/field_test.go index 25be25d..b0c8245 100644 --- a/field_test.go +++ b/field_test.go @@ -59,22 +59,24 @@ func TestStringer_TypedNil(t *testing.T) { } } -func TestF_TypedNilError(t *testing.T) { +func TestError_TypedNilError(t *testing.T) { var err *concreteError - f := F("err", err) + // Error() must detect the typed nil and return a String field, not a dangling pointer. + f := Error("err", err) if f.Type != FieldTypeString { - t.Errorf("expected FieldTypeString for typed nil via F(), got %v", f.Type) + t.Errorf("expected FieldTypeString for typed nil via Error(), got %v", f.Type) } } -func TestF_TypedNilStringer(t *testing.T) { +func TestStringer_TypedNilStringer(t *testing.T) { var s *concreteStringer - f := F("s", fmt.Stringer(s)) + // Stringer() must detect the typed nil and return a String field. + f := Stringer("s", fmt.Stringer(s)) if f.Type != FieldTypeString { - t.Errorf("expected FieldTypeString for typed nil stringer via F(), got %v", f.Type) + t.Errorf("expected FieldTypeString for typed nil stringer via Stringer(), got %v", f.Type) } } diff --git a/go.mod b/go.mod index 0222a53..ad59344 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/tensorfoundrylabs/velocity +module github.com/tensorfoundrylabs/velocity/v2 go 1.24.0 diff --git a/group.go b/group.go new file mode 100644 index 0000000..6089d72 --- /dev/null +++ b/group.go @@ -0,0 +1,229 @@ +package velocity + +import ( + "bytes" + "io" + "strings" + "unsafe" +) + +// GroupItem is one entry in a Group block. Marker is an optional prefix glyph +// (e.g. "├─", "✓", "•"); if empty, the renderer supplies a default tree glyph +// and promotes the last item to "└─" automatically. +type GroupItem struct { + Marker string + Text string +} + +// Group is a Renderable that emits a count-headed block: +// +// INFO Registering routes (3) +// ├─ GET /api/v1/users +// ├─ POST /api/v1/users +// └─ GET /api/v1/users/:id +// +// On TTY the count token is coloured with SlotCount; items are indented past the +// message column. On non-TTY the count token is plain text. JSON output emits a +// single entry with "count" and "items" fields — markers are visual-only and +// are stripped from JSON. TTY detection happens at Render time via IsTerminalWriter. +type Group struct { + theme *Theme + msg string + items []GroupItem +} + +// NewGroup constructs a Group. theme may be nil (falls back to ThemeNightOwl). +// TTY detection is deferred to Render time — callers do not need to pass isTTY. +func NewGroup(msg string, items []GroupItem, theme *Theme) *Group { + if theme == nil { + theme = ThemeNightOwl + } + // Copy so the caller's slice is safe to mutate after the call. + its := make([]GroupItem, len(items)) + copy(its, items) + return &Group{ + msg: msg, + items: its, + theme: theme, + } +} + +// Render writes the group block to w. TTY is detected from w at call time. +// The header line carries the message and count; each item follows on its own indented line. +// +// Note: when called via Logger.Render the writer is an intermediate buffer. +// Logger.Render detects TTYRenderable and calls RenderTTY with the correct TTY state. +func (g *Group) Render(w io.Writer) error { + if g == nil { + return nil + } + return g.RenderTTY(w, IsTerminalWriter(w)) +} + +// RenderTTY writes the group block to w with explicit TTY state. Callers that +// already know the terminal state (e.g. Logger.Render) should use this to avoid +// false-negative TTY detection on intermediate buffers. +func (g *Group) RenderTTY(w io.Writer, isTTY bool) error { + if g == nil { + return nil + } + var buf bytes.Buffer + if isTTY { + renderGroupTTY(&buf, g.msg, g.items, g.theme) + } else { + renderGroupPlain(&buf, g.msg, g.items) + } + _, err := w.Write(buf.Bytes()) + return err +} + +// String renders the group to a string. Useful in tests and for capture. +func (g *Group) String() string { + if g == nil { + return "" + } + var buf bytes.Buffer + _ = g.Render(&buf) + return buf.String() +} + +// renderGroupTTY writes the ANSI form: coloured count token, indented item lines. +func renderGroupTTY(buf *bytes.Buffer, msg string, items []GroupItem, theme *Theme) { + // Header: message + " (" + coloured count + ")" + msgCode := theme.CachedMessageFg() + if msgCode != "" { + buf.WriteString(msgCode) + } + buf.WriteString(msg) + if msgCode != "" { + buf.WriteString(theme.ResetStr()) + } + buf.WriteString(" (") + countPrefix, countSuffix := theme.Wrap(SlotCount) + buf.WriteString(countPrefix) + var tmp [20]byte + n := formatInt(tmp[:], int64(len(items))) + buf.Write(tmp[:n]) + buf.WriteString(countSuffix) + buf.WriteByte(')') + buf.WriteByte('\n') + + // Item lines. + writeGroupConsoleTTYItems(buf, items, theme) +} + +// renderGroupPlain writes a non-ANSI form suitable for pipes and log files. +func renderGroupPlain(buf *bytes.Buffer, msg string, items []GroupItem) { + buf.WriteString(msg) + buf.WriteString(" (") + var tmp [20]byte + n := formatInt(tmp[:], int64(len(items))) + buf.Write(tmp[:n]) + buf.WriteByte(')') + buf.WriteByte('\n') + + writeGroupConsoleItems(buf, items) +} + +// resolvedMarker returns the item's explicit marker, or a default tree glyph. +// When the marker is empty and there are multiple items, the last gets "└─". +func resolvedMarker(marker string, idx, total int) string { + if marker != "" { + return marker + } + if total > 1 && idx == total-1 { + return groupLastGlyph + } + return groupDefaultGlyph +} + +// groupItemIndent is the per-item prefix before the marker. When rendered via +// Logger.Group the full message-column indent is prepended separately via +// indentLines, so item content lands flush under the log message text. +const groupItemIndent = " " + +const ( + groupDefaultGlyph = "├─" + groupLastGlyph = "└─" +) + +// groupCountKey and groupItemsKey are the JSON field names emitted by Logger.Group. +const ( + groupCountKey = "count" + groupItemsKey = "items" +) + +// writeGroupConsoleTTYItems writes ANSI-coloured item lines into buf. +func writeGroupConsoleTTYItems(buf *bytes.Buffer, items []GroupItem, theme *Theme) { + keyCode := theme.CachedFieldKeyFg() + msgCode := theme.CachedMessageFg() + for i, item := range items { + marker := resolvedMarker(item.Marker, i, len(items)) + buf.WriteString(groupItemIndent) + if keyCode != "" { + buf.WriteString(keyCode) + } + buf.WriteString(marker) + buf.WriteByte(' ') + if keyCode != "" { + buf.WriteString(theme.ResetStr()) + } + if msgCode != "" { + buf.WriteString(msgCode) + } + buf.WriteString(item.Text) + if msgCode != "" { + buf.WriteString(theme.ResetStr()) + } + buf.WriteByte('\n') + } +} + +// writeGroupConsoleItems writes plain (non-ANSI) item lines into buf. +func writeGroupConsoleItems(buf *bytes.Buffer, items []GroupItem) { + for i, item := range items { + marker := resolvedMarker(item.Marker, i, len(items)) + buf.WriteString(groupItemIndent) + buf.WriteString(marker) + buf.WriteByte(' ') + buf.WriteString(item.Text) + buf.WriteByte('\n') + } +} + +// groupMsgWithCount builds the composite header string "msg (N)" used as the +// Entry.Message when routing through the standard log pipeline. The count is +// styled at render time; this is the plain-text form for the structured channel. +func groupMsgWithCount(msg string, count int) string { + var sb strings.Builder + sb.WriteString(msg) + sb.WriteString(" (") + var tmp [20]byte + n := formatInt(tmp[:], int64(count)) + sb.Write(tmp[:n]) + sb.WriteByte(')') + return sb.String() +} + +// groupItemsField constructs a Field that carries a []GroupItem slice. +// One heap alloc per Logger.Group call; entries that never call Group pay nothing. +// The key is set to groupItemsKey so JSON writers can use it directly. +func groupItemsField(items []GroupItem) Field { + // Copy so the caller's variadic slice is safe to mutate after return. + cp := make([]GroupItem, len(items)) + copy(cp, items) + return Field{ + Key: groupItemsKey, + Type: FieldTypeGroupItems, + value: unsafe.Pointer(&cp), //nolint:gosec // G103: same unsafe.Pointer pattern used throughout field.go + } +} + +// groupItemsFromField recovers the []GroupItem stored in a FieldTypeGroupItems field. +// Returns nil if f is not of that type. +func groupItemsFromField(f Field) []GroupItem { + if f.Type != FieldTypeGroupItems || f.value == nil { + return nil + } + return *(*[]GroupItem)(f.value) +} diff --git a/group_test.go b/group_test.go new file mode 100644 index 0000000..3b16f6b --- /dev/null +++ b/group_test.go @@ -0,0 +1,456 @@ +package velocity + +import ( + "bytes" + "io" + "strings" + "testing" +) + +// --- resolvedMarker --- + +func TestResolvedMarker_ExplicitMarker(t *testing.T) { + t.Parallel() + + got := resolvedMarker("✓", 0, 3) + if got != "✓" { + t.Errorf("expected explicit marker, got %q", got) + } +} + +func TestResolvedMarker_DefaultGlyph(t *testing.T) { + t.Parallel() + + // Non-last item with no marker gets the default branch glyph. + got := resolvedMarker("", 0, 3) + if got != groupDefaultGlyph { + t.Errorf("expected %q, got %q", groupDefaultGlyph, got) + } +} + +func TestResolvedMarker_LastGlyph(t *testing.T) { + t.Parallel() + + // Last item with no marker and total > 1 gets the terminal glyph. + got := resolvedMarker("", 2, 3) + if got != groupLastGlyph { + t.Errorf("expected %q, got %q", groupLastGlyph, got) + } +} + +func TestResolvedMarker_SingleItem(t *testing.T) { + t.Parallel() + + // Single item should use the default glyph (total == 1, not > 1). + got := resolvedMarker("", 0, 1) + if got != groupDefaultGlyph { + t.Errorf("single item: expected %q, got %q", groupDefaultGlyph, got) + } +} + +// --- Group.String / Render nil receiver --- + +func TestGroupNilReceiver(t *testing.T) { + t.Parallel() + + var g *Group + if s := g.String(); s != "" { + t.Errorf("nil.String() = %q, want empty", s) + } + var buf bytes.Buffer + if err := g.Render(&buf); err != nil { + t.Errorf("nil.Render() error: %v", err) + } + if buf.Len() != 0 { + t.Errorf("nil.Render() wrote bytes: %q", buf.String()) + } +} + +// --- Empty items --- + +func TestGroupRenderEmpty(t *testing.T) { + t.Parallel() + + g := NewGroup("Loaded plugins", nil, ThemeMono) + out := g.String() + + if !strings.Contains(out, "Loaded plugins (0)") { + t.Errorf("expected header with (0) count, got: %q", out) + } + // No item lines beyond the header. + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(lines) != 1 { + t.Errorf("expected 1 line for empty group, got %d: %q", len(lines), out) + } +} + +// --- One item, default marker --- + +func TestGroupRenderSingleItem(t *testing.T) { + t.Parallel() + + g := NewGroup("Loaded plugins", []GroupItem{ + {Text: "auth"}, + }, ThemeMono) + out := g.String() + + if !strings.Contains(out, "(1)") { + t.Errorf("expected (1) count, got: %q", out) + } + // Single item uses the default glyph (not the last-item glyph, because total==1). + if !strings.Contains(out, groupDefaultGlyph+" auth") { + t.Errorf("expected default glyph + text, got: %q", out) + } +} + +// --- Multiple items: auto last glyph --- + +func TestGroupRenderMultipleItemsAutoLast(t *testing.T) { + t.Parallel() + + items := []GroupItem{ + {Text: "GET /api/v1/users"}, + {Text: "POST /api/v1/users"}, + {Text: "GET /api/v1/users/:id"}, + } + g := NewGroup("Registering routes", items, ThemeMono) + out := g.String() + + if !strings.Contains(out, "(3)") { + t.Errorf("expected (3) count, got: %q", out) + } + // First two items get the default glyph. + if !strings.Contains(out, groupDefaultGlyph+" GET /api/v1/users\n") { + t.Errorf("expected default glyph on non-last item, got: %q", out) + } + // Last item gets the terminal glyph. + if !strings.Contains(out, groupLastGlyph+" GET /api/v1/users/:id") { + t.Errorf("expected last glyph on final item, got: %q", out) + } +} + +// --- Multiple items with explicit markers --- + +func TestGroupRenderExplicitMarkers(t *testing.T) { + t.Parallel() + + items := []GroupItem{ + {Marker: "✓", Text: "passed"}, + {Marker: "✗", Text: "failed"}, + {Marker: "~", Text: "skipped"}, + } + g := NewGroup("Test results", items, ThemeMono) + out := g.String() + + for _, want := range []string{"✓ passed", "✗ failed", "~ skipped"} { + if !strings.Contains(out, want) { + t.Errorf("expected %q in output, got: %q", want, out) + } + } + // Explicit markers: auto-last should NOT override. + if strings.Contains(out, groupLastGlyph) { + t.Errorf("last glyph should not appear when explicit markers are set, got: %q", out) + } +} + +// --- TTY render: count has different colour (no ANSI in Mono, just check structure) --- +// Exercises the TTY path via renderGroupTTY directly, since bytes.Buffer is not a +// terminal and g.String() uses the plain path automatically. + +func TestGroupRenderTTY(t *testing.T) { + t.Parallel() + + items := []GroupItem{ + {Text: "item A"}, + {Text: "item B"}, + } + g := NewGroup("Processing", items, ThemeMono) + + var buf bytes.Buffer + renderGroupTTY(&buf, g.msg, g.items, g.theme) + out := buf.String() + + if !strings.Contains(out, "Processing (2)") { + t.Errorf("expected header with count, got: %q", out) + } + if !strings.Contains(out, "item A") || !strings.Contains(out, "item B") { + t.Errorf("expected item text in TTY output, got: %q", out) + } +} + +// --- Logger.Group: console routing --- + +func TestLoggerGroupConsole(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(&buf), + WithColour(false), + ) + + log.Group(LevelInfo, "Registering routes", + GroupItem{Text: "GET /api/v1/users"}, + GroupItem{Text: "POST /api/v1/users"}, + GroupItem{Text: "GET /api/v1/users/:id"}, + ) + + out := buf.String() + if !strings.Contains(out, "Registering routes") { + t.Errorf("expected message in console output: %q", out) + } + // Count must appear somewhere (either in the header or items line). + if !strings.Contains(out, "(3)") { + t.Errorf("expected count (3) in console output: %q", out) + } + // At least one item must appear. + if !strings.Contains(out, "/api/v1/users") { + t.Errorf("expected item text in console output: %q", out) + } +} + +// --- Logger.Group: JSON routing --- + +func TestLoggerGroupJSON(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + + log.Group(LevelInfo, "Registering routes", + GroupItem{Text: "GET /api/v1/users"}, + GroupItem{Text: "POST /api/v1/users"}, + GroupItem{Text: "GET /api/v1/users/:id"}, + ) + + out := buf.String() + // Single JSON line. + lines := strings.Split(strings.TrimRight(out, "\n"), "\n") + if len(lines) != 1 { + t.Errorf("expected one JSON line, got %d: %q", len(lines), out) + } + if !strings.Contains(out, `"count":3`) { + t.Errorf("expected count field in JSON: %q", out) + } + if !strings.Contains(out, `"items":[`) { + t.Errorf("expected items array in JSON: %q", out) + } + // Message in JSON should not include the " (N)" suffix. + if !strings.Contains(out, `"message":"Registering routes"`) { + t.Errorf("expected clean message in JSON (no count suffix): %q", out) + } + // Item text (markers stripped). + if !strings.Contains(out, `"GET /api/v1/users"`) { + t.Errorf("expected item text in JSON items array: %q", out) + } +} + +// --- JSON: markers stripped from items array --- + +func TestLoggerGroupJSONMarkersStripped(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + + log.Group(LevelInfo, "Results", + GroupItem{Marker: "✓", Text: "auth passed"}, + GroupItem{Marker: "✗", Text: "rate limit failed"}, + ) + + out := buf.String() + // Markers must not appear in the JSON items array. + if strings.Contains(out, "✓") || strings.Contains(out, "✗") { + t.Errorf("markers leaked into JSON items: %q", out) + } + if !strings.Contains(out, `"auth passed"`) || !strings.Contains(out, `"rate limit failed"`) { + t.Errorf("expected item text in JSON: %q", out) + } +} + +// --- JSON: empty items --- + +func TestLoggerGroupJSONEmpty(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + + log.Group(LevelInfo, "No routes") + + out := buf.String() + if !strings.Contains(out, `"count":0`) { + t.Errorf("expected count:0 in JSON: %q", out) + } + if !strings.Contains(out, `"items":[]`) { + t.Errorf("expected empty items array in JSON: %q", out) + } +} + +// --- Logger.Group: nil logger --- + +func TestLoggerGroupNil(t *testing.T) { + t.Parallel() + + // Must not panic. + var log *Logger + log.Group(LevelInfo, "should not panic", + GroupItem{Text: "item one"}, + ) +} + +// --- Logger.Group: level filtering --- + +func TestLoggerGroupLevelFilter(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithStructuredOutput(&buf), + WithLevel(LevelError), + WithStructuredLevel(LevelError), + ) + + log.Group(LevelInfo, "this should be filtered", + GroupItem{Text: "item"}, + ) + + if out := buf.String(); out != "" { + t.Errorf("expected no output when filtered, got: %q", out) + } +} + +// --- TTY indent alignment: items indented past the message column --- + +func TestLoggerGroupTTYIndent(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(&buf), + WithColour(false), + ) + + // Fake a TTY by setting up a logger with a console writer whose isTTY is true. + // We can't force this in tests, but we can verify the non-TTY item indent + // is present (groupItemIndent at a minimum). + log.Group(LevelInfo, "Routes", + GroupItem{Text: "GET /"}, + GroupItem{Text: "POST /"}, + ) + + out := buf.String() + // Items must have at least the two-space groupItemIndent prefix. + for line := range strings.SplitSeq(out, "\n") { + if strings.Contains(line, "GET /") || strings.Contains(line, "POST /") { + if !strings.HasPrefix(line, " ") { + t.Errorf("item line missing indent: %q", line) + } + } + } +} + +// --- groupMsgWithCount --- + +func TestGroupMsgWithCount(t *testing.T) { + t.Parallel() + + cases := []struct { + msg string + count int + want string + }{ + {"Registering routes", 3, "Registering routes (3)"}, + {"Loaded plugins", 0, "Loaded plugins (0)"}, + {"Items", 100, "Items (100)"}, + } + + for _, tc := range cases { + t.Run(tc.want, func(t *testing.T) { + t.Parallel() + got := groupMsgWithCount(tc.msg, tc.count) + if got != tc.want { + t.Errorf("groupMsgWithCount(%q, %d) = %q, want %q", tc.msg, tc.count, got, tc.want) + } + }) + } +} + +// --- groupItemsField / groupItemsFromField roundtrip --- + +func TestGroupItemsFieldRoundtrip(t *testing.T) { + t.Parallel() + + items := []GroupItem{ + {Marker: "•", Text: "one"}, + {Text: "two"}, + } + + f := groupItemsField(items) + if f.Type != FieldTypeGroupItems { + t.Fatalf("expected FieldTypeGroupItems, got %v", f.Type) + } + + got := groupItemsFromField(f) + if len(got) != len(items) { + t.Fatalf("roundtrip length mismatch: got %d, want %d", len(got), len(items)) + } + for i, item := range items { + if got[i] != item { + t.Errorf("item[%d] = %+v, want %+v", i, got[i], item) + } + } +} + +// --- groupItemsFromField: wrong type returns nil --- + +func TestGroupItemsFromFieldWrongType(t *testing.T) { + t.Parallel() + + f := String("key", "val") + if got := groupItemsFromField(f); got != nil { + t.Errorf("expected nil for non-GroupItems field, got %v", got) + } +} + +// --- Render parity: TTY and non-TTY both have all items --- + +func TestGroupRenderParity(t *testing.T) { + t.Parallel() + + items := []GroupItem{ + {Text: "alpha"}, + {Text: "beta"}, + {Text: "gamma"}, + } + + // Both render paths (TTY and plain) must include all item text. + g := NewGroup("Test", items, ThemeMono) + + // Plain path (bytes.Buffer is not a terminal). + plainOut := g.String() + for _, item := range items { + if !strings.Contains(plainOut, item.Text) { + t.Errorf("plain: missing item %q in output: %q", item.Text, plainOut) + } + } + + // TTY path via internal helper. + var ttyBuf bytes.Buffer + renderGroupTTY(&ttyBuf, g.msg, g.items, g.theme) + ttyOut := ttyBuf.String() + for _, item := range items { + if !strings.Contains(ttyOut, item.Text) { + t.Errorf("tty: missing item %q in output: %q", item.Text, ttyOut) + } + } +} diff --git a/hyperlink.go b/hyperlink.go new file mode 100644 index 0000000..986618b --- /dev/null +++ b/hyperlink.go @@ -0,0 +1,140 @@ +package velocity + +import ( + "os" + "sync" +) + +// HyperlinkFallback controls how Hyperlink renders when OSC 8 is not supported. +type HyperlinkFallback uint8 + +const ( + // HyperlinkFallbackParens renders as "text (uri)" — the default. + HyperlinkFallbackParens HyperlinkFallback = iota + // HyperlinkFallbackNone renders as "text" only. Zero-alloc when hyperlinks + // are unsupported; the input text string is returned directly. + HyperlinkFallbackNone + // HyperlinkFallbackBrackets renders as "text [uri]". + HyperlinkFallbackBrackets +) + +// HyperlinkOption configures Hyperlink behaviour. +type HyperlinkOption func(*hyperlinkOptions) + +type hyperlinkOptions struct { + fallback HyperlinkFallback +} + +// WithHyperlinkFallback sets the fallback rendering mode used when the +// terminal does not support OSC 8 hyperlinks. +func WithHyperlinkFallback(f HyperlinkFallback) HyperlinkOption { + return func(o *hyperlinkOptions) { + o.fallback = f + } +} + +// hyperlinkOnce guards a single env-var read; the result is stable for the +// process lifetime. VELOCITY_HYPERLINKS=1 force-enables, =0 force-disables — +// useful in tests that cannot construct a real supporting terminal. +var ( + hyperlinkOnce sync.Once + hyperlinkSupported bool +) + +// HyperlinksSupported reports whether the current terminal supports OSC 8. +// The result is cached after the first call. Useful for branching logic at call +// sites that need to choose between plain and hyperlinked output. +func HyperlinksSupported() bool { + hyperlinkOnce.Do(detectHyperlinkSupport) + return hyperlinkSupported +} + +// detectHyperlinkSupport runs once. Order of precedence: +// 1. VELOCITY_HYPERLINKS=1|0 explicit override (tests + CI scripts) +// 2. $TERM_PROGRAM: iTerm.app, WezTerm, vscode, Terminus +// 3. $WT_SESSION: Windows Terminal sets this to a non-empty GUID +// 4. $KITTY_WINDOW_ID: set by kitty terminal +func detectHyperlinkSupport() { + switch os.Getenv("VELOCITY_HYPERLINKS") { + case "1": + hyperlinkSupported = true + return + case "0": + hyperlinkSupported = false + return + } + + switch os.Getenv("TERM_PROGRAM") { + case "iTerm.app", "WezTerm", "vscode", "Terminus": + hyperlinkSupported = true + return + } + + if os.Getenv("WT_SESSION") != "" { + hyperlinkSupported = true + return + } + + if os.Getenv("KITTY_WINDOW_ID") != "" { + hyperlinkSupported = true + return + } +} + +// Hyperlink wraps text in an OSC 8 hyperlink sequence when the terminal +// supports it, otherwise returns a fallback string. +// +// OSC 8 sequence: \x1b]8;;\x07\x1b]8;;\x07 +// +// Detection is cached via sync.Once on first call. Override via: +// - VELOCITY_HYPERLINKS=1 force enable (useful in tests) +// - VELOCITY_HYPERLINKS=0 force disable +// +// Theme colouring is intentionally NOT applied here. To produce a coloured +// hyperlink, compose with Theme.Format: +// +// theme.Format(SlotHyperlink, velocity.Hyperlink(uri, text)) +// +// Empty text returns an empty string regardless of hyperlink support — there +// is nothing meaningful to wrap. Empty uri with non-empty text falls through +// to the fallback (the URI would be empty in the OSC sequence, which most +// terminals treat as clearing the link; we skip it rather than emit noise). +func Hyperlink(uri, text string, opts ...HyperlinkOption) string { + if text == "" { + return "" + } + + o := hyperlinkOptions{fallback: HyperlinkFallbackParens} + for _, opt := range opts { + opt(&o) + } + + // Skip the OSC sequence when the URI is empty — a zero-length URI in the + // sequence is valid per the spec (it closes an active link) but emitting it + // mid-string where no link was opened produces invisible garbage in most + // terminals. Fall through to fallback instead. + if uri == "" || !HyperlinksSupported() { + return fallbackHyperlink(uri, text, o.fallback) + } + + return "\x1b]8;;" + uri + "\x07" + text + "\x1b]8;;\x07" +} + +// fallbackHyperlink returns the plain-text representation for terminals that +// do not support OSC 8. HyperlinkFallbackNone is the only zero-alloc path. +func fallbackHyperlink(uri, text string, mode HyperlinkFallback) string { + switch mode { + case HyperlinkFallbackNone: + return text + case HyperlinkFallbackBrackets: + if uri == "" { + return text + } + return text + " [" + uri + "]" + default: // HyperlinkFallbackParens + if uri == "" { + return text + } + return text + " (" + uri + ")" + } +} diff --git a/hyperlink_test.go b/hyperlink_test.go new file mode 100644 index 0000000..a026008 --- /dev/null +++ b/hyperlink_test.go @@ -0,0 +1,158 @@ +package velocity + +import ( + "sync" + "testing" +) + +// resetHyperlinkDetection tears down the sync.Once so tests that mutate +// VELOCITY_HYPERLINKS can start from a clean state. Each test that uses it +// must defer a second call to restore the zero value. +func resetHyperlinkDetection() { + hyperlinkOnce = sync.Once{} + hyperlinkSupported = false +} + +// withHyperlinkEnv sets VELOCITY_HYPERLINKS to value, calls reset so detection +// re-runs on next call, and returns a cleanup func. +func withHyperlinkEnv(t *testing.T, value string) { + t.Helper() + t.Setenv("VELOCITY_HYPERLINKS", value) + resetHyperlinkDetection() + t.Cleanup(resetHyperlinkDetection) +} + +// ---- HyperlinksSupported ---------------------------------------------------- + +func TestHyperlinksSupported_ForceOn(t *testing.T) { + withHyperlinkEnv(t, "1") + + if !HyperlinksSupported() { + t.Error("expected HyperlinksSupported()=true when VELOCITY_HYPERLINKS=1") + } +} + +func TestHyperlinksSupported_ForceOff(t *testing.T) { + withHyperlinkEnv(t, "0") + + if HyperlinksSupported() { + t.Error("expected HyperlinksSupported()=false when VELOCITY_HYPERLINKS=0") + } +} + +// ---- Hyperlink — OSC 8 path ------------------------------------------------- + +func TestHyperlink_OSC8_ForceOn(t *testing.T) { + withHyperlinkEnv(t, "1") + + const uri = "https://example.com" + const text = "click here" + got := Hyperlink(uri, text) + want := "\x1b]8;;" + uri + "\x07" + text + "\x1b]8;;\x07" + if got != want { + t.Errorf("OSC 8 sequence wrong\nwant: %q\ngot: %q", want, got) + } +} + +func TestHyperlink_OSC8_ContainsText(t *testing.T) { + withHyperlinkEnv(t, "1") + + got := Hyperlink("https://example.com", "docs") + if len(got) == 0 { + t.Fatal("expected non-empty result") + } + // The visible text must appear between the two OSC sequences. + if got[len("\x1b]8;;https://example.com\x07"):len(got)-len("\x1b]8;;\x07")] != "docs" { + t.Errorf("text not in expected position: %q", got) + } +} + +// ---- Hyperlink — fallback paths --------------------------------------------- + +func TestHyperlink_Fallback_Parens(t *testing.T) { + withHyperlinkEnv(t, "0") + + got := Hyperlink("https://example.com", "click here") + want := "click here (https://example.com)" + if got != want { + t.Errorf("parens fallback: want %q, got %q", want, got) + } +} + +func TestHyperlink_Fallback_None(t *testing.T) { + withHyperlinkEnv(t, "0") + + const text = "click here" + got := Hyperlink("https://example.com", text, WithHyperlinkFallback(HyperlinkFallbackNone)) + if got != text { + t.Errorf("none fallback: want %q, got %q", text, got) + } + // None fallback returns the exact input slice — no allocation. + if got != text { + t.Errorf("identity: want same string, got %q", got) + } +} + +func TestHyperlink_Fallback_Brackets(t *testing.T) { + withHyperlinkEnv(t, "0") + + got := Hyperlink("https://example.com", "click here", WithHyperlinkFallback(HyperlinkFallbackBrackets)) + want := "click here [https://example.com]" + if got != want { + t.Errorf("brackets fallback: want %q, got %q", want, got) + } +} + +// ---- Edge cases ------------------------------------------------------------- + +func TestHyperlink_EmptyText(t *testing.T) { + // Empty text returns empty regardless of hyperlink support or URI. + for _, env := range []string{"0", "1"} { + withHyperlinkEnv(t, env) + got := Hyperlink("https://example.com", "") + if got != "" { + t.Errorf("VELOCITY_HYPERLINKS=%s: empty text must return empty, got %q", env, got) + } + } +} + +func TestHyperlink_EmptyURI_Supported(t *testing.T) { + withHyperlinkEnv(t, "1") + + // Empty URI with supported terminal falls through to fallback — we don't + // emit a zero-URI OSC sequence mid-string as it would close any active link. + got := Hyperlink("", "label", WithHyperlinkFallback(HyperlinkFallbackParens)) + // No URI to append in parens form, so just the text. + if got != "label" { + t.Errorf("empty URI + supported: want %q, got %q", "label", got) + } +} + +func TestHyperlink_EmptyURI_Unsupported_AllFallbacks(t *testing.T) { + withHyperlinkEnv(t, "0") + + cases := []struct { + mode HyperlinkFallback + want string + }{ + {HyperlinkFallbackParens, "label"}, + {HyperlinkFallbackNone, "label"}, + {HyperlinkFallbackBrackets, "label"}, + } + for _, tc := range cases { + got := Hyperlink("", "label", WithHyperlinkFallback(tc.mode)) + if got != tc.want { + t.Errorf("mode %d, empty URI: want %q, got %q", tc.mode, tc.want, got) + } + } +} + +func TestHyperlink_DefaultFallback_IsParens(t *testing.T) { + withHyperlinkEnv(t, "0") + + // Confirm the zero-value of HyperlinkFallback is Parens, not None. + var f HyperlinkFallback + if f != HyperlinkFallbackParens { + t.Errorf("default HyperlinkFallback should be HyperlinkFallbackParens (0), got %d", f) + } +} diff --git a/integration_test.go b/integration_test.go index 635b57a..5c11eb8 100644 --- a/integration_test.go +++ b/integration_test.go @@ -6,22 +6,19 @@ import ( ) func TestIntegration(_ *testing.T) { - // Test basic development logger - log := NewDevelopment() + log := New(WithDevelopment()) log.Debug("Debug message") log.Info("Info message") log.Warn("Warning message") log.Error("Error message") - // Test with fields log.Info("Server started", String("addr", ":8080"), Int("pid", os.Getpid()), Bool("tls", true), ) - // Test themes themes := []*Theme{ ThemeNightOwl, ThemeSolarized, @@ -30,19 +27,16 @@ func TestIntegration(_ *testing.T) { } for _, theme := range themes { - log := NewWithOptions( + log := New( WithConsoleOutput(os.Stdout), WithTheme(theme), WithLevel(LevelInfo), ) - log.Info("Testing theme", String("theme", theme.Name)) + log.Info("Testing theme", String("theme", theme.Name())) } } func TestMultiWriterIntegration(t *testing.T) { - // Multi-writer enables simultaneous output to different destinations, - // essential for maintaining human-readable console logs while preserving - // structured data for monitoring systems multiWriter := NewMultiWriter() if multiWriter == nil { diff --git a/level.go b/level.go index 669c89b..e027a6b 100644 --- a/level.go +++ b/level.go @@ -1,8 +1,8 @@ package velocity import ( + "fmt" "strings" - "sync/atomic" ) type Level int32 @@ -102,31 +102,25 @@ func (l Level) ConciseLabel() string { } } -// AtomicLevel provides thread-safe level management with zero-cost reads. -type AtomicLevel struct { - level int32 -} - -func NewAtomicLevel(l Level) *AtomicLevel { - return &AtomicLevel{level: int32(l)} -} - -func (al *AtomicLevel) Level() Level { - return Level(atomic.LoadInt32(&al.level)) -} - -func (al *AtomicLevel) SetLevel(l Level) { - atomic.StoreInt32(&al.level, int32(l)) -} - -// Enabled checks if the given level is enabled. -// This is the critical path - called on every log attempt. -func (al *AtomicLevel) Enabled(l Level) bool { - return l >= Level(atomic.LoadInt32(&al.level)) -} - -func (al *AtomicLevel) CompareAndSwap(old, newLevel Level) bool { - return atomic.CompareAndSwapInt32(&al.level, int32(old), int32(newLevel)) +// ParseLevel converts a string level name to a Level constant. +// Valid levels (case-insensitive): debug, info, warn, warning, error, fatal, off. +func ParseLevel(level string) (Level, error) { + switch strings.ToLower(level) { + case "debug": + return LevelDebug, nil + case "info": + return LevelInfo, nil + case "warn", "warning": + return LevelWarn, nil + case "error": + return LevelError, nil + case "fatal": + return LevelFatal, nil + case "off": + return LevelOff, nil + default: + return LevelOff, fmt.Errorf("velocity: invalid log level: %q", level) + } } // MustParseLevel converts a string level to Level constant. diff --git a/live/doc.go b/live/doc.go new file mode 100644 index 0000000..daeec12 --- /dev/null +++ b/live/doc.go @@ -0,0 +1,16 @@ +// Package live provides stateful terminal UI primitives: progress bars, spinners, +// and multi-progress displays. These types own goroutines and have explicit lifecycle +// (Stop/Complete), which is why they live apart from the static Renderables in the +// root package. +// +// # TTY awareness +// +// All types detect whether their writer is a real terminal at construction time. +// When the writer is not a terminal (piped output, redirected stdout, CI runners): +// - ProgressBar suppresses per-tick renders; Complete() emits a single summary line. +// - Spinner suppresses per-frame renders; Stop/StopWithMessage still print their message. +// - MultiProgress suppresses all renders; Stop() is a no-op for display cleanup. +// +// This prevents \r, \033[K, and cursor movement sequences from appearing in log files +// or aggregated output streams. +package live diff --git a/pretty/progress.go b/live/progress.go similarity index 74% rename from pretty/progress.go rename to live/progress.go index 5210f8c..f435d99 100644 --- a/pretty/progress.go +++ b/live/progress.go @@ -1,16 +1,45 @@ -package pretty +package live import ( "fmt" "io" + "os" "strings" "sync" "sync/atomic" "time" + + "golang.org/x/term" ) +// shouldEmitColour reports whether ANSI control sequences should be written to w. +// Priority order matches the root velocity package: +// 1. NO_COLOR= — always suppress (https://no-color.org) +// 2. FORCE_COLOR= — always emit +// 3. fd-level TTY detection via term.IsTerminal +// +// Using FORCE_COLOR=1 is the documented workaround for Windows terminal emulators +// (VS Code, Git Bash, Windows Terminal) that proxy stdout through a named pipe, +// causing term.IsTerminal to return false even on a colour-capable terminal. +func shouldEmitColour(w io.Writer) bool { + if os.Getenv("NO_COLOR") != "" { + return false + } + if os.Getenv("FORCE_COLOR") != "" { + return true + } + if f, ok := w.(*os.File); ok { + return term.IsTerminal(int(f.Fd())) //nolint:gosec // G115: uintptr fd fits in int on all supported platforms + } + return false +} + // ProgressBar displays progress for long-running operations. // Thread-safe for concurrent updates while logging continues. +// +// When the writer is not a terminal (piped output, CI, log files), per-tick +// renders are suppressed to avoid emitting \r and ANSI erase sequences. +// A single summary line is written on Complete(). type ProgressBar struct { started time.Time lastDraw time.Time @@ -23,6 +52,7 @@ type ProgressBar struct { mu sync.Mutex active atomic.Bool noOp bool + isTTY bool } func NewProgressBar(w io.Writer, total int64, label string) *ProgressBar { @@ -45,6 +75,7 @@ func NewProgressBar(w io.Writer, total int64, label string) *ProgressBar { width: 40, started: time.Now(), done: make(chan struct{}), + isTTY: shouldEmitColour(w), } pb.active.Store(true) @@ -114,7 +145,10 @@ func (pb *ProgressBar) Complete() { close(pb.done) - if pb.writer != nil { + // Finalise the output line. On TTY the render() call left the cursor at + // end-of-bar; a newline finishes the line. On non-TTY render() emits a + // summary already, so nothing extra is needed. + if pb.writer != nil && pb.isTTY { _, _ = fmt.Fprintln(pb.writer) } } @@ -128,10 +162,23 @@ func (pb *ProgressBar) render() { percent = float64(pb.current) / float64(pb.total) * 100 } - filled := min(int(float64(pb.width)*percent/100), pb.width) - elapsed := time.Since(pb.started) + // Non-TTY: only emit a summary line on completion; skip in-progress ticks + // so \r and ANSI erase sequences don't appear in pipes or log files. + if !pb.isTTY { + if pb.current >= pb.total { + if pb.label != "" { + _, _ = fmt.Fprintf(pb.writer, "%s: completed in %s\n", pb.label, formatDuration(elapsed)) + } else { + _, _ = fmt.Fprintf(pb.writer, "completed in %s\n", formatDuration(elapsed)) + } + } + return + } + + filled := min(int(float64(pb.width)*percent/100), pb.width) + var eta time.Duration if pb.current > 0 && pb.current < pb.total { rate := float64(pb.current) / elapsed.Seconds() @@ -190,6 +237,10 @@ func (pb *ProgressBar) renderLoop() { // Spinner displays an animated spinner for indeterminate progress. // Thread-safe and works concurrently with logging. +// +// When the writer is not a terminal (piped output, CI, log files), frame +// renders are suppressed to avoid emitting \r and ANSI erase sequences. +// Stop/StopWithMessage still emit their final line. type Spinner struct { writer io.Writer done chan struct{} @@ -199,6 +250,7 @@ type Spinner struct { mu sync.Mutex active atomic.Bool noOp bool + isTTY bool } func NewSpinner(w io.Writer, label string) *Spinner { @@ -214,6 +266,7 @@ func NewSpinner(w io.Writer, label string) *Spinner { label: label, frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, done: make(chan struct{}), + isTTY: shouldEmitColour(w), } s.active.Store(true) @@ -244,7 +297,8 @@ func (s *Spinner) Stop() { close(s.done) - if s.writer != nil { + // Only erase the spinner line on TTY; non-TTY never drew anything. + if s.writer != nil && s.isTTY { _, _ = fmt.Fprint(s.writer, "\r\033[K") } } @@ -301,6 +355,11 @@ func (s *Spinner) render() { s.mu.Lock() defer s.mu.Unlock() + // Suppress control sequences on non-TTY writers (pipes, files, CI). + if !s.isTTY { + return + } + _, _ = fmt.Fprint(s.writer, "\r\033[K") _, _ = fmt.Fprintf(s.writer, "%s %s", s.frames[s.current], s.label) @@ -327,6 +386,7 @@ func (s *Spinner) start() { }() } +// SpinnerStyle selects the animation frame set. type SpinnerStyle int const ( @@ -377,6 +437,9 @@ func formatDuration(d time.Duration) string { } // MultiProgress manages multiple progress bars or spinners simultaneously. +// +// When the writer is not a terminal, frame renders are suppressed to avoid +// emitting cursor movement and ANSI erase sequences into pipes or log files. type MultiProgress struct { lastDraw time.Time writer io.Writer @@ -384,6 +447,7 @@ type MultiProgress struct { items []ProgressItem mu sync.Mutex active atomic.Bool + isTTY bool } // ProgressItem is implemented by types that can render themselves as a progress line. @@ -396,6 +460,7 @@ func NewMultiProgress(w io.Writer) *MultiProgress { writer: w, items: make([]ProgressItem, 0), done: make(chan struct{}), + isTTY: shouldEmitColour(w), } mp.active.Store(true) @@ -430,18 +495,22 @@ func (mp *MultiProgress) Stop() { close(mp.done) - mp.mu.Lock() - for range mp.items { - _, _ = fmt.Fprint(mp.writer, "\r\033[K\n") + // Only erase progress lines on TTY; non-TTY never drew any. + if mp.isTTY { + mp.mu.Lock() + for range mp.items { + _, _ = fmt.Fprint(mp.writer, "\r\033[K\n") + } + mp.mu.Unlock() } - mp.mu.Unlock() } func (mp *MultiProgress) render() { mp.mu.Lock() defer mp.mu.Unlock() - if len(mp.items) == 0 { + // Suppress control sequences on non-TTY writers (pipes, files, CI). + if !mp.isTTY || len(mp.items) == 0 { return } diff --git a/live/progress_test.go b/live/progress_test.go new file mode 100644 index 0000000..c0bfad7 --- /dev/null +++ b/live/progress_test.go @@ -0,0 +1,192 @@ +package live + +import ( + "bytes" + "io" + "strings" + "sync" + "testing" +) + +// TestProgressBar_ConcurrentComplete verifies that simultaneous Complete calls +// do not panic due to double-close of the done channel. +func TestProgressBar_ConcurrentComplete(_ *testing.T) { + pb := NewProgressBar(io.Discard, 100, "test") + + var wg sync.WaitGroup + for range 100 { + wg.Add(1) + go func() { + defer wg.Done() + pb.Complete() + }() + } + wg.Wait() +} + +// TestSpinner_ConcurrentStop verifies that simultaneous Stop calls +// do not panic due to double-close of the done channel. +func TestSpinner_ConcurrentStop(_ *testing.T) { + s := NewSpinner(io.Discard, "test") + + var wg sync.WaitGroup + for range 100 { + wg.Add(1) + go func() { + defer wg.Done() + s.Stop() + }() + } + wg.Wait() +} + +// TestMultiProgress_ConcurrentStop verifies that simultaneous Stop calls +// do not panic due to double-close of the done channel. +func TestMultiProgress_ConcurrentStop(_ *testing.T) { + mp := NewMultiProgress(io.Discard) + + var wg sync.WaitGroup + for range 100 { + wg.Add(1) + go func() { + defer wg.Done() + mp.Stop() + }() + } + wg.Wait() +} + +// TestSpinner_SetStyle_NilReceiver verifies nil safety, consistent with other +// Spinner methods. +func TestSpinner_SetStyle_NilReceiver(_ *testing.T) { + var s *Spinner + s.SetStyle(SpinnerStyleDots) +} + +// --- Non-TTY branch: bytes.Buffer is not a terminal --- + +// TestProgressBar_NonTTY_NoControlSequences verifies that a ProgressBar writing to +// a bytes.Buffer (non-TTY) never emits \r or ANSI erase sequences during updates, +// and that Complete() writes a plain summary line instead. +func TestProgressBar_NonTTY_NoControlSequences(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + pb := NewProgressBar(&buf, 10, "loading") + + if pb.isTTY { + t.Skip("test environment has a TTY-backed buffer — skipping non-TTY test") + } + + // Update a few times; the non-TTY render should produce no output. + pb.Update(3) + pb.Update(7) + + if buf.Len() != 0 { + t.Errorf("non-TTY ProgressBar wrote bytes before Complete: %q", buf.String()) + } + + pb.Complete() + + out := buf.String() + if strings.Contains(out, "\r") || strings.Contains(out, "\033[") { + t.Errorf("non-TTY ProgressBar emitted control sequences: %q", out) + } + // Must include the label and completion indication. + if !strings.Contains(out, "loading") { + t.Errorf("expected label in non-TTY summary: %q", out) + } + if !strings.Contains(out, "completed") { + t.Errorf("expected 'completed' in non-TTY summary: %q", out) + } +} + +// TestSpinner_NonTTY_NoControlSequences verifies that a Spinner writing to a +// bytes.Buffer never emits \r or ANSI erase sequences per frame, but still +// writes a message on StopWithMessage. +func TestSpinner_NonTTY_NoControlSequences(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + s := NewSpinner(&buf, "working") + + if s.isTTY { + t.Skip("test environment has a TTY-backed buffer — skipping non-TTY test") + } + + // After construction the goroutine may have ticked once; give it a moment, + // then confirm no control bytes were written. + s.Stop() + + out := buf.String() + if strings.Contains(out, "\r") || strings.Contains(out, "\033[") { + t.Errorf("non-TTY Spinner emitted control sequences on Stop: %q", out) + } +} + +// TestSpinner_NonTTY_StopWithMessage writes a final line on non-TTY. +func TestSpinner_NonTTY_StopWithMessage(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + s := NewSpinner(&buf, "working") + + if s.isTTY { + t.Skip("test environment has a TTY-backed buffer — skipping non-TTY test") + } + + s.StopWithMessage("all done") + + out := buf.String() + if strings.Contains(out, "\r") || strings.Contains(out, "\033[") { + t.Errorf("non-TTY Spinner emitted control sequences: %q", out) + } + if !strings.Contains(out, "all done") { + t.Errorf("expected message in output: %q", out) + } +} + +// --- FORCE_COLOR / NO_COLOR env var handling --- + +// TestProgressBar_ForceColor_EnablesOnNonTTY is a regression test for the bug where +// progress types ignored FORCE_COLOR, leaving them inactive on non-TTY writers even +// when FORCE_COLOR=1 was set (e.g. Windows Terminal piping output to the shell). +// We use io.Discard to avoid racing the render goroutine against the test goroutine +// on a shared bytes.Buffer — the assertion is about the isTTY flag, not I/O content. +func TestProgressBar_ForceColor_EnablesOnNonTTY(t *testing.T) { + // Cannot run in parallel — t.Setenv modifies process-wide env vars. + t.Setenv("FORCE_COLOR", "1") + + pb := NewProgressBar(io.Discard, 10, "loading") + + if !pb.isTTY { + t.Error("ProgressBar.isTTY should be true under FORCE_COLOR=1 regardless of fd type") + } + pb.Complete() +} + +// TestSpinner_ForceColor_EnablesOnNonTTY verifies the same for Spinner. +func TestSpinner_ForceColor_EnablesOnNonTTY(t *testing.T) { + t.Setenv("FORCE_COLOR", "1") + + s := NewSpinner(io.Discard, "working") + + if !s.isTTY { + t.Error("Spinner.isTTY should be true under FORCE_COLOR=1 regardless of fd type") + } + s.Stop() +} + +// TestProgressBar_NoColor_Disables verifies that NO_COLOR=1 suppresses the isTTY flag +// even when FORCE_COLOR is absent. +func TestProgressBar_NoColor_Disables(t *testing.T) { + t.Setenv("NO_COLOR", "1") + t.Setenv("FORCE_COLOR", "") // ensure FORCE_COLOR does not interfere + + pb := NewProgressBar(io.Discard, 10, "loading") + + if pb.isTTY { + t.Error("ProgressBar.isTTY should be false under NO_COLOR=1") + } + pb.Complete() +} diff --git a/logger.go b/logger.go index 6707e3b..9d006d9 100644 --- a/logger.go +++ b/logger.go @@ -1,110 +1,188 @@ package velocity import ( + "errors" "fmt" "io" "os" "runtime" + "strings" "sync" "sync/atomic" "time" ) +// writerSet is a shared container for the MultiWriter, its guard mutex, and +// the scanSecure flag. Parent and child loggers hold the same *writerSet pointer +// so that a writer added to the parent after a child is created is visible to +// all siblings — and the scanSecure flag update reaches them too. +// AddWriter initialises the inner MultiWriter on first use. +type writerSet struct { + mw *MultiWriter + mu sync.RWMutex + + // scanSecure is true when at least one output path would redact secure data. + // Stored here (not on Logger) so AddWriter/RemoveWriter on any family member + // immediately propagates to all child loggers sharing the same writerSet. + scanSecure atomic.Bool +} + type Logger struct { sampler Sampler - cfg *Config - bufPool *BufferPool - consoleWriter *ConsoleWriter - jsonWriter *JSONWriter - statusFormatter *StatusFormatter + cfg *config + bufPool *BufferPool + consoleWriter *ConsoleWriter + jsonWriter *JSONWriter - // Additional writers added post-initialisation for dynamic log routing - additionalWriters *MultiWriter + // writers is shared by reference between a logger and all children created + // via With / Detailed / WithComponent / WithRequest. AddWriter on any member + // of the family is immediately visible to all siblings. + writers *writerSet // baseFields are prepended to every log entry on this logger. // Set by With() and inherited by child loggers. baseFields []Field - writersMu sync.RWMutex - level atomic.Int32 + // forceTreeDisplay makes every log call on this logger render fields as a tree, + // regardless of FieldDisplayMode. Set via Detailed(). + forceTreeDisplay bool + + // secureScanEnabled is the user-facing gate. False when WithSecureTags(false) was + // applied; in that case writers.scanSecure stays false regardless of the writer mix. + secureScanEnabled atomic.Bool + + level atomic.Int32 + closed atomic.Bool +} + +// New constructs a Logger from the given options. Panics if the resolved +// configuration is invalid (e.g. BufferSize < 256, sampler with both counts +// zero). Apply preset options first, then override-specific ones: +// +// log := velocity.New(velocity.WithDevelopment(), velocity.WithLevel(velocity.LevelWarn)) +func New(opts ...Option) *Logger { + l, err := TryNew(opts...) + if err != nil { + panic(fmt.Sprintf("velocity: invalid configuration: %v", err)) + } + return l +} + +// NopLogger returns a Logger that discards all output. Intended for tests and +// wiring paths where a non-nil logger is required but output is unwanted. +func NopLogger() *Logger { + return New(WithNop()) +} + +// TryNew constructs a Logger from the given options, returning any validation +// error rather than panicking. +func TryNew(opts ...Option) (*Logger, error) { + cfg := defaultConfig() + for _, opt := range opts { + if opt != nil { + opt(cfg) + } + } + + if err := validateConfig(cfg); err != nil { + return nil, err + } + + l := newFromConfig(cfg) + + // WithTesting registers cleanup on the testing.T after the logger is built + // so that Close() flushes the async MultiWriter before the test ends. + for _, opt := range opts { + if tw, ok := extractTestingOpt(opt); ok { + tw.t.Cleanup(func() { _ = l.Close() }) + break + } + } + + return l, nil } -func New(w io.Writer) *Logger { - cfg := DefaultConfig() - cfg.ConsoleOutput = w - return NewWithConfig(cfg) +// extractTestingOpt peeks at an option to see whether it wired a testingWriter. +// We need the TestingT so we can register t.Cleanup on the logger after build. +func extractTestingOpt(opt Option) (*testingWriter, bool) { + if opt == nil { + return nil, false + } + probe := &config{} + opt(probe) + if tw, ok := probe.ConsoleOutput.(*testingWriter); ok { + return tw, true + } + return nil, false } -func NewWithConfig(cfg *Config) *Logger { - // Respect the config's intention - nil output means disabled +func newFromConfig(cfg *config) *Logger { logger := &Logger{ cfg: cfg, bufPool: NewBufferPool(), sampler: cfg.Sampler, + writers: &writerSet{}, } + // Default: secure tag scanning is enabled unless explicitly disabled. + logger.secureScanEnabled.Store(!cfg.DisableSecureTags) - // Using the most permissive level ensures logs aren't dropped when outputs have different thresholds + // Use the most permissive level so logs aren't dropped when outputs have + // different thresholds. effectiveLevel := min(cfg.StructuredLevel, cfg.ConsoleLevel) logger.level.Store(int32(effectiveLevel)) - // Initialise console writer if configured if cfg.ConsoleOutput != nil && cfg.ConsoleOutput != io.Discard { - logger.consoleWriter = NewConsoleWriterWithOptions(cfg.ConsoleOutput, cfg.ConsoleTheme, cfg.DisplayTimezone, cfg.FieldDisplayMode) - // Apply time format if specified. Recompute cached prefix widths so - // Logger.Render's indent matches the actual rendered timestamp width — - // otherwise a custom TimeFormat shorter than RFC3339 leaves the indent - // stale at the construction-time width. + // Resolve the theme before constructing the writer so it never needs to + // know about DisableColour. When colour is disabled we pass noColourTheme + // (all cached escapes are empty strings) instead of the user-supplied theme. + consoleTheme := cfg.ConsoleTheme + if cfg.DisableColour { + consoleTheme = noColourTheme + } + logger.consoleWriter = NewConsoleWriterWithOptions(cfg.ConsoleOutput, consoleTheme, cfg.DisplayTimezone, cfg.FieldDisplayMode) + // Recompute cached prefix widths after applying a custom TimeFormat so + // Logger.Render's indent matches the actual rendered timestamp width. if cfg.TimeFormat != "" && logger.consoleWriter != nil { logger.consoleWriter.template.timeFormat = cfg.TimeFormat logger.consoleWriter.template.initCache() } - // Status formatter respects terminal capability and colours - isTTY := logger.consoleWriter != nil && logger.consoleWriter.IsTTY() - theme := cfg.ConsoleTheme - if cfg.DisableColour { - theme = nil - } - logger.statusFormatter = NewStatusFormatter(theme, isTTY) } - // Initialise JSON writer if configured if cfg.StructuredOutput != nil && cfg.StructuredOutput != io.Discard { logger.jsonWriter = NewJSONWriter(cfg.StructuredOutput) } - // Ensure status formatter exists even without console writer - if logger.statusFormatter == nil { - logger.statusFormatter = NewStatusFormatter(nil, false) - } + // Compute initial scan flag based on writer mix at construction time. + logger.recomputeScanSecure() return logger } -func NewWithBuilder(builder *Builder) *Logger { - cfg := builder.MustBuild() - return NewWithConfig(cfg) -} +func validateConfig(cfg *config) error { + var errs []error -func NewWithOptions(opts ...Option) *Logger { - builder := NewConfig() - for _, opt := range opts { - opt(builder) + if cfg.BufferSize < 256 { + errs = append(errs, fmt.Errorf("buffer size must be at least 256 bytes, got %d", cfg.BufferSize)) + } + if cfg.BufferSize > 1024*1024 { + errs = append(errs, fmt.Errorf("buffer size must not exceed 1MB, got %d", cfg.BufferSize)) + } + if cfg.FieldPoolSize < 0 { + errs = append(errs, fmt.Errorf("field pool size must not be negative, got %d", cfg.FieldPoolSize)) + } + if cfg.FieldPoolSize > 10000 { + errs = append(errs, fmt.Errorf("field pool size must not exceed 10000, got %d", cfg.FieldPoolSize)) + } + if cfg.Sampler != nil { + if cs, ok := cfg.Sampler.(*CountSampler); ok { + if cs.Initial == 0 && cs.Thereafter == 0 { + errs = append(errs, errors.New("sampler initial and thereafter counts must not both be zero")) + } + } } - return NewWithBuilder(builder) -} - -// NewDevelopment creates a logger optimised for development with colourful console output. -func NewDevelopment() *Logger { - builder := PresetDevelopment() - cfg := builder.MustBuild() - return NewWithConfig(cfg) -} -func NewForTesting(w io.Writer) *Logger { - builder := PresetTesting(w) - cfg := builder.MustBuild() - return NewWithConfig(cfg) + return errors.Join(errs...) } func (l *Logger) SetLevel(level Level) { @@ -122,25 +200,26 @@ func (l *Logger) Level() Level { } // With returns a child logger that prepends the given fields to every log entry. -// The child shares writers, config, and sampler with the parent. +// The child shares the writer topology (writers) with the parent, so writers +// added to the parent after the child is created are immediately visible to both. // Level is snapshotted at the time of the call; dynamic parent level changes // do not propagate to the child after creation. func (l *Logger) With(fields ...Field) *Logger { if l == nil || len(fields) == 0 { return l } - // additionalWriters is shared by reference. AddWriter on the child after - // creation diverges from the parent because writersMu is not shared. child := &Logger{ - cfg: l.cfg, - bufPool: l.bufPool, - consoleWriter: l.consoleWriter, - jsonWriter: l.jsonWriter, - statusFormatter: l.statusFormatter, - sampler: l.sampler, - additionalWriters: l.additionalWriters, + cfg: l.cfg, + bufPool: l.bufPool, + consoleWriter: l.consoleWriter, + jsonWriter: l.jsonWriter, + sampler: l.sampler, + writers: l.writers, // shared pointer — parent topology changes propagate + forceTreeDisplay: l.forceTreeDisplay, } child.level.Store(l.level.Load()) + child.secureScanEnabled.Store(l.secureScanEnabled.Load()) + // scanSecure lives on the shared writers — no copy needed. newBase := make([]Field, len(l.baseFields)+len(fields)) copy(newBase, l.baseFields) copy(newBase[len(l.baseFields):], fields) @@ -148,53 +227,178 @@ func (l *Logger) With(fields ...Field) *Logger { return child } -// AddWriter adds a named writer to receive log entries. -// Thread-safe for concurrent calls. -// Writers process entries asynchronously via MultiWriter. -func (l *Logger) AddWriter(name string, w Writer) { - if l == nil { +// recomputeScanSecure recalculates whether the tag scan must run on +// every log call. The result is written to writers.scanSecure so all loggers +// sharing the same writerSet (parent + every child created via With/Detailed) +// see the updated flag immediately. Called at AddWriter/RemoveWriter time. +// +// The scan fires when: +// - scan is globally enabled (secureScanEnabled), AND +// - at least one output path is untrusted: +// a) the JSON writer is always untrusted, OR +// b) the console writer is on a non-TTY (pipe/file), OR +// c) any additional writer registered without WriterTrusted() +// +// Must be called with writers.mu held (write lock) or before the logger is shared. +func (l *Logger) recomputeScanSecure() { + if !l.secureScanEnabled.Load() { + l.writers.scanSecure.Store(false) return } - l.writersMu.Lock() - defer l.writersMu.Unlock() + // JSON writer is always untrusted. + if l.jsonWriter != nil { + l.writers.scanSecure.Store(true) + return + } - if l.additionalWriters == nil { - l.additionalWriters = NewMultiWriter() + // Non-TTY console writer is untrusted (writing to a pipe or file). + if l.consoleWriter != nil && !l.consoleWriter.isTTY { + l.writers.scanSecure.Store(true) + return } - l.additionalWriters.AddWriter(name, w) + + // Any untrusted additional writer flips the flag. + // We hold l.writers.mu (write lock) here; mw.mu is separate, so take it briefly. + if l.writers != nil && l.writers.mw != nil { + l.writers.mw.mu.Lock() + hasUntrusted := false + for _, ws := range l.writers.mw.workers { + if !ws.isTrusted { + hasUntrusted = true + break + } + } + l.writers.mw.mu.Unlock() + if hasUntrusted { + l.writers.scanSecure.Store(true) + return + } + } + + l.writers.scanSecure.Store(false) } -// RemoveWriter removes a named writer. -// Thread-safe for concurrent calls. -func (l *Logger) RemoveWriter(name string) { +// AddWriter registers a named writer to receive log entries. +// Options control per-writer behaviour; see WriterTrusted. +// Thread-safe; writers process entries asynchronously via MultiWriter. +// Writers added to a parent logger are immediately visible to all child loggers +// created via With, Detailed, WithComponent, or WithRequest. +func (l *Logger) AddWriter(name string, w Writer, opts ...WriterOption) { if l == nil { return } - l.writersMu.Lock() - defer l.writersMu.Unlock() + l.writers.mu.Lock() + defer l.writers.mu.Unlock() + + if l.writers.mw == nil { + l.writers.mw = NewMultiWriter() + } + l.writers.mw.AddWriter(name, w, opts...) + + // Propagate trust to the writer itself when it exposes the hook. + // This keeps writer.IsTrusted() consistent with the MultiWriter worker state. + o := applyWriterOptions(opts) + if o.isTrusted { + if st, ok := w.(interface{ SetTrusted(bool) }); ok { + st.SetTrusted(true) + } + } + + l.recomputeScanSecure() +} - if l.additionalWriters != nil { - l.additionalWriters.RemoveWriter(name) +// RemoveWriter removes the named writer and returns it for inspection or flush. +// The MultiWriter worker drains and closes the writer asynchronously after removal — +// do not call Close on the returned value, or you risk a double-close panic on writers +// that aren't idempotent. Returns nil if no writer with that name exists. +// Thread-safe. +func (l *Logger) RemoveWriter(name string) Writer { + if l == nil { + return nil } + + l.writers.mu.Lock() + defer l.writers.mu.Unlock() + + if l.writers.mw == nil { + return nil + } + w := l.writers.mw.RemoveWriter(name) + l.recomputeScanSecure() + return w } -// Close closes any additional writers that were added to the logger. -// Thread-safe and nil-safe - returns nil if logger is nil or has no additional writers. +// Writer returns the writer registered under name, or nil. +// Useful for inspecting writer capabilities without removing it. +// Thread-safe. +func (l *Logger) Writer(name string) Writer { + if l == nil { + return nil + } + + l.writers.mu.RLock() + defer l.writers.mu.RUnlock() + + if l.writers.mw == nil { + return nil + } + return l.writers.mw.WriterByName(name) +} + +// Close flushes and shuts down all writers owned by the logger. +// +// Specifically: the console writer is flushed (its output buffer drained), the +// JSON writer is flushed, and all named writers added via AddWriter are drained +// and closed. Caller-supplied io.Writers passed via WithConsoleOutput / +// WithStructuredOutput are NOT closed — the logger does not own those handles. +// +// Close is idempotent: subsequent calls are no-ops. After Close returns, any +// further log calls on this logger drop silently. +// +// Returns the first error encountered; remaining flushes still proceed. func (l *Logger) Close() error { if l == nil { return nil } + // Already closed — nothing to do. + if !l.closed.CompareAndSwap(false, true) { + return nil + } - l.writersMu.Lock() - defer l.writersMu.Unlock() + var firstErr error + setErr := func(e error) { + if firstErr == nil && e != nil { + firstErr = e + } + } - if l.additionalWriters != nil { - return l.additionalWriters.Close() + // Flush the console writer if it implements io.Closer (ring-buffer path does). + if l.consoleWriter != nil { + if c, ok := any(l.consoleWriter).(io.Closer); ok { + setErr(c.Close()) + } } - return nil + // Flush the JSON writer if it implements io.Closer. + if l.jsonWriter != nil { + if c, ok := any(l.jsonWriter).(io.Closer); ok { + setErr(c.Close()) + } + } + + l.writers.mu.Lock() + defer l.writers.mu.Unlock() + + if l.writers.mw != nil { + setErr(l.writers.mw.Close()) + // Nil out after close so sibling loggers (children sharing the same + // writerSet) don't attempt a second close on the already-drained MultiWriter. + l.writers.mw = nil + } + + return firstErr } func (l *Logger) Debug(msg string, fields ...Field) { @@ -202,7 +406,7 @@ func (l *Logger) Debug(msg string, fields ...Field) { fmt.Fprintf(os.Stderr, "[!DBG] %s\n", msg) return } - if !l.isEnabled(LevelDebug) { + if l.closed.Load() || !l.isEnabled(LevelDebug) { return } l.log(LevelDebug, msg, fields...) @@ -213,7 +417,7 @@ func (l *Logger) Info(msg string, fields ...Field) { fmt.Fprintf(os.Stderr, "[INFO] %s\n", msg) return } - if !l.isEnabled(LevelInfo) { + if l.closed.Load() || !l.isEnabled(LevelInfo) { return } l.log(LevelInfo, msg, fields...) @@ -224,7 +428,7 @@ func (l *Logger) Warn(msg string, fields ...Field) { fmt.Fprintf(os.Stderr, "[WARN] %s\n", msg) return } - if !l.isEnabled(LevelWarn) { + if l.closed.Load() || !l.isEnabled(LevelWarn) { return } l.log(LevelWarn, msg, fields...) @@ -235,71 +439,215 @@ func (l *Logger) Error(msg string, fields ...Field) { fmt.Fprintf(os.Stderr, "[ERR!] %s\n", msg) return } - if !l.isEnabled(LevelError) { + if l.closed.Load() || !l.isEnabled(LevelError) { return } l.log(LevelError, msg, fields...) } -func (l *Logger) Fatal(msg string, fields ...Field) { +// Status logs a message at the given level with a StatusKind badge. +// +// Console output: renders an inline indented badge ([OKAY] / [FAIL] etc.) with +// no timestamp or level label — visually subordinate to the surrounding log lines. +// +// JSON / structured output: emits a full structured record with a "status" field +// set to the lowercase kind string (ok, fail, warn, info, pending, skip), so log +// queries continue to work. +// +// All standard log-call semantics apply: level filtering, sampling, base fields. +func (l *Logger) Status(level Level, kind StatusKind, msg string, fields ...Field) { if l == nil { - fmt.Fprintf(os.Stderr, "[FATL] %s\n", msg) - os.Exit(1) + fmt.Fprintf(os.Stderr, "[%s] %s\n", kind.String(), msg) + return } - l.log(LevelFatal, msg, fields...) - if l.cfg != nil && l.cfg.FatalHandler != nil { - l.cfg.FatalHandler() + if l.closed.Load() || !l.isEnabled(level) { return } - os.Exit(1) + + // Honour the sampler before doing any work — consistent with logInternal. + if l.sampler != nil && !l.sampler.Sample(level, msg) { + return + } + + // Merge baseFields with call-site fields so child loggers stamp their + // context fields onto both the console badge and the structured record. + allFields := fields + if len(l.baseFields) > 0 { + merged := make([]Field, len(l.baseFields)+len(fields)) + copy(merged, l.baseFields) + copy(merged[len(l.baseFields):], fields) + allFields = merged + } + + // Console path: inline badge via Render, no timestamp or level label. + // Uses the logger's active theme and routes through the console writer mutex + // so status items cannot interleave with concurrent log lines. + if l.consoleWriter != nil && level >= l.cfg.ConsoleLevel { + // Apply secure-tag processing to the message before rendering to the console. + // TTY (trusted) writers show the plaintext with delimiters stripped; + // non-TTY (untrusted, e.g. piped to a file) writers show the redaction mark. + consoleMsg := msg + if l.writers.scanSecure.Load() && strings.IndexByte(msg, '<') >= 0 { + if l.consoleWriter.isTTY { + consoleMsg = stripSecureTags(msg) + } else { + consoleMsg = redactSecureTags(msg, "[REDACTED]") + } + } + item := NewStatusItem(kind, consoleMsg, l.Theme(), allFields...) + l.Render(item) + } + + // Structured / additional-writer path: full record with statusKind set. + // The console writer is skipped here — it already rendered inline above. + // Pass allFields so structured output also includes baseFields. + l.logStatusStructuredWithFields(level, kind, msg, allFields) } -// DebugDetailed logs a debug message with fields always displayed in tree format -func (l *Logger) DebugDetailed(msg string, fields ...Field) { +// logStatusStructuredWithFields emits a structured log entry for Status calls. +// Only JSON and additional writers receive this entry; the console writer is +// intentionally skipped because Status renders inline via Render instead. +// fields must already include baseFields — the caller is responsible for merging. +func (l *Logger) logStatusStructuredWithFields(level Level, kind StatusKind, msg string, fields []Field) { if l == nil { - fmt.Fprintf(os.Stderr, "[DEBU] %s\n", msg) return } - if !l.isEnabled(LevelDebug) { + + // Nothing to do when there are no structured outputs. + hasStructured := (l.jsonWriter != nil && level >= l.cfg.StructuredLevel) || + l.writers.mw != nil + if !hasStructured { return } - l.logDetailed(LevelDebug, msg, fields...) + + entry := GetEntry() + defer entry.Release() + + entry.SetLevel(level) + entry.SetMessage(msg) + entry.SetTime(time.Now()) + entry.forceTreeDisplay = l.forceTreeDisplay + entry.statusKind = kind + + if l.writers.scanSecure.Load() && strings.IndexByte(msg, '<') >= 0 { + entry.maybeSecure = true + } + + if len(fields) > 0 { + entry.WithFields(fields...) + } + + l.captureCaller(entry, 0) + + if l.jsonWriter != nil && level >= l.cfg.StructuredLevel { + if err := l.jsonWriter.WriteStatus(entry); err != nil { //nolint:staticcheck // Silently drop on write errors to prevent logging from blocking + } + } + + entry.Write() + + l.writers.mu.RLock() + if l.writers.mw != nil { + _ = l.writers.mw.Write(entry) + } + l.writers.mu.RUnlock() } -// InfoDetailed logs an info message with fields always displayed in tree format -func (l *Logger) InfoDetailed(msg string, fields ...Field) { +// Group logs a count-headed block with one item per line. +// +// On a TTY console the output is: +// +// 2006-01-02T15:04:05+10:00 [INFO] Registering routes (3) +// ├─ GET /api/v1/users +// ├─ POST /api/v1/users +// └─ GET /api/v1/users/:id +// +// The JSON writer emits a single entry with "count" and "items" fields. +// Item markers are visual-only and are stripped from JSON output. +// All standard log-call semantics apply: level filtering, sampling, base fields. +func (l *Logger) Group(level Level, msg string, items ...GroupItem) { if l == nil { - fmt.Fprintf(os.Stderr, "[INFO] %s\n", msg) + fmt.Fprintf(os.Stderr, "[%s] %s (%d)\n", level.ConciseLabel(), msg, len(items)) return } - if !l.isEnabled(LevelInfo) { + if l.closed.Load() || !l.isEnabled(level) { return } - l.logDetailed(LevelInfo, msg, fields...) + l.logGroup(level, msg, items) } -// WarnDetailed logs a warning message with fields always displayed in tree format -func (l *Logger) WarnDetailed(msg string, fields ...Field) { +// logGroup is the internal implementation of Group. +func (l *Logger) logGroup(level Level, msg string, items []GroupItem) { if l == nil { - fmt.Fprintf(os.Stderr, "[WARN] %s\n", msg) return } - if !l.isEnabled(LevelWarn) { + + if l.sampler != nil && !l.sampler.Sample(level, msg) { return } - l.logDetailed(LevelWarn, msg, fields...) + + entry := GetEntry() + defer entry.Release() + + // The composite "msg (N)" string is set as the entry message so the standard + // template path renders the count on non-TTY paths without special-casing. + entry.SetLevel(level) + entry.SetMessage(groupMsgWithCount(msg, len(items))) + entry.SetTime(time.Now()) + entry.forceTreeDisplay = l.forceTreeDisplay + + if l.writers.scanSecure.Load() && strings.IndexByte(msg, '<') >= 0 { + entry.maybeSecure = true + } + + if len(l.baseFields) > 0 { + entry.WithFields(l.baseFields...) + } + + l.captureCaller(entry, 0) + + if l.cfg != nil { + // Console and JSON writers receive items directly — their dedicated Group + // methods handle rendering without adding a FieldTypeGroupItems field to + // the entry, which would cause the standard template to emit "[N items]". + if level >= l.cfg.ConsoleLevel && l.consoleWriter != nil { + if err := l.consoleWriter.WriteGroup(entry, items); err != nil { //nolint:staticcheck // Silently drop on write errors to prevent logging from blocking + } + } + + if level >= l.cfg.StructuredLevel && l.jsonWriter != nil { + if err := l.jsonWriter.WriteGroup(entry, items); err != nil { //nolint:staticcheck // Silently drop on write errors to prevent logging from blocking + } + } + + entry.Write() + + l.writers.mu.RLock() + if l.writers.mw != nil { + // Additional writers get the typed field so they can optionally + // render the items. Writers that don't understand FieldTypeGroupItems + // emit "[N items]" as a fallback hint (see writeFormatted). + entry.WithFields(groupItemsField(items)) + _ = l.writers.mw.Write(entry) + } + l.writers.mu.RUnlock() + return + } + + entry.Write() } -// ErrorDetailed logs an error message with fields always displayed in tree format -func (l *Logger) ErrorDetailed(msg string, fields ...Field) { +func (l *Logger) Fatal(msg string, fields ...Field) { if l == nil { - fmt.Fprintf(os.Stderr, "[ERR!] %s\n", msg) - return + fmt.Fprintf(os.Stderr, "[FATL] %s\n", msg) + os.Exit(1) } - if !l.isEnabled(LevelError) { + l.log(LevelFatal, msg, fields...) + if l.cfg != nil && l.cfg.FatalHandler != nil { + l.cfg.FatalHandler() return } - l.logDetailed(LevelError, msg, fields...) + os.Exit(1) } func (l *Logger) isEnabled(level Level) bool { @@ -307,7 +655,9 @@ func (l *Logger) isEnabled(level Level) bool { } // captureCaller populates entry with caller information if configured. -// extraSkip allows callers to account for additional frames in the call stack. +// extraSkip lets callers that add extra frames (e.g. wrappers) adjust the skip depth. +// +//nolint:unparam // extraSkip is always 0 today but reserved for future use by non-direct call paths func (l *Logger) captureCaller(entry *Entry, extraSkip int) { if l.cfg == nil || !l.cfg.AddCaller { return @@ -322,8 +672,6 @@ func (l *Logger) captureCaller(entry *Entry, extraSkip int) { return } - // Extract just the filename from full path - // Use bit shift to find last separator for performance shortFile := file for i := len(file) - 1; i >= 0; i-- { if file[i] == '/' || file[i] == '\\' { @@ -335,19 +683,13 @@ func (l *Logger) captureCaller(entry *Entry, extraSkip int) { entry.Caller = shortFile entry.Line = line - // Get function name if available if fn := runtime.FuncForPC(pc); fn != nil { entry.Function = fn.Name() } } func (l *Logger) log(level Level, msg string, fields ...Field) { - l.logInternal(level, msg, false, fields...) -} - -// logDetailed logs a message with fields forced to display in tree format. -func (l *Logger) logDetailed(level Level, msg string, fields ...Field) { - l.logInternal(level, msg, true, fields...) + l.logInternal(level, msg, l.forceTreeDisplay, fields...) } // LogEntry dispatches a pre-populated entry to all configured writers. @@ -357,13 +699,17 @@ func (l *Logger) LogEntry(e *Entry) { return } // Prepend base fields from With() so child loggers propagate their fields. - // Reuse the existing slice when baseFields fit to avoid a fresh allocation. if len(l.baseFields) > 0 { existing := e.Fields e.Fields = e.Fields[:0] e.WithFields(l.baseFields...) e.WithFields(existing...) } + // Apply the same tag scan as logInternal so entries routed through + // external adapters (e.g. slogbridge) benefit from message-level redaction. + if l.writers.scanSecure.Load() && strings.IndexByte(e.Message, '<') >= 0 { + e.maybeSecure = true + } if l.cfg != nil { if e.Level >= l.cfg.ConsoleLevel && l.consoleWriter != nil { _ = l.consoleWriter.Write(e) @@ -372,29 +718,26 @@ func (l *Logger) LogEntry(e *Entry) { _ = l.jsonWriter.Write(e) } e.Write() - l.writersMu.RLock() - if l.additionalWriters != nil { - _ = l.additionalWriters.Write(e) + l.writers.mu.RLock() + if l.writers.mw != nil { + _ = l.writers.mw.Write(e) } - l.writersMu.RUnlock() + l.writers.mu.RUnlock() return } e.Write() } // logInternal is the shared implementation for log and logDetailed. -// forceTree controls whether the entry's forceTreeDisplay flag is set. func (l *Logger) logInternal(level Level, msg string, forceTree bool, fields ...Field) { if l == nil { return } - // Early sampling check to avoid allocation when entry will be dropped if l.sampler != nil && !l.sampler.Sample(level, msg) { return } - // Pool reduces GC pressure in high-throughput scenarios by reusing Entry objects entry := GetEntry() defer entry.Release() @@ -402,6 +745,16 @@ func (l *Logger) logInternal(level Level, msg string, forceTree bool, fields ... entry.SetMessage(msg) entry.SetTime(time.Now()) entry.forceTreeDisplay = forceTree + + // When any output path is untrusted, check whether the message contains a + // tag. strings.IndexByte is SIMD-accelerated in the Go runtime (~3-5ns), + // zero-alloc on string input. The flag is read without a lock — worst case a + // concurrent AddWriter races and we miss one log line; acceptable for a + // best-effort security feature. + if l.writers.scanSecure.Load() && strings.IndexByte(msg, '<') >= 0 { + entry.maybeSecure = true + } + if len(l.baseFields) > 0 { entry.WithFields(l.baseFields...) } @@ -409,11 +762,9 @@ func (l *Logger) logInternal(level Level, msg string, forceTree bool, fields ... entry.WithFields(fields...) } - // Capture caller information if enabled (no extra skip needed beyond the 4 already counted) l.captureCaller(entry, 0) if l.cfg != nil { - // Synchronous writers (console and JSON) if level >= l.cfg.ConsoleLevel && l.consoleWriter != nil { if err := l.consoleWriter.Write(entry); err != nil { //nolint:staticcheck // Silently drop on write errors to prevent logging from blocking } @@ -424,19 +775,17 @@ func (l *Logger) logInternal(level Level, msg string, forceTree bool, fields ... } } - // Additional writers (async via MultiWriter - handles Retain internally) - // NOTE: Must call entry.Write() BEFORE async writes to avoid race on written field - entry.Write() // Marks entry as written (required before Release can return to pool) + entry.Write() - l.writersMu.RLock() - if l.additionalWriters != nil { - _ = l.additionalWriters.Write(entry) // Non-blocking async write + l.writers.mu.RLock() + if l.writers.mw != nil { + _ = l.writers.mw.Write(entry) } - l.writersMu.RUnlock() + l.writers.mu.RUnlock() return } - entry.Write() // Marks entry as written (required before Release can return to pool) + entry.Write() } // Theme returns the console theme configured for this logger. @@ -449,145 +798,151 @@ func (l *Logger) Theme() *Theme { } // SetTheme updates the active theme on all writers that support it. -// Updates cfg.ConsoleTheme so subsequent With() clones and WithTemplate calls inherit the new theme. -// Nil theme is treated as explicit colour-disable; writers receive nil and handle it themselves. -// User-defined themes are cached automatically: if the theme's ANSI sequences are not yet populated -// a clone is cached and used, so the caller's original pointer is not mutated. Nil-safe. +// Updates cfg.ConsoleTheme so subsequent With() clones inherit the new theme. +// A nil theme resets to the default (ThemeNightOwl); it does not disable colour. +// To disable colour use WithColour(false) or the NO_COLOR environment variable. +// User-defined themes are cached automatically: if the theme's ANSI sequences are +// not yet populated they are computed in-place. Nil-safe. func (l *Logger) SetTheme(theme *Theme) { if l == nil { return } - // Ensure ANSI sequences are populated without mutating the caller's pointer. - theme = ensureCached(theme) + // Nil means "reset to default". Normalise here so cfg and all writers agree. + if theme == nil { + theme = ThemeNightOwl + } if l.cfg != nil { l.cfg.ConsoleTheme = theme } - type themeSetter interface{ SetTheme(*Theme) } - - if s, ok := any(l.consoleWriter).(themeSetter); ok && l.consoleWriter != nil { + if s, ok := any(l.consoleWriter).(ThemedWriter); ok && l.consoleWriter != nil { s.SetTheme(theme) } - l.writersMu.RLock() - defer l.writersMu.RUnlock() + l.writers.mu.RLock() + defer l.writers.mu.RUnlock() - if l.additionalWriters == nil { + if l.writers.mw == nil { return } - l.additionalWriters.mu.Lock() - defer l.additionalWriters.mu.Unlock() + l.writers.mw.mu.Lock() + defer l.writers.mw.mu.Unlock() - for _, w := range l.additionalWriters.writers { - if s, ok := w.(themeSetter); ok { + for _, ws := range l.writers.mw.workers { + if s, ok := ws.w.(ThemedWriter); ok { s.SetTheme(theme) } } } -// Status returns the StatusFormatter for coloured status indicators. -// Safe to call even if logger is nil - returns a non-coloured formatter. -func (l *Logger) Status() *StatusFormatter { - if l == nil || l.statusFormatter == nil { - return NewStatusFormatter(nil, false) +// Style returns the active theme for use in manual ANSI formatting. +// Follows the same fallback logic as Theme(): nil cfg.ConsoleTheme falls back +// to ThemeNightOwl (matching the console writer), not to the no-colour sentinel. +// noColourTheme is only returned when colour is explicitly disabled, or when the +// logger has no console writer at all (JSON-only, nop, or production preset). +func (l *Logger) Style() *Theme { + if l == nil { + return noColourTheme + } + // Colour explicitly disabled — return a mono theme regardless of writer. + if l.cfg != nil && l.cfg.DisableColour { + return noColourTheme + } + // No console output configured — there is no styled channel to match. + if l.consoleWriter == nil { + return noColourTheme } - return l.statusFormatter + // Colour resolved to off for this writer (NO_COLOR, piped, non-TTY) — return + // mono so callers using Style().Format() don't emit ANSI into pipes or files. + if !l.consoleWriter.isTTY { + return noColourTheme + } + // Console writer is active and colour-capable: return the themed palette. + return l.Theme() } -// Raw prints a message without any formatting, timestamp, or level. -// The caller is responsible for including newlines if desired. -func (l *Logger) Raw(message string) { +// BannerLines prints multiple lines of pre-formatted text to the console writer +// without log timestamps, levels, or field formatting. +// Named BannerLines to avoid collision with the Banner Renderable type. +// Nil-safe. +func (l *Logger) BannerLines(lines ...string) { if l == nil { - _, _ = fmt.Fprint(os.Stdout, message) + for _, line := range lines { + _, _ = fmt.Fprintln(os.Stdout, line) + } return } + var out io.Writer switch { case l.consoleWriter != nil && l.consoleWriter.out != nil: l.consoleWriter.mu.Lock() - _, _ = io.WriteString(l.consoleWriter.out, message) - l.consoleWriter.mu.Unlock() + defer l.consoleWriter.mu.Unlock() + out = l.consoleWriter.out case l.cfg != nil && l.cfg.ConsoleOutput != nil: - _, _ = io.WriteString(l.cfg.ConsoleOutput, message) + out = l.cfg.ConsoleOutput default: - _, _ = fmt.Fprint(os.Stdout, message) - } -} - -// Banner prints multiple lines of text without formatting. -// Newlines are automatically added after each line. -func (l *Logger) Banner(lines ...string) { - if l == nil { - for _, line := range lines { - _, _ = fmt.Fprintln(os.Stdout, line) - } - return + out = os.Stdout } for _, line := range lines { - l.Raw(line + "\n") + _, _ = fmt.Fprintln(out, line) } } -func (l *Logger) SetTemplate(t *Template) { - if l == nil { - return - } - - if l.consoleWriter != nil { - l.consoleWriter.SetTemplate(t) - } -} - -// WithTemplate creates a child logger with a different output template. -// The consoleWriter is intentionally recreated so the new template takes effect. -func (l *Logger) WithTemplate(t *Template) *Logger { +// Detailed returns a child logger that forces every log call to render fields +// in tree format, regardless of the logger's FieldDisplayMode setting. +// The child shares writers, config, sampler, and pool with the parent. +// One alloc at the call site; zero extra cost per log call after that. +func (l *Logger) Detailed() *Logger { if l == nil { return nil } - - newLogger := &Logger{ - cfg: l.cfg, - bufPool: l.bufPool, - jsonWriter: l.jsonWriter, - statusFormatter: l.statusFormatter, - sampler: l.sampler, - additionalWriters: l.additionalWriters, + child := &Logger{ + cfg: l.cfg, + bufPool: l.bufPool, + consoleWriter: l.consoleWriter, + jsonWriter: l.jsonWriter, + sampler: l.sampler, + writers: l.writers, // shared pointer — parent topology changes propagate + forceTreeDisplay: true, } - newLogger.level.Store(l.level.Load()) - + child.level.Store(l.level.Load()) + child.secureScanEnabled.Store(l.secureScanEnabled.Load()) + // scanSecure lives on the shared writers — no copy needed. if len(l.baseFields) > 0 { newBase := make([]Field, len(l.baseFields)) copy(newBase, l.baseFields) - newLogger.baseFields = newBase - } - - if l.cfg.ConsoleOutput != nil && l.cfg.ConsoleOutput != io.Discard { - newLogger.consoleWriter = NewConsoleWriterWithOptions(l.cfg.ConsoleOutput, l.cfg.ConsoleTheme, l.cfg.DisplayTimezone, l.cfg.FieldDisplayMode) - if t != nil { - newLogger.consoleWriter.SetTemplate(t) - } + child.baseFields = newBase } + return child +} - return newLogger +// WithComponent returns a child logger that stamps every entry with a +// "component" string field. Sugar for l.With(String("component", name)). +func (l *Logger) WithComponent(name string) *Logger { + return l.With(String("component", name)) } -func NopLogger() *Logger { - cfg := DefaultConfig() - cfg.ConsoleOutput = io.Discard - cfg.StructuredOutput = io.Discard - cfg.ConsoleLevel = LevelOff - cfg.StructuredLevel = LevelOff - return NewWithConfig(cfg) +// WithRequest returns a child logger that stamps every entry with a +// "request_id" string field. Sugar for l.With(String("request_id", id)). +func (l *Logger) WithRequest(id string) *Logger { + return l.With(String("request_id", id)) } // Render writes r to the console writer, indented to align with the message column. // Each line after the first is prefixed with spaces equal to the template prefix width // so the output sits flush with log messages in tree mode. // +// When r implements TTYRenderable, RenderTTY is called with the console writer's +// resolved TTY state (which accounts for FORCE_COLOR / NO_COLOR and fd detection), +// so colour decisions match the rest of the log line. Types must implement TTYRenderable +// if they use IsTerminalWriter internally — calling it on the intermediate buffer +// passed by Render always yields false regardless of the actual output destination. +// // JSON writers and MultiWriter silently ignore Render calls — indented rich output // is only meaningful on a terminal-backed console writer. // @@ -598,13 +953,19 @@ func (l *Logger) Render(r Renderable) { } indent := l.consoleWriter.template.CachedMessageIndentStr() + isTTY := l.consoleWriter.isTTY tmp := GetTemplateBuffer() defer PutTemplateBuffer(tmp) - // Render into the temporary buffer, then indent and write under the lock. - if err := r.Render(tmp); err != nil { - return + if tr, ok := r.(TTYRenderable); ok { + if err := tr.RenderTTY(tmp, isTTY); err != nil { + return + } + } else { + if err := r.Render(tmp); err != nil { + return + } } out := indentLines(tmp.Bytes(), indent) @@ -616,17 +977,26 @@ func (l *Logger) Render(r Renderable) { // RenderRaw writes r flush-left to the console writer, with no indentation. // Like Render, it is terminal-only and ignored by JSON/multi writers. -// Nil-safe. +// When r implements TTYRenderable, the console writer's TTY state is passed +// rather than detecting it from the intermediate buffer. Nil-safe. func (l *Logger) RenderRaw(r Renderable) { if l == nil || r == nil || l.consoleWriter == nil { return } + isTTY := l.consoleWriter.isTTY + tmp := GetTemplateBuffer() defer PutTemplateBuffer(tmp) - if err := r.Render(tmp); err != nil { - return + if tr, ok := r.(TTYRenderable); ok { + if err := tr.RenderTTY(tmp, isTTY); err != nil { + return + } + } else { + if err := r.Render(tmp); err != nil { + return + } } l.consoleWriter.mu.Lock() @@ -647,16 +1017,192 @@ func (l *Logger) Newline() { l.consoleWriter.mu.Unlock() } -// indentLines prefixes every non-empty line in b with indent. Render writes a -// self-contained block at the message column, so the first line must also be -// indented; otherwise multi-line output like table top borders lands flush-left -// while subsequent lines align to the indent column. +// notifyMu is the fallback mutex for Notify calls on loggers that have no console +// writer. It prevents interleaving across loggers that share os.Stderr as their +// notify destination but have no common mutex. +var notifyMu sync.Mutex + +// notifyDest returns the writer and mutex to use for Notify output. +// When a console writer is present it shares that writer's mutex so Notify and +// log lines on a shared terminal cannot interleave. Otherwise the package-level +// fallback is used with os.Stderr (or the configured override). +func (l *Logger) notifyDest() (io.Writer, *sync.Mutex) { + if l.consoleWriter != nil { + // Share the console writer's mutex regardless of the notify output + // destination — this is the primary non-interleave guarantee. + out := l.cfg.NotifyOutput + if out == nil { + out = os.Stderr + } + return out, &l.consoleWriter.mu + } + out := l.cfg.NotifyOutput + if out == nil { + out = os.Stderr + } + return out, ¬ifyMu +} + +// Notify writes a formatted message directly to the notify destination (default +// os.Stderr), bypassing all writers, the level filter, the sampler, and the +// structured pipeline. Intended for ephemeral operator-visible output such as +// setup URLs and one-time bootstrap messages that must appear regardless of log +// level or writer configuration. +// +// Uses the console writer mutex when present to prevent interleaving with +// concurrent log output on shared terminals. Nil-safe. +// +//nolint:goprintffuncname // Notify is an intentional API name, not a generic printf wrapper. +func (l *Logger) Notify(format string, args ...any) { + if l == nil || l.closed.Load() { + return + } + out, mu := l.notifyDest() + msg := fmt.Sprintf(format, args...) + mu.Lock() + _, _ = io.WriteString(out, msg) + mu.Unlock() +} + +// NotifyLines writes each line to the notify destination separated by newlines. +// Behaves identically to Notify with respect to writer bypass and mutex sharing. +// Nil-safe. +func (l *Logger) NotifyLines(lines ...string) { + if l == nil || l.closed.Load() || len(lines) == 0 { + return + } + out, mu := l.notifyDest() + mu.Lock() + for _, line := range lines { + _, _ = io.WriteString(out, line) + _, _ = io.WriteString(out, "\n") + } + mu.Unlock() +} + +// NotifyBox renders a Box to the notify destination. Useful for visually-prominent +// operator messages — the canonical use case is an onboarding URL that must stand +// out regardless of whether structured logging is active. +// Nil-safe; a nil Box is a no-op. +func (l *Logger) NotifyBox(b *Box) { + if l == nil || l.closed.Load() || b == nil { + return + } + out, mu := l.notifyDest() + tmp := GetTemplateBuffer() + if err := b.Render(tmp); err != nil { + PutTemplateBuffer(tmp) + return + } + mu.Lock() + _, _ = out.Write(tmp.Bytes()) + mu.Unlock() + PutTemplateBuffer(tmp) +} + +// Box renders a bordered box with an optional title to the console writer, +// indented to align with the message column. Uses the logger's active theme. +// Nil-safe; no-op when there is no console writer. +func (l *Logger) Box(title, body string) { + if l == nil || l.closed.Load() || l.consoleWriter == nil { + return + } + l.Render(NewBox(title, body, l.Style())) +} + +// Table renders an aligned table with auto-sized columns to the console writer, +// indented to align with the message column. Uses the logger's active theme. +// Nil-safe; no-op when there is no console writer. +func (l *Logger) Table(headers []string, rows [][]string) { + if l == nil || l.closed.Load() || l.consoleWriter == nil { + return + } + l.Render(NewTable(headers, rows, l.Style())) +} + +// Tree renders a hierarchical tree of TreeItem nodes to the console writer, +// indented to align with the message column. Uses the logger's active theme. +// Nil-safe; no-op when there is no console writer. +func (l *Logger) Tree(items []TreeItem) { + if l == nil || l.closed.Load() || l.consoleWriter == nil { + return + } + l.Render(NewTree(items, l.Style())) +} + +// KeyValues renders a sequence of key-value pairs to the console writer, +// indented to align with the message column. Uses the logger's active theme. +// Nil-safe; no-op when there is no console writer or pairs is empty. +func (l *Logger) KeyValues(pairs []KeyValuePair) { + if l == nil || l.closed.Load() || l.consoleWriter == nil || len(pairs) == 0 { + return + } + // Render each pair under the same indent; they read as a continuation block. + theme := l.Style() + indent := l.consoleWriter.template.CachedMessageIndentStr() + tmp := GetTemplateBuffer() + defer PutTemplateBuffer(tmp) + for _, p := range pairs { + kv := NewKeyValue(p.Key, p.Value, theme) + if err := kv.Render(tmp); err != nil { + return + } + } + out := indentLines(tmp.Bytes(), indent) + l.consoleWriter.mu.Lock() + _, _ = l.consoleWriter.out.Write(out) + l.consoleWriter.mu.Unlock() +} + +// SystemInfo renders a titled block of key-value system metadata to the console +// writer, indented to align with the message column. Uses the logger's active theme. +// Nil-safe; no-op when there is no console writer or info is nil. +func (l *Logger) SystemInfo(info *SystemInfoData) { + if l == nil || l.closed.Load() || l.consoleWriter == nil || info == nil { + return + } + l.Render(NewSystemInfo(info, l.Style())) +} + +// Bullet renders an indented bullet point at the given nesting level to the +// console writer, aligned with the message column. Uses the logger's active theme. +// Bullets cycle through •, ◦, ▪, ▫ with depth. Nil-safe; no-op without a console writer. +func (l *Logger) Bullet(level int, text string) { + if l == nil || l.closed.Load() || l.consoleWriter == nil { + return + } + theme := l.Style() + indent := strings.Repeat(" ", level) + bullets := []string{"•", "◦", "▪", "▫"} + bullet := bullets[level%len(bullets)] + + tmp := GetTemplateBuffer() + defer PutTemplateBuffer(tmp) + + tmp.WriteString(indent) + tmp.WriteString(theme.CachedFieldKeyFg()) + tmp.WriteString(bullet) + tmp.WriteString(Reset) + tmp.WriteString(" ") + tmp.WriteString(theme.CachedMessageFg()) + tmp.WriteString(text) + tmp.WriteString(Reset) + tmp.WriteString("\n") + + msgIndent := l.consoleWriter.template.CachedMessageIndentStr() + out := indentLines(tmp.Bytes(), msgIndent) + + l.consoleWriter.mu.Lock() + _, _ = l.consoleWriter.out.Write(out) + l.consoleWriter.mu.Unlock() +} + +// indentLines prefixes every non-empty line in b with indent. func indentLines(b []byte, indent string) []byte { if len(b) == 0 || indent == "" { return b } - // Count newlines to size the output buffer without reallocation. nlCount := 0 for _, c := range b { if c == '\n' { @@ -672,14 +1218,12 @@ func indentLines(b []byte, indent string) []byte { if c == '\n' { out = append(out, b[start:i+1]...) start = i + 1 - // Prefix the next line only if it has content. if start < len(b) { out = append(out, indent...) } } } - // Append any trailing content without a newline. if start < len(b) { out = append(out, b[start:]...) } diff --git a/logger_addwriter_test.go b/logger_addwriter_test.go index fc8fb02..7d0a886 100644 --- a/logger_addwriter_test.go +++ b/logger_addwriter_test.go @@ -11,7 +11,7 @@ import ( // TestLogger_AddWriter verifies that a writer receives log entries. func TestLogger_AddWriter(t *testing.T) { var buf bytes.Buffer - logger := New(&buf) + logger := New(WithConsoleOutput(&buf)) var callCount atomic.Int64 @@ -34,7 +34,7 @@ func TestLogger_AddWriter(t *testing.T) { // TestLogger_AddWriter_Concurrent verifies thread-safety of AddWriter. func TestLogger_AddWriter_Concurrent(t *testing.T) { var buf bytes.Buffer - logger := New(&buf) + logger := New(WithConsoleOutput(&buf)) var wg sync.WaitGroup @@ -65,7 +65,7 @@ func TestLogger_AddWriter_Concurrent(t *testing.T) { // TestLogger_RemoveWriter verifies that a removed writer no longer receives entries. func TestLogger_RemoveWriter(t *testing.T) { var buf bytes.Buffer - logger := New(&buf) + logger := New(WithConsoleOutput(&buf)) var count1 atomic.Int64 var count2 atomic.Int64 @@ -125,7 +125,7 @@ func TestLogger_AddWriter_NilSafety(t *testing.T) { nilLogger.AddWriter("test", &NoOpWriter{}) nilLogger.RemoveWriter("test") - logger := NewForTesting(nil) + logger := newForTesting(nil) logger.AddWriter("noop", &NoOpWriter{}) logger.Info("test message") @@ -140,7 +140,7 @@ func TestLogger_AddWriter_NilSafety(t *testing.T) { // TestLogger_AddWriter_MultipleWriters verifies multiple writers receive entries. func TestLogger_AddWriter_MultipleWriters(t *testing.T) { var buf bytes.Buffer - logger := New(&buf) + logger := New(WithConsoleOutput(&buf)) var count1 atomic.Int64 var count2 atomic.Int64 @@ -195,18 +195,11 @@ func TestLogger_AddWriter_MultipleWriters(t *testing.T) { // TestLogger_AddWriter_LevelFiltering verifies writers respect level filtering. func TestLogger_AddWriter_LevelFiltering(t *testing.T) { - builder := NewConfig() - builder.WithLevel(LevelInfo) - var buf bytes.Buffer - builder.WithOutput(&buf) - - cfg, err := builder.Build() - if err != nil { - t.Fatalf("Build() failed: %v", err) - } - - logger := NewWithConfig(cfg) + logger := New( + WithConsoleOutput(&buf), + WithLevel(LevelInfo), + ) var debugCount atomic.Int64 var infoCount atomic.Int64 @@ -247,7 +240,7 @@ func TestLogger_AddWriter_LevelFiltering(t *testing.T) { // TestLogger_AddWriter_EntryIntegrity verifies entry fields are preserved. func TestLogger_AddWriter_EntryIntegrity(t *testing.T) { var buf bytes.Buffer - logger := New(&buf) + logger := New(WithConsoleOutput(&buf)) // Use a channel to receive the entry data safely type entryData struct { @@ -298,3 +291,33 @@ func TestLogger_NilSetLevel(_ *testing.T) { l.SetLevel(LevelDebug) // must not panic _ = l.Level() // must not panic } + +// TestLogger_ChildSeesWriterAddedAfterCreation is a regression test for the bug where +// child loggers created via With() did not see writers added to the parent after the +// child was created. The shared writerSet pointer must propagate the new writer to +// all members of the logger family. +func TestLogger_ChildSeesWriterAddedAfterCreation(t *testing.T) { + t.Parallel() + + parent := New(WithConsoleOutput(&bytes.Buffer{})) + + // Create a child before adding any writer. + child := parent.With(String("child", "true")) + + var count atomic.Int64 + fn := WriterFunc(func(_ *Entry) error { + count.Add(1) + return nil + }) + + // Add the writer to the parent AFTER the child was created. + parent.AddWriter("tracker", &fn) + + // Log through the child — the shared writerSet means the tracker should fire. + child.Info("from child") + + waitFor(t, func() bool { + return count.Load() >= 1 + }, 300*time.Millisecond, 10*time.Millisecond, + "child should route through writer added to parent after child creation") +} diff --git a/logger_close_test.go b/logger_close_test.go new file mode 100644 index 0000000..8dd7051 --- /dev/null +++ b/logger_close_test.go @@ -0,0 +1,182 @@ +package velocity + +import ( + "bytes" + "strings" + "testing" +) + +func TestClose_Idempotent(t *testing.T) { + t.Parallel() + + log := New(WithConsoleOutput(bytes.NewBuffer(nil))) + + if err := log.Close(); err != nil { + t.Fatalf("first Close() error: %v", err) + } + // Second call must not panic or return an error. + if err := log.Close(); err != nil { + t.Fatalf("second Close() error: %v", err) + } +} + +func TestClose_PostCloseDropsSilently(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + log := New(WithConsoleOutput(&buf)) + + _ = log.Close() + before := buf.Len() + + // Log calls after Close must not write anything. + log.Info("should be dropped") + log.Warn("also dropped") + + if buf.Len() != before { + t.Errorf("expected no output after Close, got %d extra bytes", buf.Len()-before) + } +} + +func TestClose_NilLogger(t *testing.T) { + t.Parallel() + + var l *Logger + if err := l.Close(); err != nil { + t.Errorf("nil logger Close() should return nil, got: %v", err) + } +} + +func TestClose_WithAdditionalWriter(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + log := New(WithConsoleOutput(&buf)) + log.AddWriter("test", WriterFunc(func(_ *Entry) error { return nil })) + + log.Info("before close") + + // Close must not block indefinitely; MultiWriter drains its channel. + if err := log.Close(); err != nil { + t.Fatalf("Close() with additional writer: %v", err) + } +} + +func TestStyle_ReturnsTheme(t *testing.T) { + // Cannot run in parallel because t.Setenv modifies a process-wide env var. + // Style() returns mono for non-TTY writers; use FORCE_COLOR to test the + // colour path without requiring a real terminal in CI. + t.Setenv("FORCE_COLOR", "1") + + log := New( + WithConsoleOutput(bytes.NewBuffer(nil)), + WithTheme(ThemeNightOwl), + ) + + theme := log.Style() + if theme == nil { + t.Fatal("Style() returned nil") + } + if theme == noColourTheme { + t.Error("expected themed logger to return its theme under FORCE_COLOR=1, not noColourTheme") + } +} + +func TestStyle_NoConsoleWriter_ReturnsNoColour(t *testing.T) { + t.Parallel() + + // WithNop produces a logger with no console writer. + log := New(WithNop()) + theme := log.Style() + if theme == nil { + t.Fatal("Style() returned nil even for nop logger") + } + // The no-colour theme has an empty Name or all-zero ANSI codes. + // We only verify it's non-nil and doesn't panic when its methods are called. + _ = theme.CachedMessageFg() +} + +func TestStyle_NilLogger(t *testing.T) { + t.Parallel() + + var l *Logger + theme := l.Style() + if theme == nil { + t.Fatal("nil logger Style() returned nil") + } +} + +func TestNopLogger_NonNil(t *testing.T) { + t.Parallel() + + l := NopLogger() + if l == nil { + t.Fatal("NopLogger() returned nil") + } +} + +func TestNopLogger_NoOutput(t *testing.T) { + t.Parallel() + + // NopLogger must accept log calls without panicking and write nothing to stderr. + // We verify by exercising all severity levels — none should panic. + l := NopLogger() + l.Debug("debug") + l.Info("info") + l.Warn("warn") + l.Error("error") +} + +func TestNopLogger_CloseSafe(t *testing.T) { + t.Parallel() + + l := NopLogger() + if err := l.Close(); err != nil { + t.Fatalf("NopLogger Close() returned error: %v", err) + } +} + +// TestWithProduction_ProducesOutput is a regression test for the bug where +// WithProduction set StructuredOutput to nil, producing no output at all. +// The preset must route JSON entries to stderr (captured here via a buffer). +func TestWithProduction_ProducesOutput(t *testing.T) { + t.Parallel() + + var buf safeBuffer + + log := New( + WithProduction(), + // Redirect the structured output to a buffer so we can inspect it. + WithStructuredOutput(&buf), + ) + defer func() { _ = log.Close() }() + + log.Info("hello") + + out := buf.String() + if out == "" { + t.Fatal("WithProduction() produced no output — StructuredOutput was likely nil") + } + if !strings.Contains(out, "hello") { + t.Errorf("expected 'hello' in JSON output, got: %q", out) + } +} + +// TestSetTheme_NilResetsToDefault verifies that SetTheme(nil) resets to NightOwl +// rather than silently disabling colour. The documented way to disable colour is +// WithColour(false), not passing nil to SetTheme. +func TestSetTheme_NilResetsToDefault(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + log := New(WithConsoleOutput(&buf), WithTheme(ThemeSolarized)) + + log.SetTheme(nil) + + if log.Theme() != ThemeNightOwl { + t.Errorf("SetTheme(nil): Theme() returned %v, want ThemeNightOwl", log.Theme()) + } + if log.cfg.ConsoleTheme != ThemeNightOwl { + t.Errorf("SetTheme(nil): cfg.ConsoleTheme = %v, want ThemeNightOwl", log.cfg.ConsoleTheme) + } +} diff --git a/logger_detailed_test.go b/logger_detailed_test.go index 5d6f403..b677d00 100644 --- a/logger_detailed_test.go +++ b/logger_detailed_test.go @@ -2,227 +2,196 @@ package velocity import ( "bytes" + "strings" "sync" "testing" ) -func TestDetailedLogging(t *testing.T) { +func TestDetailed_ForcesTreeDisplay(t *testing.T) { + t.Parallel() + tests := []struct { - name string - setupLogger func() *Logger - logFunc func(*Logger) - wantTree bool + name string + logFunc func(l *Logger) }{ { - name: "InfoDetailed always uses tree display even with inline config", - setupLogger: func() *Logger { - cfg := DefaultConfig() - cfg.FieldDisplayMode = FieldDisplayInline - return NewWithConfig(cfg) - }, + name: "Info on Detailed() child uses tree display even with inline config", logFunc: func(l *Logger) { - l.InfoDetailed("Test message", + l.Detailed().Info("Test message", String("key1", "value1"), Int("key2", 42), Bool("key3", true)) }, - wantTree: true, }, { - name: "ErrorDetailed always uses tree display", - setupLogger: func() *Logger { - cfg := DefaultConfig() - cfg.FieldDisplayMode = FieldDisplayInline - return NewWithConfig(cfg) - }, + name: "Error on Detailed() child always uses tree display", logFunc: func(l *Logger) { - l.ErrorDetailed("Error occurred", + l.Detailed().Error("Error occurred", String("error", "connection timeout"), Int("retry", 3)) }, - wantTree: true, }, { - name: "WarnDetailed always uses tree display", - setupLogger: func() *Logger { - cfg := DefaultConfig() - cfg.FieldDisplayMode = FieldDisplayInline - return NewWithConfig(cfg) - }, + name: "Warn on Detailed() child always uses tree display", logFunc: func(l *Logger) { - l.WarnDetailed("Warning message", + l.Detailed().Warn("Warning message", String("warning", "high memory usage"), Float64("usage_percent", 89.5)) }, - wantTree: true, - }, - { - name: "DebugDetailed always uses tree display", - setupLogger: func() *Logger { - cfg := DefaultConfig() - cfg.FieldDisplayMode = FieldDisplayInline - cfg.ConsoleLevel = LevelDebug - return NewWithConfig(cfg) - }, - logFunc: func(l *Logger) { - l.DebugDetailed("Debug info", - String("module", "auth"), - String("action", "token_refresh")) - }, - wantTree: true, - }, - { - name: "Regular Info uses inline when configured", - setupLogger: func() *Logger { - cfg := DefaultConfig() - cfg.FieldDisplayMode = FieldDisplayInline - return NewWithConfig(cfg) - }, - logFunc: func(l *Logger) { - l.Info("Regular message", - String("key", "value"), - Int("number", 123)) - }, - wantTree: false, }, } for _, tt := range tests { - t.Run(tt.name, func(_ *testing.T) { - logger := tt.setupLogger() + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.FieldDisplayMode = FieldDisplayInline + cfg.ConsoleOutput = &buf + l := newFromConfig(cfg) + + tt.logFunc(l) + + // Tree display uses "├" or "└" as branch glyphs. + out := buf.String() + if !strings.ContainsAny(out, "├└") { + t.Errorf("expected tree glyphs in output, got: %s", out) + } + }) + } +} - // The test will use the configured console writer - // We're primarily testing that the forceTreeDisplay flag - // is properly set and passed through the system +func TestDetailed_Debug_ForcesTreeDisplay(t *testing.T) { + t.Parallel() - tt.logFunc(logger) + var buf bytes.Buffer + cfg := defaultConfig() + cfg.FieldDisplayMode = FieldDisplayInline + cfg.ConsoleLevel = LevelDebug + cfg.ConsoleOutput = &buf + l := newFromConfig(cfg) - // For this test, we're mainly verifying that the methods compile - // and execute without panics. Full output testing would require - // more setup to capture console writer output. + l.Detailed().Debug("Debug info", + String("module", "auth"), + String("action", "token_refresh")) - // The key thing we're testing is that the forceTreeDisplay flag - // is properly set and passed through the system. - }) + out := buf.String() + if !strings.ContainsAny(out, "├└") { + t.Errorf("expected tree glyphs in debug output, got: %s", out) + } +} + +func TestDetailed_RegularLoggerStillInline(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.FieldDisplayMode = FieldDisplayInline + cfg.ConsoleOutput = &buf + l := newFromConfig(cfg) + + l.Info("Regular message", + String("key", "value"), + Int("number", 123)) + + out := buf.String() + if strings.ContainsAny(out, "├└") { + t.Errorf("regular logger should not produce tree glyphs in inline mode, got: %s", out) } } -func TestDetailedLoggingThreadSafety(_ *testing.T) { - cfg := DefaultConfig() +func TestDetailed_ThreadSafety(_ *testing.T) { + cfg := defaultConfig() cfg.FieldDisplayMode = FieldDisplayInline - logger := NewWithConfig(cfg) + logger := newFromConfig(cfg) + detailed := logger.Detailed() - // Run concurrent detailed and normal logs - done := make(chan bool) + var wg sync.WaitGroup + wg.Add(3) go func() { + defer wg.Done() for i := range 100 { - logger.InfoDetailed("Detailed log", Int("iteration", i)) + detailed.Info("Detailed log", Int("iteration", i)) } - done <- true }() go func() { + defer wg.Done() for i := range 100 { logger.Info("Normal log", Int("iteration", i)) } - done <- true }() go func() { + defer wg.Done() for i := range 100 { - logger.ErrorDetailed("Detailed error", Int("iteration", i)) + detailed.Error("Detailed error", Int("iteration", i)) } - done <- true }() - // Wait for all goroutines - for range 3 { - <-done - } - - // If we get here without panics or races, thread safety is maintained + wg.Wait() } -func TestDetailedMethodsWithNilLogger(_ *testing.T) { - var logger *Logger = nil - - // These should not panic, just print to stderr - logger.InfoDetailed("Test", String("key", "value")) - logger.ErrorDetailed("Test", String("key", "value")) - logger.WarnDetailed("Test", String("key", "value")) - logger.DebugDetailed("Test", String("key", "value")) +func TestDetailed_NilLogger(_ *testing.T) { + var logger *Logger + // Nil Detailed() returns nil — subsequent calls must not panic. + d := logger.Detailed() + if d != nil { + d.Info("should not panic") + } } -func TestDetailedMethodsRespectLogLevel(_ *testing.T) { - cfg := DefaultConfig() - cfg.ConsoleLevel = LevelWarn // Only warn and above - logger := NewWithConfig(cfg) +func TestDetailed_RespectLogLevel(_ *testing.T) { + cfg := defaultConfig() + cfg.ConsoleLevel = LevelWarn + logger := newFromConfig(cfg) + detailed := logger.Detailed() - // These should be filtered out - logger.DebugDetailed("Debug", String("key", "value")) - logger.InfoDetailed("Info", String("key", "value")) + // These are below threshold and should be filtered. + detailed.Debug("Debug", String("key", "value")) + detailed.Info("Info", String("key", "value")) - // These should pass through - logger.WarnDetailed("Warn", String("key", "value")) - logger.ErrorDetailed("Error", String("key", "value")) + // These should pass through without panic. + detailed.Warn("Warn", String("key", "value")) + detailed.Error("Error", String("key", "value")) } -func TestRaw_ConcurrentWithWrite(_ *testing.T) { - buf := &bytes.Buffer{} - log := New(buf) - - var wg sync.WaitGroup - const iters = 200 - - wg.Add(3) - - go func() { - defer wg.Done() - for range iters { - log.Raw("raw line\n") - } - }() +func TestDetailed_InheritsBaseFields(t *testing.T) { + t.Parallel() - go func() { - defer wg.Done() - for range iters { - log.Info("info message") - } - }() + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + parent := newFromConfig(cfg) + parent.baseFields = []Field{String("svc", "payments")} - go func() { - defer wg.Done() - for range iters { - log.Raw("another raw\n") - } - }() + detailed := parent.Detailed() + detailed.Info("checkout") - wg.Wait() + if !strings.Contains(buf.String(), "payments") { + t.Errorf("expected base field to propagate to Detailed child, got: %s", buf.String()) + } } -// TestEntryPoolResetsForceTreeDisplay verifies that the forceTreeDisplay flag -// is properly reset when entries are returned to the pool and reused. -// This prevents the bug where entries from logDetailed() calls would retain -// the tree display flag and affect subsequent regular log calls. func TestEntryPoolResetsForceTreeDisplay(t *testing.T) { + t.Parallel() + // Get an entry from the pool entry1 := GetEntry() - // Verify it starts with forceTreeDisplay = false if entry1.forceTreeDisplay { t.Error("New entry from pool should have forceTreeDisplay = false") } - // Simulate using it in a logDetailed call + // Simulate what logDetailed used to do — flag on the entry directly. entry1.forceTreeDisplay = true entry1.Write() entry1.Release() - // Get another entry from the pool (might be the same one we just released) + // Get another entry (may be the same one returned to pool). entry2 := GetEntry() - - // Verify forceTreeDisplay has been reset to false if entry2.forceTreeDisplay { t.Error("Entry from pool should have forceTreeDisplay reset to false after Reset()") } diff --git a/logger_notify_test.go b/logger_notify_test.go new file mode 100644 index 0000000..c8fc897 --- /dev/null +++ b/logger_notify_test.go @@ -0,0 +1,300 @@ +package velocity + +import ( + "bytes" + "strings" + "sync" + "testing" +) + +func TestLogger_Notify_WritesToConfiguredOutput(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + cfg.StructuredOutput = nil + cfg.NotifyOutput = &buf + + log := newFromConfig(cfg) + log.Notify("hello %s", "operator") + + if !strings.Contains(buf.String(), "hello operator") { + t.Errorf("expected notify message in output, got: %q", buf.String()) + } +} + +func TestLogger_Notify_DefaultsToStderr(t *testing.T) { + t.Parallel() + + // Verify that a logger with no NotifyOutput set does not panic and still + // routes through the fallback path. We can't capture real stderr here, so + // we confirm the notifyDest call returns a non-nil writer. + cfg := defaultConfig() + cfg.ConsoleOutput = &bytes.Buffer{} + cfg.StructuredOutput = nil + // NotifyOutput deliberately unset — should fall back to os.Stderr. + log := newFromConfig(cfg) + + out, mu := log.notifyDest() + if out == nil { + t.Error("expected non-nil notify destination") + } + if mu == nil { + t.Error("expected non-nil mutex from notifyDest") + } +} + +func TestLogger_Notify_BypassesLevelFilter(t *testing.T) { + t.Parallel() + + var notifyBuf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &bytes.Buffer{} + cfg.StructuredOutput = nil + cfg.ConsoleLevel = LevelOff // nothing would normally reach the console writer + cfg.NotifyOutput = ¬ifyBuf + + log := newFromConfig(cfg) + log.Notify("this must appear even at LevelOff") + + if !strings.Contains(notifyBuf.String(), "this must appear") { + t.Errorf("Notify must bypass level filter; got: %q", notifyBuf.String()) + } +} + +func TestLogger_Notify_BypassesWriters(t *testing.T) { + t.Parallel() + + // The JSON writer must not receive Notify output. + var jsonBuf bytes.Buffer + var notifyBuf bytes.Buffer + + cfg := defaultConfig() + cfg.ConsoleOutput = nil + cfg.StructuredOutput = &jsonBuf + cfg.StructuredLevel = LevelDebug + cfg.NotifyOutput = ¬ifyBuf + + log := newFromConfig(cfg) + log.Notify("operator-only message") + + if jsonBuf.Len() != 0 { + t.Errorf("Notify must not write to JSON writer; got: %q", jsonBuf.String()) + } + if !strings.Contains(notifyBuf.String(), "operator-only message") { + t.Errorf("Notify content missing from notify output; got: %q", notifyBuf.String()) + } +} + +func TestLogger_Notify_ClosedLoggerDropsSilently(t *testing.T) { + t.Parallel() + + var notifyBuf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &bytes.Buffer{} + cfg.StructuredOutput = nil + cfg.NotifyOutput = ¬ifyBuf + + log := newFromConfig(cfg) + _ = log.Close() + log.Notify("should be dropped") + + if notifyBuf.Len() != 0 { + t.Errorf("expected no output from closed logger, got: %q", notifyBuf.String()) + } +} + +func TestLogger_Notify_NilLogger(t *testing.T) { + t.Parallel() + + var l *Logger + // Must not panic. + l.Notify("ignored") + l.NotifyLines("ignored") + l.NotifyBox(NewBox("t", "b", ThemeNightOwl)) +} + +func TestLogger_NotifyLines_JoinsWithNewlines(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &bytes.Buffer{} + cfg.StructuredOutput = nil + cfg.NotifyOutput = &buf + + log := newFromConfig(cfg) + log.NotifyLines("line one", "line two", "line three") + + out := buf.String() + for _, want := range []string{"line one\n", "line two\n", "line three\n"} { + if !strings.Contains(out, want) { + t.Errorf("expected %q in notify output; got: %q", want, out) + } + } +} + +func TestLogger_NotifyLines_EmptyIsNoop(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &bytes.Buffer{} + cfg.StructuredOutput = nil + cfg.NotifyOutput = &buf + + log := newFromConfig(cfg) + log.NotifyLines() // no lines — must not panic or write anything + + if buf.Len() != 0 { + t.Errorf("expected no output for empty NotifyLines; got: %q", buf.String()) + } +} + +func TestLogger_NotifyBox_RendersToNotifyOutput(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &bytes.Buffer{} + cfg.StructuredOutput = nil + cfg.NotifyOutput = &buf + + log := newFromConfig(cfg) + log.NotifyBox(NewBox("Setup required", "Open the URL to continue.", ThemeNightOwl)) + + out := buf.String() + if !strings.Contains(out, "Setup required") { + t.Errorf("expected box title in notify output; got: %q", out) + } + if !strings.Contains(out, "Open the URL") { + t.Errorf("expected box content in notify output; got: %q", out) + } +} + +func TestLogger_NotifyBox_NilBoxIsNoop(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &bytes.Buffer{} + cfg.StructuredOutput = nil + cfg.NotifyOutput = &buf + + log := newFromConfig(cfg) + log.NotifyBox(nil) + + if buf.Len() != 0 { + t.Errorf("expected no output for nil box; got: %q", buf.String()) + } +} + +func TestLogger_NotifyBox_BypassesJSONWriter(t *testing.T) { + t.Parallel() + + var jsonBuf, notifyBuf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = nil + cfg.StructuredOutput = &jsonBuf + cfg.StructuredLevel = LevelDebug + cfg.NotifyOutput = ¬ifyBuf + + log := newFromConfig(cfg) + log.NotifyBox(NewBox("Onboarding", "https://example.com/setup", ThemeNightOwl)) + + if jsonBuf.Len() != 0 { + t.Errorf("NotifyBox must not write to JSON writer; got: %q", jsonBuf.String()) + } + if !strings.Contains(notifyBuf.String(), "Onboarding") { + t.Errorf("expected box title in notify output; got: %q", notifyBuf.String()) + } +} + +// TestLogger_Notify_WithNotifyOutput verifies the WithNotifyOutput option redirects correctly. +func TestLogger_Notify_WithNotifyOutput(t *testing.T) { + t.Parallel() + + var notifyBuf bytes.Buffer + log := New(WithDevelopment(), WithConsoleOutput(&bytes.Buffer{}), WithNotifyOutput(¬ifyBuf)) + defer func() { _ = log.Close() }() + + log.Notify("redirected output") + + if !strings.Contains(notifyBuf.String(), "redirected output") { + t.Errorf("WithNotifyOutput did not redirect; got: %q", notifyBuf.String()) + } +} + +// TestLogger_Notify_SharesConsoleWriterMutex verifies that concurrent Info and Notify +// calls do not interleave — a heuristic race test that runs under -race. +func TestLogger_Notify_SharesConsoleWriterMutex(t *testing.T) { + t.Parallel() + + var consoleBuf safeBuffer + var notifyBuf safeBuffer + + cfg := defaultConfig() + cfg.ConsoleOutput = &consoleBuf + cfg.StructuredOutput = nil + cfg.NotifyOutput = ¬ifyBuf + + log := newFromConfig(cfg) + + const goroutines = 50 + var wg sync.WaitGroup + wg.Add(goroutines * 2) + + for range goroutines { + go func() { + defer wg.Done() + log.Info("concurrent log call") + }() + go func() { + defer wg.Done() + log.Notify("concurrent notify\n") + }() + } + + wg.Wait() + + // The race detector enforces mutual exclusion; the count check confirms writes landed. + notifyOut := notifyBuf.String() + count := strings.Count(notifyOut, "concurrent notify") + if count != goroutines { + t.Errorf("expected %d notify writes, got %d; output:\n%s", goroutines, count, notifyOut) + } +} + +// TestLogger_Notify_FallbackMutexWithNoConsoleWriter verifies the package-level fallback +// mutex is used when no console writer is configured, preventing races on the notify dest. +func TestLogger_Notify_FallbackMutexWithNoConsoleWriter(t *testing.T) { + t.Parallel() + + var notifyBuf safeBuffer + + cfg := defaultConfig() + cfg.ConsoleOutput = nil // no console writer — fallback mutex path + cfg.StructuredOutput = nil + cfg.NotifyOutput = ¬ifyBuf + + log := newFromConfig(cfg) + + const goroutines = 30 + var wg sync.WaitGroup + wg.Add(goroutines) + + for range goroutines { + go func() { + defer wg.Done() + log.Notify("fallback\n") + }() + } + + wg.Wait() + + count := strings.Count(notifyBuf.String(), "fallback") + if count != goroutines { + t.Errorf("expected %d writes via fallback mutex, got %d", goroutines, count) + } +} diff --git a/logger_render_test.go b/logger_render_test.go index 6f2bc08..ddf445f 100644 --- a/logger_render_test.go +++ b/logger_render_test.go @@ -22,13 +22,13 @@ func TestLogger_Render_IndentMatchesCustomTimeFormat(t *testing.T) { var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleTheme = ThemeNightOwl cfg.StructuredOutput = nil cfg.TimeFormat = "2006-01-02 15:04:05" // 19 chars, shorter than RFC3339 - log := NewWithConfig(cfg) + log := newFromConfig(cfg) indent := log.consoleWriter.template.CachedMessageIndentStr() // Expected width: 19 (time) + 1 (space) + 6 (badge "[INFO]") + 1 (space) = 27. @@ -48,12 +48,12 @@ func TestLogger_Render_WritesIndentedOutput(t *testing.T) { var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleTheme = ThemeNightOwl cfg.StructuredOutput = nil - log := NewWithConfig(cfg) + log := newFromConfig(cfg) indent := log.consoleWriter.template.CachedMessageIndentStr() if indent == "" { t.Fatal("expected non-empty cached message indent string") @@ -83,12 +83,12 @@ func TestLogger_RenderRaw_WritesFlushLeft(t *testing.T) { var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleTheme = ThemeNightOwl cfg.StructuredOutput = nil - log := NewWithConfig(cfg) + log := newFromConfig(cfg) r := &testRenderable{content: "rawline\n"} log.RenderRaw(r) @@ -103,12 +103,12 @@ func TestLogger_Newline_WritesNewline(t *testing.T) { var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleTheme = ThemeNightOwl cfg.StructuredOutput = nil - log := NewWithConfig(cfg) + log := newFromConfig(cfg) log.Newline() if buf.String() != "\n" { @@ -127,10 +127,10 @@ func TestLogger_Render_NilSafety(t *testing.T) { // nil renderable must not panic on a real logger var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.StructuredOutput = nil - log := NewWithConfig(cfg) + log := newFromConfig(cfg) log.Render(nil) } @@ -141,12 +141,12 @@ func TestLogger_Render_JSONWriterIgnores(t *testing.T) { var jsonBuf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = nil // no console writer cfg.StructuredOutput = &jsonBuf cfg.StructuredLevel = LevelDebug - log := NewWithConfig(cfg) + log := newFromConfig(cfg) r := &testRenderable{content: "should-not-appear\n"} log.Render(r) @@ -164,11 +164,11 @@ func TestLogger_Render_JSONWriterIgnores(t *testing.T) { func TestLogger_Render_NoConsoleWriter(t *testing.T) { t.Parallel() - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = nil cfg.StructuredOutput = nil - log := NewWithConfig(cfg) + log := newFromConfig(cfg) // Must not panic. log.Render(&testRenderable{content: "noop\n"}) @@ -176,17 +176,209 @@ func TestLogger_Render_NoConsoleWriter(t *testing.T) { log.Newline() } +// TestLogger_Box verifies Box routes through Render and produces bordered output. +func TestLogger_Box(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + cfg.ConsoleTheme = ThemeNightOwl + cfg.StructuredOutput = nil + + log := newFromConfig(cfg) + log.Box("Title", "line one\nline two") + + out := buf.String() + if !strings.Contains(out, "Title") { + t.Errorf("expected title in box output, got: %s", out) + } + if !strings.Contains(out, "line one") { + t.Errorf("expected content in box output, got: %s", out) + } +} + +// TestLogger_Table verifies Table routes through Render and produces column output. +func TestLogger_Table(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + cfg.ConsoleTheme = ThemeNightOwl + cfg.StructuredOutput = nil + + log := newFromConfig(cfg) + log.Table( + []string{"Name", "Status"}, + [][]string{{"auth", "ok"}, {"payments", "ok"}}, + ) + + out := buf.String() + if !strings.Contains(out, "Name") || !strings.Contains(out, "auth") { + t.Errorf("expected table content in output, got: %s", out) + } +} + +// TestLogger_Tree verifies Tree routes through Render and produces hierarchy output. +func TestLogger_Tree(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + cfg.ConsoleTheme = ThemeNightOwl + cfg.StructuredOutput = nil + + log := newFromConfig(cfg) + log.Tree([]TreeItem{ + {Key: "root", Children: []TreeItem{{Key: "child", Value: "val"}}}, + }) + + out := buf.String() + if !strings.Contains(out, "root") || !strings.Contains(out, "child") { + t.Errorf("expected tree nodes in output, got: %s", out) + } +} + +// TestLogger_KeyValues verifies KeyValues renders each pair to the console writer. +func TestLogger_KeyValues(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + cfg.ConsoleTheme = ThemeNightOwl + cfg.StructuredOutput = nil + + log := newFromConfig(cfg) + log.KeyValues([]KeyValuePair{ + {Key: "version", Value: "2.0.0"}, + {Key: "env", Value: "prod"}, + }) + + out := buf.String() + if !strings.Contains(out, "version") || !strings.Contains(out, "2.0.0") { + t.Errorf("expected key-value content in output, got: %s", out) + } +} + +// TestLogger_SystemInfo verifies SystemInfo renders the titled metadata block. +func TestLogger_SystemInfo(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + cfg.ConsoleTheme = ThemeNightOwl + cfg.StructuredOutput = nil + + log := newFromConfig(cfg) + log.SystemInfo(&SystemInfoData{ + Title: "TensorFoundry", + Version: "2.0.0", + Fields: []KeyValuePair{{Key: "env", Value: "production"}}, + }) + + out := buf.String() + if !strings.Contains(out, "TensorFoundry") { + t.Errorf("expected title in system info output, got: %s", out) + } +} + +// TestLogger_Bullet verifies Bullet renders the indented bullet to the console writer. +func TestLogger_Bullet(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + cfg.ConsoleTheme = ThemeNightOwl + cfg.StructuredOutput = nil + + log := newFromConfig(cfg) + log.Bullet(0, "top-level item") + log.Bullet(1, "nested item") + + out := buf.String() + if !strings.Contains(out, "top-level item") || !strings.Contains(out, "nested item") { + t.Errorf("expected bullet text in output, got: %s", out) + } + // Level 0 uses •, level 1 uses ◦. + if !strings.Contains(out, "•") || !strings.Contains(out, "◦") { + t.Errorf("expected bullet glyphs in output, got: %s", out) + } +} + +// TestLogger_Convenience_NilSafety verifies all convenience methods tolerate a nil receiver. +func TestLogger_Convenience_NilSafety(t *testing.T) { + t.Parallel() + + var l *Logger + l.Box("t", "b") + l.Table([]string{"h"}, [][]string{{"v"}}) + l.Tree([]TreeItem{{Key: "k"}}) + l.KeyValues([]KeyValuePair{{Key: "k", Value: "v"}}) + l.SystemInfo(&SystemInfoData{Title: "T"}) + l.Bullet(0, "text") +} + +// TestLogger_Convenience_ClosedLogger verifies methods are no-ops after Close. +func TestLogger_Convenience_ClosedLogger(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = &buf + cfg.ConsoleTheme = ThemeNightOwl + cfg.StructuredOutput = nil + + log := newFromConfig(cfg) + _ = log.Close() + + log.Box("title", "body") + log.Table([]string{"h"}, [][]string{{"v"}}) + log.Bullet(0, "text") + + if buf.Len() != 0 { + t.Errorf("expected no output after Close, got: %s", buf.String()) + } +} + +// TestLogger_Convenience_JSONOnlyIgnores verifies methods are no-ops without a console writer. +func TestLogger_Convenience_JSONOnlyIgnores(t *testing.T) { + t.Parallel() + + var jsonBuf bytes.Buffer + cfg := defaultConfig() + cfg.ConsoleOutput = nil + cfg.StructuredOutput = &jsonBuf + cfg.StructuredLevel = LevelDebug + + log := newFromConfig(cfg) + log.Box("title", "body") + log.Table([]string{"h"}, [][]string{{"v"}}) + log.Tree([]TreeItem{{Key: "k"}}) + log.KeyValues([]KeyValuePair{{Key: "k", Value: "v"}}) + log.SystemInfo(&SystemInfoData{Title: "T"}) + log.Bullet(0, "text") + + if jsonBuf.Len() != 0 { + t.Errorf("expected no JSON output from convenience methods, got: %s", jsonBuf.String()) + } +} + func TestLogger_Render_ConcurrentWithInfo(t *testing.T) { t.Parallel() var buf safeBuffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleTheme = ThemeNightOwl cfg.StructuredOutput = nil - log := NewWithConfig(cfg) + log := newFromConfig(cfg) var wg sync.WaitGroup diff --git a/logger_settheme_test.go b/logger_settheme_test.go index 3a2a1a7..4015fef 100644 --- a/logger_settheme_test.go +++ b/logger_settheme_test.go @@ -13,10 +13,10 @@ func TestLogger_SetTheme(t *testing.T) { t.Parallel() var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleTheme = ThemeNightOwl - log := NewWithConfig(cfg) + log := newFromConfig(cfg) log.Info("before theme change") before := buf.String() @@ -38,14 +38,36 @@ func TestLogger_SetTheme(t *testing.T) { } } -// TestLogger_SetTheme_Nil verifies that a nil theme does not panic. +// TestLogger_SetTheme_Nil verifies that a nil theme does not panic and that Theme(), +// Style(), and cfg all agree on NightOwl afterwards. This is a regression guard for the +// case where SetTheme(nil) silently left cfg nil while Style() fell back elsewhere, +// causing ANSI output under FORCE_COLOR=1 even though the caller intended a reset. func TestLogger_SetTheme_Nil(t *testing.T) { - t.Parallel() + // Cannot run in parallel — t.Setenv modifies a process-wide env var. + t.Setenv("FORCE_COLOR", "1") var buf bytes.Buffer - log := New(&buf) + log := New(WithConsoleOutput(&buf)) + // Must not panic. log.SetTheme(nil) + + // Theme(), cfg.ConsoleTheme, and Style() must all agree on NightOwl. + if got := log.Theme(); got != ThemeNightOwl { + t.Errorf("Theme() after SetTheme(nil): got %v, want ThemeNightOwl", got) + } + if log.cfg.ConsoleTheme != ThemeNightOwl { + t.Errorf("cfg.ConsoleTheme after SetTheme(nil): got %v, want ThemeNightOwl", log.cfg.ConsoleTheme) + } + // Style() must return a coloured theme (NightOwl) under FORCE_COLOR=1 after nil reset, + // not noColourTheme — the nil reset must not accidentally disable colour. + style := log.Style() + if style == noColourTheme { + t.Error("Style() returned noColourTheme after SetTheme(nil) with FORCE_COLOR=1 — nil should reset to NightOwl, not disable colour") + } + if style != ThemeNightOwl { + t.Errorf("Style() after SetTheme(nil): got %v, want ThemeNightOwl", style) + } } // TestLogger_SetTheme_NilLogger verifies nil receiver is safe. @@ -62,10 +84,10 @@ func TestLogger_Theme_Returns(t *testing.T) { t.Parallel() var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleTheme = ThemeNightOwl - log := NewWithConfig(cfg) + log := newFromConfig(cfg) if got := log.Theme(); got != ThemeNightOwl { t.Errorf("Theme() returned unexpected theme: %v", got) @@ -90,12 +112,12 @@ func TestLogger_SetTheme_PropagatestoAdditionalWriter(t *testing.T) { var primaryBuf safeBuffer var extraBuf safeBuffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &primaryBuf cfg.ConsoleTheme = ThemeNightOwl cfg.StructuredOutput = nil - log := NewWithConfig(cfg) + log := newFromConfig(cfg) // Add a console writer as an additional writer — it implements SetTheme. extra := NewConsoleWriter(&extraBuf, ThemeNightOwl) @@ -130,12 +152,12 @@ func TestLogger_SetTheme_WithCloneInherits(t *testing.T) { t.Parallel() var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleTheme = ThemeNightOwl cfg.StructuredOutput = nil - log := NewWithConfig(cfg) + log := newFromConfig(cfg) log.SetTheme(ThemeSolarized) @@ -157,11 +179,11 @@ func TestLogger_SetTheme_WithCloneInherits(t *testing.T) { buf.Reset() // Build a NightOwl logger for comparison. - cfg2 := DefaultConfig() + cfg2 := defaultConfig() cfg2.ConsoleOutput = &buf cfg2.ConsoleTheme = ThemeNightOwl cfg2.StructuredOutput = nil - owlLog := NewWithConfig(cfg2) + owlLog := newFromConfig(cfg2) owlLog.With(String("child", "true")).Info("from child") owlOut := buf.String() @@ -169,3 +191,60 @@ func TestLogger_SetTheme_WithCloneInherits(t *testing.T) { t.Log("note: themes produced identical byte sequences (unlikely but not a hard failure)") } } + +// TestLogger_Style_ColourFollowsActiveTheme is a regression test for the bug where +// Style() returned noColourTheme even when a console writer was active because +// cfg.ConsoleTheme was nil (the nil-means-NightOwl convention). +// WithDevelopment() leaves ConsoleTheme nil (uses the default), so Style() must +// return a coloured theme when colour is enabled (FORCE_COLOR=1 or real TTY). +func TestLogger_Style_ColourFollowsActiveTheme(t *testing.T) { + // Cannot run in parallel because t.Setenv modifies a process-wide env var. + // Style() is colour-aware: it returns mono for non-TTY writers and the + // themed palette for TTY writers. Use FORCE_COLOR=1 to test the colour + // path without requiring a real terminal in CI. + t.Setenv("FORCE_COLOR", "1") + + log := New(WithDevelopment()) + style := log.Style() + + // A coloured theme must not be the no-colour sentinel under FORCE_COLOR. + if style == noColourTheme { + t.Error("Style() returned noColourTheme under FORCE_COLOR=1 for a development logger") + } + + // Must not be nil. + if style == nil { + t.Error("Style() returned nil") + } + + // Confirm at least one ANSI code is present (timestamp or level colour). + if style.cachedTimestampFgStr() == "" && style.cachedLevelCode(LevelInfo) == "" { + t.Error("Style() returned a theme with no ANSI codes under FORCE_COLOR=1 — expected coloured output") + } +} + +// TestLogger_Style_NoConsoleWriter returns mono theme for JSON-only loggers. +func TestLogger_Style_NoConsoleWriter(t *testing.T) { + t.Parallel() + + // Production preset: JSON only, no console output. + log := New(WithProduction()) + style := log.Style() + + if style != noColourTheme { + t.Errorf("Style() on JSON-only logger should return noColourTheme, got %v", style) + } +} + +// TestLogger_Style_DisableColour returns mono theme when colour is off. +func TestLogger_Style_DisableColour(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + log := New(WithConsoleOutput(&buf), WithColour(false)) + style := log.Style() + + if style != noColourTheme { + t.Errorf("Style() with DisableColour should return noColourTheme, got %v", style) + } +} diff --git a/options.go b/options.go index c66baa1..8ca9ee8 100644 --- a/options.go +++ b/options.go @@ -3,76 +3,223 @@ package velocity import ( "fmt" "io" + "os" "time" ) -// Option is a functional option pattern for configuring a Logger. +// Option is a functional option that mutates a config during logger construction. // Options are applied in order, so later options override earlier ones. -type Option func(*Builder) +// Preset options (WithDevelopment, WithProduction, etc.) reset the config to a +// known baseline; layering overrides after them is the intended pattern. +type Option func(*config) + +// WithDevelopment resets config to development defaults: coloured console on +// stdout, debug level, local timezone, no structured output. +func WithDevelopment() Option { + return func(c *config) { + c.ConsoleOutput = io.Writer(nil) // reset first, then assign + *c = config{ + ConsoleOutput: defaultStdout(), + ConsoleTheme: nil, + ConsoleLevel: LevelDebug, + StructuredOutput: nil, + StructuredFormat: FormatJSON, + StructuredLevel: LevelOff, + BufferSize: 1024, + FieldPoolSize: 50, + DisableColour: false, + TimeFormat: "2006-01-02 15:04:05", + DisplayTimezone: time.Local, + FieldDisplayMode: FieldDisplayInline, + } + } +} + +// WithProduction resets config to production defaults: JSON to stderr at info +// level, no console output, UTC timestamps. +// +// stderr is used rather than stdout so application-level output (piped to +// another process, written to a file, etc.) is not contaminated by log lines. +// Override with WithStructuredOutput if a different destination is required. +func WithProduction() Option { + return func(c *config) { + *c = config{ + ConsoleOutput: io.Discard, + ConsoleTheme: nil, + ConsoleLevel: LevelOff, + StructuredOutput: defaultStderr(), + StructuredFormat: FormatJSON, + StructuredLevel: LevelInfo, + BufferSize: 4096, + FieldPoolSize: 200, + DisableColour: true, + TimeFormat: "2006-01-02T15:04:05Z07:00", + DisplayTimezone: time.UTC, + FieldDisplayMode: FieldDisplayInline, + } + } +} + +// WithContainer resets config for containerised environments: JSON to stdout at +// info level, colour disabled unless stdout is a TTY. +func WithContainer() Option { + return func(c *config) { + *c = config{ + ConsoleOutput: nil, + ConsoleTheme: nil, + ConsoleLevel: LevelOff, + StructuredOutput: defaultStdout(), + StructuredFormat: FormatJSON, + StructuredLevel: LevelInfo, + BufferSize: 2048, + FieldPoolSize: 100, + DisableColour: !isTerminal(defaultStdoutFile()), + TimeFormat: "2006-01-02T15:04:05Z07:00", + DisplayTimezone: time.UTC, + FieldDisplayMode: FieldDisplayInline, + } + } +} + +// TestingT is the subset of *testing.T needed by WithTesting. +// Defined here to avoid importing the testing package in the core library. +type TestingT interface { + Log(args ...any) + Cleanup(func()) + Helper() +} + +// WithTesting configures a logger for use in tests. Writes via t.Log, disables +// colour, sets level to Debug, and registers t.Cleanup(logger.Close). +// Notify output is also captured via the same testingWriter so tests can assert +// on ephemeral output without stderr pollution. +// The cleanup registration happens at construction time. +func WithTesting(t TestingT) Option { + tw := &testingWriter{t: t} + return func(c *config) { + *c = config{ + ConsoleOutput: tw, + NotifyOutput: tw, + ConsoleTheme: nil, + ConsoleLevel: LevelDebug, + StructuredOutput: nil, + StructuredFormat: FormatJSON, + StructuredLevel: LevelOff, + BufferSize: 512, + FieldPoolSize: 25, + DisableColour: true, + TimeFormat: "15:04:05.000", + DisplayTimezone: time.Local, + FieldDisplayMode: FieldDisplayInline, + } + } +} + +// WithNop configures a logger that discards all output. Use for tests or when a no-op logger is needed. +func WithNop() Option { + return func(c *config) { + *c = config{ + ConsoleOutput: io.Discard, + ConsoleLevel: LevelOff, + StructuredOutput: io.Discard, + StructuredLevel: LevelOff, + BufferSize: 256, + FieldPoolSize: 0, + TimeFormat: "2006-01-02T15:04:05Z07:00", + FieldDisplayMode: FieldDisplayInline, + } + } +} + +// WithHighThroughput resets config for high-throughput scenarios: JSON to stderr, +// info level, large buffer, sampling enabled (1000 initial, 100 thereafter). +func WithHighThroughput() Option { + return func(c *config) { + *c = config{ + ConsoleOutput: io.Discard, + ConsoleTheme: nil, + ConsoleLevel: LevelOff, + StructuredOutput: defaultStderr(), + StructuredFormat: FormatJSON, + StructuredLevel: LevelInfo, + BufferSize: 8192, + FieldPoolSize: 500, + DisableColour: true, + TimeFormat: "2006-01-02T15:04:05Z07:00", + DisplayTimezone: time.UTC, + FieldDisplayMode: FieldDisplayInline, + Sampler: NewCountSampler(1000, 100), + } + } +} func WithLevel(level Level) Option { - return func(b *Builder) { - b.config.ConsoleLevel = level + return func(c *config) { + c.ConsoleLevel = level } } func WithConsoleOutput(w io.Writer) Option { - return func(b *Builder) { - b.config.ConsoleOutput = w + return func(c *config) { + c.ConsoleOutput = w + } +} + +// WithNotifyOutput redirects Notify/NotifyLines/NotifyBox output to w instead of +// os.Stderr. Useful in tests where stderr is not captured by the test runner, or +// when the operator channel should go to a specific file descriptor. +func WithNotifyOutput(w io.Writer) Option { + return func(c *config) { + c.NotifyOutput = w } } func WithStructuredOutput(w io.Writer) Option { - return func(b *Builder) { - b.config.StructuredOutput = w + return func(c *config) { + c.StructuredOutput = w } } func WithFormat(format Format) Option { - return func(b *Builder) { - b.config.StructuredFormat = format + return func(c *config) { + c.StructuredFormat = format } } func WithStructuredLevel(level Level) Option { - return func(b *Builder) { - b.config.StructuredLevel = level + return func(c *config) { + c.StructuredLevel = level } } func WithTheme(theme *Theme) Option { - return func(b *Builder) { - b.config.ConsoleTheme = theme + return func(c *config) { + c.ConsoleTheme = theme } } func WithTimeFormat(format string) Option { - return func(b *Builder) { - b.config.TimeFormat = format + return func(c *config) { + c.TimeFormat = format } } func WithBufferSize(size int) Option { - return func(b *Builder) { - b.config.BufferSize = size + return func(c *config) { + c.BufferSize = size } } func WithFieldPoolSize(size int) Option { - return func(b *Builder) { - b.config.FieldPoolSize = size + return func(c *config) { + c.FieldPoolSize = size } } -func WithColourEnabled(enabled bool) Option { - return func(b *Builder) { - b.config.DisableColour = !enabled - } -} - -func WithColourDisabled() Option { - return func(b *Builder) { - b.config.DisableColour = true +// WithColour enables or disables ANSI colour in console output. +func WithColour(enabled bool) Option { + return func(c *config) { + c.DisableColour = !enabled } } @@ -80,51 +227,107 @@ func WithColourDisabled() Option { // initial is the number of initial messages to log before sampling begins. // thereafter is the sampling interval (1 in thereafter messages). func WithSampling(initial, thereafter uint32) Option { - return func(b *Builder) { - b.config.Sampler = NewCountSampler(uint64(initial), uint64(thereafter)) + return func(c *config) { + c.Sampler = NewCountSampler(uint64(initial), uint64(thereafter)) } } -// WithSampler sets a sampler for the logger. -// Pass nil to disable sampling (default). +// WithSampler sets a sampler for the logger. Pass nil to disable sampling. func WithSampler(s Sampler) Option { - return func(b *Builder) { - b.config.Sampler = s + return func(c *config) { + c.Sampler = s } } -// WithDisplayTimezone sets the timezone for displaying timestamps. -// The timezone parameter should be a valid IANA timezone name (e.g., "America/New_York", "Australia/Sydney"). -// Logs are always stored in UTC internally, but this controls how they're displayed in console output. -// Panics if the timezone name is invalid (use during logger initialisation). -func WithDisplayTimezone(tz string) Option { - return func(b *Builder) { - loc, err := time.LoadLocation(tz) - if err != nil { - panic(fmt.Sprintf("velocity: invalid timezone %q: %v", tz, err)) +// WithDisplayTimezone sets the timezone for displaying timestamps in console output. +// Logs are stored in UTC but displayed in this zone. Use MustLocation to parse +// an IANA name when building options at init time. +func WithDisplayTimezone(loc *time.Location) Option { + return func(c *config) { + if loc != nil { + c.DisplayTimezone = loc } - b.config.DisplayTimezone = loc } } // WithCaller enables or disables caller information capture (file:line and function name). func WithCaller(enabled bool) Option { - return func(b *Builder) { - b.config.AddCaller = enabled + return func(c *config) { + c.AddCaller = enabled } } -// WithCallerSkip sets the number of stack frames to skip when capturing caller information. -// Use this when wrapping the logger to skip your wrapper's frames. +// WithCallerSkip sets the number of extra stack frames to skip when capturing +// caller information. Use this when wrapping the logger to skip wrapper frames. func WithCallerSkip(skip int) Option { - return func(b *Builder) { - b.config.CallerSkip = skip + return func(c *config) { + c.CallerSkip = skip + } +} + +// WithFatalHandler overrides the function called after Fatal() writes its entry. +// Useful in tests to prevent os.Exit. +func WithFatalHandler(fn FatalHandler) Option { + return func(c *config) { + c.FatalHandler = fn + } +} + +// WithFieldDisplayMode sets how fields are rendered in console output. +func WithFieldDisplayMode(mode FieldDisplayMode) Option { + return func(c *config) { + c.FieldDisplayMode = mode + } +} + +// WithSecureTags controls the per-call ... message scanner. +// Defaults to true (scan when warranted by the writer mix). +// Set to false only for extreme-perf consumers that never embed sensitive data +// in message strings. The field constructors Secure/SecureURL/Redacted are +// unaffected — they rely on field type, not message scanning. +func WithSecureTags(enabled bool) Option { + return func(c *config) { + c.DisableSecureTags = !enabled } } -func ApplyOptions(b *Builder, opts ...Option) *Builder { - for _, opt := range opts { - opt(b) +// MustLocation parses an IANA timezone name and panics on failure. +// Intended for package-level variable initialisation. +func MustLocation(name string) *time.Location { + loc, err := time.LoadLocation(name) + if err != nil { + panic(fmt.Sprintf("velocity: invalid timezone %q: %v", name, err)) + } + return loc +} + +// defaultStdout returns os.Stdout. Extracted so preset closures don't capture +// the global at the wrong moment. +func defaultStdout() *os.File { + return os.Stdout +} + +func defaultStdoutFile() *os.File { + return os.Stdout +} + +func defaultStderr() *os.File { + return os.Stderr +} + +// testingWriter adapts TestingT.Log to io.Writer so the console writer can +// forward formatted log lines into the test's output stream. +type testingWriter struct { + t TestingT +} + +func (w *testingWriter) Write(p []byte) (int, error) { + w.t.Helper() + // Trim trailing newline — t.Log adds its own. + s := string(p) + if len(s) > 0 && s[len(s)-1] == '\n' { + s = s[:len(s)-1] } - return b + w.t.Log(s) + return len(p), nil } diff --git a/pretty.go b/pretty.go new file mode 100644 index 0000000..56c06f9 --- /dev/null +++ b/pretty.go @@ -0,0 +1,332 @@ +package velocity + +import ( + "fmt" + "io" + "strings" +) + +// Pretty is the standalone facade for rich terminal output. It is distinct from Logger: +// CLI commands that want coloured output without a structured logging pipeline use Pretty +// directly. Both Pretty and Logger are facades over the same Renderable types in this package. +// +// Callers sharing a writer with an active Logger must serialise writes externally — +// Pretty has no mutex of its own, intentionally, because the Logger's consoleWriter +// mutex is the serialisation point when routing through NewPrettyFromLogger. +type Pretty struct { + writer io.Writer + theme *Theme +} + +// NewPretty returns a Pretty that writes to w using the given theme. +// When theme is nil, colour capability is derived from w using resolveColourForWriter +// (which honours FORCE_COLOR / NO_COLOR and fd-level TTY detection): a colour-capable +// writer gets ThemeNightOwl; a non-colour writer gets ThemeMono. +// If w is nil, output goes to io.Discard with no colour. +func NewPretty(w io.Writer, theme *Theme) *Pretty { + if w == nil { + return &Pretty{writer: io.Discard, theme: ThemeMono} + } + if theme == nil { + if resolveColourForWriter(w) { + theme = ThemeNightOwl + } else { + theme = ThemeMono + } + } + return &Pretty{writer: w, theme: theme} +} + +// NewPrettyFromLogger returns a Pretty whose writes are serialised under the logger's +// console writer mutex, preventing interleaving with concurrent log calls. +// The theme is derived from Logger.Style(), which returns a mono theme when the +// console writer has colour disabled (NO_COLOR, piped output, or WithProduction). +// Returns nil if log is nil — callers can branch on presence without a nil check ladder. +func NewPrettyFromLogger(log *Logger) *Pretty { + if log == nil { + return nil + } + return &Pretty{ + writer: &prettyLoggerWriter{log: log}, + theme: log.Style(), + } +} + +// prettyLoggerWriter routes writes through Logger.RenderRaw so output is flush-left +// and serialised under the console writer's mutex. +type prettyLoggerWriter struct { + log *Logger +} + +func (lw *prettyLoggerWriter) Write(p []byte) (int, error) { + lw.log.RenderRaw(&rawBytesRenderable{data: p}) + return len(p), nil +} + +// rawBytesRenderable wraps a byte slice as a Renderable for use in RenderRaw. +type rawBytesRenderable struct { + data []byte +} + +func (r *rawBytesRenderable) Render(w io.Writer) error { + _, err := w.Write(r.data) + return err +} + +// Box draws a bordered box around content, with an optional title in the top border. +func (p *Pretty) Box(title, content string) { + if p == nil { + return + } + _ = NewBox(title, content, p.theme).Render(p.writer) +} + +// Table renders an aligned table with auto-sized columns. +func (p *Pretty) Table(headers []string, rows [][]string) { + if p == nil { + return + } + _ = NewTable(headers, rows, p.theme).Render(p.writer) +} + +// Tree prints a hierarchy of TreeItem nodes. +func (p *Pretty) Tree(nodes []TreeItem) { + if p == nil { + return + } + _ = NewTree(nodes, p.theme).Render(p.writer) +} + +// Banner draws a double-border box around text. +func (p *Pretty) Banner(text string) { + if p == nil { + return + } + _ = NewBanner(text, p.theme).Render(p.writer) +} + +// KeyValue prints a two-column key: value line. +func (p *Pretty) KeyValue(key, value string) { + if p == nil { + fmt.Printf("%s: %s\n", key, value) + return + } + _ = NewKeyValue(key, value, p.theme).Render(p.writer) +} + +// Bullet prints an indented bullet point at the given nesting level. +func (p *Pretty) Bullet(level int, text string) { + if p == nil { + return + } + buf := GetBuffer(128) + defer PutBuffer(buf) + indent := strings.Repeat(" ", level) + bullets := []string{"•", "◦", "▪", "▫"} + bullet := bullets[level%len(bullets)] + + buf.WriteString(indent) + buf.WriteString(p.theme.CachedFieldKeyFg()) + buf.WriteString(bullet) + buf.WriteString(p.theme.ResetStr()) + buf.WriteString(" ") + buf.WriteString(p.theme.CachedMessageFg()) + buf.WriteString(text) + buf.WriteString(p.theme.ResetStr()) + buf.WriteString("\n") + _, _ = buf.WriteTo(p.writer) +} + +// SystemInfo prints a titled block of key-value pairs. +func (p *Pretty) SystemInfo(info *SystemInfoData) { + if p == nil || info == nil { + return + } + _ = NewSystemInfo(info, p.theme).Render(p.writer) +} + +// Section prints a titled section header with a dashed underline. +func (p *Pretty) Section(title string) { + if p == nil { + fmt.Println(title) + fmt.Println(strings.Repeat("─", 40)) + return + } + buf := GetBuffer(128) + defer PutBuffer(buf) + buf.WriteString(p.theme.CachedMessageFg()) + buf.WriteString(title) + buf.WriteString(p.theme.ResetStr()) + buf.WriteString("\n") + buf.WriteString(strings.Repeat("─", 40)) + buf.WriteString("\n") + _, _ = buf.WriteTo(p.writer) +} + +// Render writes an arbitrary Renderable to the Pretty's writer. +// The canonical path for custom Renderables. +func (p *Pretty) Render(r Renderable) { + if p == nil || r == nil { + return + } + _ = r.Render(p.writer) +} + +// Panel draws a simple bordered block with a title bar. +func (p *Pretty) Panel(title, content string) { + if p == nil { + return + } + buf := GetBuffer(256) + defer PutBuffer(buf) + buf.WriteString(p.theme.CachedMessageFg()) + if title != "" { + buf.WriteString("▓ ") + buf.WriteString(title) + buf.WriteString(" ▓\n") + } + buf.WriteString(content) + buf.WriteString(p.theme.ResetStr()) + buf.WriteString("\n") + _, _ = buf.WriteTo(p.writer) +} + +// Raw writes text directly to the writer without any formatting. +func (p *Pretty) Raw(text string) { + if p == nil { + return + } + _, _ = io.WriteString(p.writer, text) +} + +// Success prints a success-styled message. Nil-safe: falls back to stdout. +func (p *Pretty) Success(message string) { + if p == nil { + fmt.Println("✅ " + message) + return + } + p.printStyled("✅", message, p.theme.cachedLevelCode(LevelInfo)) +} + +// Warn prints a warning-styled message. Nil-safe: falls back to stdout. +func (p *Pretty) Warn(message string) { + if p == nil { + fmt.Println("⚠️ " + message) + return + } + p.printStyled("⚠️", message, p.theme.cachedLevelCode(LevelWarn)) +} + +// Error prints an error-styled message. Nil-safe: falls back to stdout. +func (p *Pretty) Error(message string) { + if p == nil { + fmt.Println("❌ " + message) + return + } + p.printStyled("❌", message, p.theme.cachedLevelCode(LevelError)) +} + +// Info prints an info-styled message. Nil-safe: falls back to stdout. +func (p *Pretty) Info(message string) { + if p == nil { + fmt.Println("ℹ️ " + message) + return + } + p.printStyled("ℹ️", message, p.theme.cachedLevelCode(LevelInfo)) +} + +// Muted prints a dimmed message using the timestamp/muted colour — useful for secondary +// output that should recede visually (hints, paths, supplementary context). +func (p *Pretty) Muted(message string) { + if p == nil { + fmt.Println(message) + return + } + p.printStyled("", message, p.theme.cachedTimestampFgStr()) +} + +// Debug prints a debug-styled message. Nil-safe: falls back to stdout. +func (p *Pretty) Debug(message string) { + if p == nil { + fmt.Println("🐛 " + message) + return + } + p.printStyled("🐛", message, p.theme.cachedLevelCode(LevelDebug)) +} + +// printStyled writes an ANSI-coloured line. Write errors are silently dropped — +// pretty printing must never fail the caller. +func (p *Pretty) printStyled(icon, message, ansiCode string) { + buf := GetBuffer(128) + defer PutBuffer(buf) + buf.WriteString(ansiCode) + if icon != "" { + buf.WriteString(icon) + buf.WriteString(" ") + } + buf.WriteString(message) + buf.WriteString(p.theme.ResetStr()) + buf.WriteString("\n") + _, _ = buf.WriteTo(p.writer) +} + +// CreateBanner renders a double-border banner box with ASCII art, title, version, and URL. +// Useful for CLI splash screens. Returns a string ready to print or pass to Logger.Banner. +func CreateBanner(title, version, url string, ascii []string) string { + var b strings.Builder + maxLen := 0 + + for _, line := range ascii { + if len(line) > maxLen { + maxLen = len(line) + } + } + if len(title)+len(version)+3 > maxLen { + maxLen = len(title) + len(version) + 3 + } + if len(url) > maxLen { + maxLen = len(url) + } + + boxWidth := maxLen + 4 + + b.WriteString("╔") + b.WriteString(strings.Repeat("═", boxWidth-2)) + b.WriteString("╗\n") + + for _, line := range ascii { + b.WriteString("║ ") + b.WriteString(line) + b.WriteString(strings.Repeat(" ", maxLen-len(line))) + b.WriteString(" ║\n") + } + + if len(ascii) > 0 { + b.WriteString("╠") + b.WriteString(strings.Repeat("═", boxWidth-2)) + b.WriteString("╣\n") + } + + titleLine := fmt.Sprintf("%s v%s", title, version) + padding := (maxLen - len(titleLine)) / 2 + b.WriteString("║ ") + b.WriteString(strings.Repeat(" ", padding)) + b.WriteString(titleLine) + b.WriteString(strings.Repeat(" ", maxLen-len(titleLine)-padding)) + b.WriteString(" ║\n") + + if url != "" { + urlPadding := (maxLen - len(url)) / 2 + b.WriteString("║ ") + b.WriteString(strings.Repeat(" ", urlPadding)) + b.WriteString(url) + b.WriteString(strings.Repeat(" ", maxLen-len(url)-urlPadding)) + b.WriteString(" ║\n") + } + + b.WriteString("╚") + b.WriteString(strings.Repeat("═", boxWidth-2)) + b.WriteString("╝\n") + + return b.String() +} diff --git a/pretty/benchmark_test.go b/pretty/benchmark_test.go deleted file mode 100644 index 71bd002..0000000 --- a/pretty/benchmark_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package pretty_test - -import ( - "io" - "testing" - - velocity "github.com/tensorfoundrylabs/velocity" - "github.com/tensorfoundrylabs/velocity/pretty" -) - -var ( - benchHeaders = []string{"Service", "Status"} - benchRows = [][]string{ - {"api-gateway", "running"}, - {"worker", "stopped"}, - {"scheduler", "running"}, - } -) - -func newBenchLogger() *velocity.Logger { - cfg := velocity.DefaultConfig() - cfg.ConsoleOutput = io.Discard - cfg.StructuredOutput = io.Discard - cfg.ConsoleLevel = velocity.LevelDebug - cfg.StructuredLevel = velocity.LevelDebug - return velocity.NewWithConfig(cfg) -} - -// BenchmarkPretty_NewFromLogger_Table measures the full path: construct a Pretty -// via NewFromLogger, then render a 3-row table through the logger writer. -// Construction is excluded from the timer; we want the per-render cost. -func BenchmarkPretty_NewFromLogger_Table(b *testing.B) { - log := newBenchLogger() - p := pretty.NewFromLogger(log) - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - p.Table(benchHeaders, benchRows) - } -} - -// BenchmarkPretty_New_Table measures the same table render via the standalone -// pretty.New path writing to io.Discard, for direct comparison with NewFromLogger. -func BenchmarkPretty_New_Table(b *testing.B) { - p := pretty.New(io.Discard, velocity.ThemeNightOwl) - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - p.Table(benchHeaders, benchRows) - } -} diff --git a/pretty/doc.go b/pretty/doc.go deleted file mode 100644 index 62b4fb3..0000000 --- a/pretty/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package pretty provides styled terminal output utilities for CLI applications. -// Boxes, panels, banners, tables, trees, progress bars, and spinners. -// -// When a velocity.Logger is available, use NewFromLogger to route output through -// the logger's console writer — this keeps pretty output serialised with log lines -// and indented to align with the message column. -package pretty diff --git a/pretty/pretty.go b/pretty/pretty.go deleted file mode 100644 index 729f986..0000000 --- a/pretty/pretty.go +++ /dev/null @@ -1,499 +0,0 @@ -package pretty - -import ( - "fmt" - "io" - "os" - "strings" - - velocity "github.com/tensorfoundrylabs/velocity" -) - -// Box-drawing constants for tree rendering. -const ( - treeBranch = "├─ " - treeCorner = "└─ " - treePipe = "│ " - treeBlank = " " -) - -// Pretty provides formatted output utilities for styled terminal printing. -type Pretty struct { - writer io.Writer - theme *velocity.Theme -} - -// New returns a Pretty that writes to w using the given theme. -// If theme is nil, ThemeNightOwl is used. -// If w is nil, output is discarded — prefer NewFromLogger when a logger exists. -// User-defined themes are cached automatically — no explicit Theme.Cache() call required. -func New(w io.Writer, theme *velocity.Theme) *Pretty { - if theme == nil { - theme = velocity.ThemeNightOwl - } else { - // EnsureCached populates ANSI codes in-place via sync.Once — concurrent-safe, - // always returns the same pointer. - theme = theme.EnsureCached() - } - if w == nil { - w = io.Discard - } - return &Pretty{ - writer: w, - theme: theme, - } -} - -// NewFromLogger returns a Pretty whose output is serialised under the logger's console -// writer mutex (preventing interleaving with concurrent log calls) and inherits the -// logger's theme. Output is flush-left by default, matching pretty.New semantics. -// -// To indent a specific block under the message column, use log.Render with a result -// type directly — e.g. log.Render(p.NewTree(items)). -// Returns nil if log is nil. The theme is cached automatically if not already populated. -func NewFromLogger(log *velocity.Logger) *Pretty { - if log == nil { - return nil - } - return &Pretty{ - writer: &loggerWriter{log: log}, - theme: log.Theme().EnsureCached(), - } -} - -// loggerWriter adapts velocity.Logger to io.Writer, routing writes through Logger.RenderRaw -// so that pretty output is flush-left (matching pretty.New semantics) while still -// serialising under the console writer's mutex. -type loggerWriter struct { - log *velocity.Logger -} - -func (lw *loggerWriter) Write(p []byte) (int, error) { - lw.log.RenderRaw(&bytesRenderable{data: p}) - return len(p), nil -} - -// bytesRenderable wraps a byte slice as a Renderable. -type bytesRenderable struct { - data []byte -} - -func (r *bytesRenderable) Render(w io.Writer) error { - _, err := w.Write(r.data) - return err -} - -// Info prints an info-styled message with nil-safe fallback to stdout. -func (p *Pretty) Info(message string) { - if p == nil { - fmt.Println("ℹ️ " + message) - return - } - p.printStyled("ℹ️", message, p.theme.InfoColour) -} - -// Success prints a success-styled message with nil-safe fallback to stdout. -func (p *Pretty) Success(message string) { - if p == nil { - fmt.Println("✅ " + message) - return - } - p.printStyled("✅", message, p.theme.InfoColour) -} - -// Warn prints a warning-styled message with nil-safe fallback to stdout. -func (p *Pretty) Warn(message string) { - if p == nil { - fmt.Println("⚠️ " + message) - return - } - p.printStyled("⚠️", message, p.theme.WarnColour) -} - -// Error prints an error-styled message with nil-safe fallback to stdout. -func (p *Pretty) Error(message string) { - if p == nil { - fmt.Println("❌ " + message) - return - } - p.printStyled("❌", message, p.theme.ErrorColour) -} - -// Debug prints a debug-styled message with nil-safe fallback to stdout. -func (p *Pretty) Debug(message string) { - if p == nil { - fmt.Println("🐛 " + message) - return - } - p.printStyled("🐛", message, p.theme.DebugColour) -} - -// printStyled ignores write errors to ensure pretty printing never fails. -func (p *Pretty) printStyled(icon, message string, colour velocity.Colour) { - buf := velocity.GetBuffer(128) - defer velocity.PutBuffer(buf) - buf.WriteString(colour.ANSI(true)) - buf.WriteString(icon) - buf.WriteString(" ") - buf.WriteString(message) - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - _, _ = buf.WriteTo(p.writer) -} - -// Section prints a titled section header with an underline. -func (p *Pretty) Section(title string) { - if p == nil { - fmt.Println(title) - fmt.Println(strings.Repeat("─", 40)) - return - } - - if p.theme == nil { - p.theme = velocity.ThemeNightOwl - } - - buf := velocity.GetBuffer(128) - defer velocity.PutBuffer(buf) - buf.WriteString(p.theme.CachedMessageFg()) - buf.WriteString(title) - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - buf.WriteString(strings.Repeat("─", 40)) - buf.WriteString("\n") - - if p.writer == nil { - fmt.Print(buf.String()) - return - } - - _, _ = buf.WriteTo(p.writer) -} - -// Box draws a bordered box around content, with an optional title in the top border. -func (p *Pretty) Box(title, content string) { - _ = NewBoxResult(title, content, p.theme).Render(p.writer) -} - -// NewBox returns a BoxResult for the given title and content using p's theme. -// Use this with Logger.Render to route the box through the logger's console writer. -func (p *Pretty) NewBox(title, content string) *BoxResult { - return NewBoxResult(title, content, p.theme) -} - -// Panel draws a simple bordered block with a title bar. -func (p *Pretty) Panel(title, content string) { - buf := velocity.GetBuffer(256) - defer velocity.PutBuffer(buf) - buf.WriteString(p.theme.CachedMessageFg()) - if title != "" { - buf.WriteString("▓ ") - buf.WriteString(title) - buf.WriteString(" ▓\n") - } - buf.WriteString(content) - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - _, _ = buf.WriteTo(p.writer) -} - -// Bullet prints an indented bullet point at the given nesting level. -func (p *Pretty) Bullet(level int, text string) { - buf := velocity.GetBuffer(128) - defer velocity.PutBuffer(buf) - indent := strings.Repeat(" ", level) - bullets := []string{"•", "◦", "▪", "▫"} - bullet := bullets[level%len(bullets)] - - buf.WriteString(indent) - buf.WriteString(p.theme.CachedFieldKeyFg()) - buf.WriteString(bullet) - buf.WriteString(velocity.Reset) - buf.WriteString(" ") - buf.WriteString(p.theme.CachedMessageFg()) - buf.WriteString(text) - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - _, _ = buf.WriteTo(p.writer) -} - -// KeyValue prints a two-column key: value line with nil-safe fallback. -func (p *Pretty) KeyValue(key, value string) { - if p == nil { - fmt.Printf("%s: %s\n", key, value) - return - } - - if p.theme == nil { - p.theme = velocity.ThemeNightOwl - } - - w := p.writer - if w == nil { - buf := velocity.GetBuffer(128) - defer velocity.PutBuffer(buf) - _ = NewKeyValueResult(key, value, p.theme).Render(buf) - fmt.Print(buf.String()) - return - } - - _ = NewKeyValueResult(key, value, p.theme).Render(w) -} - -// NewKeyValue returns a KeyValueResult using p's theme. -// Use this with Logger.Render to route through the logger's console writer. -func (p *Pretty) NewKeyValue(key, value string) *KeyValueResult { - return NewKeyValueResult(key, value, p.theme) -} - -// Table renders an aligned table with auto-sized columns. -func (p *Pretty) Table(headers []string, rows [][]string) { - _ = NewTableResult(headers, rows, p.theme).Render(p.writer) -} - -// NewTable returns a TableResult for the given data using p's theme. -// Use this with Logger.Render to route the table through the logger's console writer. -func (p *Pretty) NewTable(headers []string, rows [][]string) *TableResult { - return NewTableResult(headers, rows, p.theme) -} - -// visibleLen returns the number of visible runes in s, ignoring ANSI escape sequences. -func visibleLen(s string) int { - n := 0 - inEscape := false - for _, r := range s { - if inEscape { - if r == 'm' { - inEscape = false - } - continue - } - if r == '\033' { - inEscape = true - continue - } - n++ - } - return n -} - -// padRightVisible pads s to width based on visible rune count, accounting for ANSI codes. -func padRightVisible(s string, width int) string { - visible := visibleLen(s) - if visible >= width { - return s - } - return s + strings.Repeat(" ", width-visible) -} - -func padRight(s string, length int) string { - if len(s) >= length { - return s - } - return s + strings.Repeat(" ", length-len(s)) -} - -func padRightRunes(s string, length int) string { - runeLen := len([]rune(s)) - if runeLen >= length { - return s - } - return s + strings.Repeat(" ", length-runeLen) -} - -// Tree prints a hierarchy of TreeItem nodes with nil-safe fallback to stdout. -func (p *Pretty) Tree(nodes []TreeItem) { - if p == nil { - for i, node := range nodes { - writeTreeItemStandalone(os.Stdout, node, "", i == len(nodes)-1) - } - return - } - - if p.theme == nil { - p.theme = velocity.ThemeNightOwl - } - - w := p.writer - if w == nil { - // Nil writer is a fallback path — collect and print. - buf := velocity.GetBuffer(512) - defer velocity.PutBuffer(buf) - for i, node := range nodes { - writePrettyTreeItemInto(buf, p.theme, node, "", i == len(nodes)-1) - } - fmt.Print(buf.String()) - return - } - - _ = NewTreeResult(nodes, p.theme).Render(w) -} - -// NewTree returns a TreeResult for the given nodes using p's theme. -// Use this with Logger.Render to route the tree through the logger's console writer. -func (p *Pretty) NewTree(nodes []TreeItem) *TreeResult { - return NewTreeResult(nodes, p.theme) -} - -func writeTreeItemStandalone(w io.Writer, node TreeItem, prefix string, isLast bool) { - connector := treeBranch - if isLast { - connector = treeCorner - } - - if node.Value != nil { - _, _ = fmt.Fprintf(w, "%s%s%s: %v\n", prefix, connector, node.Key, node.Value) - } else { - _, _ = fmt.Fprintf(w, "%s%s%s\n", prefix, connector, node.Key) - } - - childPrefix := prefix - if isLast { - childPrefix += treeBlank - } else { - childPrefix += treePipe - } - - for i, child := range node.Children { - writeTreeItemStandalone(w, child, childPrefix, i == len(node.Children)-1) - } -} - -// Raw writes text directly to the writer without any formatting. -func (p *Pretty) Raw(text string) { - _, _ = io.WriteString(p.writer, text) -} - -// Banner draws a double-border box around text. -func (p *Pretty) Banner(text string) { - _ = NewBannerResult(text, p.theme).Render(p.writer) -} - -// NewBanner returns a BannerResult for the given text using p's theme. -// Use this with Logger.Render to route the banner through the logger's console writer. -func (p *Pretty) NewBanner(text string) *BannerResult { - return NewBannerResult(text, p.theme) -} - -// SystemInfo is startup/configuration metadata for display. -type SystemInfo struct { - Title string - Version string - Fields []KeyValuePair -} - -// KeyValuePair is a labelled string value. -type KeyValuePair struct { - Key string - Value string -} - -// SystemInfo prints a titled block of key-value pairs with nil-safe fallback. -func (p *Pretty) SystemInfo(info *SystemInfo) { - if info == nil { - return - } - - if p == nil { - if info.Title != "" { - version := "" - if info.Version != "" { - version = " v" + info.Version - } - fmt.Printf("▓ %s%s ▓\n", info.Title, version) - } - for _, pair := range info.Fields { - fmt.Printf("%-20s %s\n", pair.Key+":", pair.Value) - } - return - } - - if p.theme == nil { - p.theme = velocity.ThemeNightOwl - } - - w := p.writer - if w == nil { - // Collect and print to stdout as fallback. - buf := velocity.GetBuffer(512) - defer velocity.PutBuffer(buf) - _ = NewSystemInfoResult(info, p.theme).Render(buf) - fmt.Print(buf.String()) - return - } - - _ = NewSystemInfoResult(info, p.theme).Render(w) -} - -// NewSystemInfo returns a SystemInfoResult for the given info using p's theme. -// Use this with Logger.Render to route the output through the logger's console writer. -func (p *Pretty) NewSystemInfo(info *SystemInfo) *SystemInfoResult { - return NewSystemInfoResult(info, p.theme) -} - -// TreeItem represents a node in a hierarchical display tree. -type TreeItem struct { - Key string - Value any - Children []TreeItem -} - -// CreateBanner renders a double-border banner box with ASCII art, title, version, and URL. -func CreateBanner(title, version, url string, ascii []string) string { - var b strings.Builder - maxLen := 0 - - for _, line := range ascii { - if len(line) > maxLen { - maxLen = len(line) - } - } - if len(title)+len(version)+3 > maxLen { - maxLen = len(title) + len(version) + 3 - } - if len(url) > maxLen { - maxLen = len(url) - } - - boxWidth := maxLen + 4 - - b.WriteString("╔") - b.WriteString(strings.Repeat("═", boxWidth-2)) - b.WriteString("╗\n") - - for _, line := range ascii { - b.WriteString("║ ") - b.WriteString(line) - b.WriteString(strings.Repeat(" ", maxLen-len(line))) - b.WriteString(" ║\n") - } - - if len(ascii) > 0 { - b.WriteString("╠") - b.WriteString(strings.Repeat("═", boxWidth-2)) - b.WriteString("╣\n") - } - - titleLine := fmt.Sprintf("%s v%s", title, version) - padding := (maxLen - len(titleLine)) / 2 - b.WriteString("║ ") - b.WriteString(strings.Repeat(" ", padding)) - b.WriteString(titleLine) - b.WriteString(strings.Repeat(" ", maxLen-len(titleLine)-padding)) - b.WriteString(" ║\n") - - if url != "" { - urlPadding := (maxLen - len(url)) / 2 - b.WriteString("║ ") - b.WriteString(strings.Repeat(" ", urlPadding)) - b.WriteString(url) - b.WriteString(strings.Repeat(" ", maxLen-len(url)-urlPadding)) - b.WriteString(" ║\n") - } - - b.WriteString("╚") - b.WriteString(strings.Repeat("═", boxWidth-2)) - b.WriteString("╝\n") - - return b.String() -} diff --git a/pretty/pretty_logger_test.go b/pretty/pretty_logger_test.go deleted file mode 100644 index db5acd8..0000000 --- a/pretty/pretty_logger_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package pretty_test - -import ( - "bytes" - "strings" - "testing" - - velocity "github.com/tensorfoundrylabs/velocity" - "github.com/tensorfoundrylabs/velocity/pretty" -) - -func TestNewFromLogger_RoutesToLogger(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - - cfg := velocity.DefaultConfig() - cfg.ConsoleOutput = &buf - cfg.ConsoleTheme = velocity.ThemeNightOwl - cfg.StructuredOutput = nil - - log := velocity.NewWithConfig(cfg) - p := pretty.NewFromLogger(log) - - if p == nil { - t.Fatal("NewFromLogger returned nil for non-nil logger") - } - - p.KeyValue("key", "value") - - out := buf.String() - if !strings.Contains(out, "key") || !strings.Contains(out, "value") { - t.Errorf("expected key-value in output, got: %s", out) - } -} - -func TestNewFromLogger_NilLogger_ReturnsNil(t *testing.T) { - t.Parallel() - - p := pretty.NewFromLogger(nil) - if p != nil { - t.Error("expected nil Pretty for nil logger") - } -} diff --git a/pretty/progress_test.go b/pretty/progress_test.go deleted file mode 100644 index 37fcec0..0000000 --- a/pretty/progress_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package pretty - -import ( - "io" - "sync" - "testing" -) - -// TestProgressBar_ConcurrentComplete verifies that simultaneous Complete calls -// do not panic due to double-close of the done channel. -func TestProgressBar_ConcurrentComplete(_ *testing.T) { - pb := NewProgressBar(io.Discard, 100, "test") - - var wg sync.WaitGroup - for range 100 { - wg.Add(1) - go func() { - defer wg.Done() - pb.Complete() - }() - } - wg.Wait() -} - -// TestSpinner_ConcurrentStop verifies that simultaneous Stop calls -// do not panic due to double-close of the done channel. -func TestSpinner_ConcurrentStop(_ *testing.T) { - s := NewSpinner(io.Discard, "test") - - var wg sync.WaitGroup - for range 100 { - wg.Add(1) - go func() { - defer wg.Done() - s.Stop() - }() - } - wg.Wait() -} - -// TestMultiProgress_ConcurrentStop verifies that simultaneous Stop calls -// do not panic due to double-close of the done channel. -func TestMultiProgress_ConcurrentStop(_ *testing.T) { - mp := NewMultiProgress(io.Discard) - - var wg sync.WaitGroup - for range 100 { - wg.Add(1) - go func() { - defer wg.Done() - mp.Stop() - }() - } - wg.Wait() -} - -// TestSpinner_SetStyle_NilReceiver verifies nil safety, consistent with other -// Spinner methods. -func TestSpinner_SetStyle_NilReceiver(_ *testing.T) { - var s *Spinner - s.SetStyle(SpinnerStyleDots) -} diff --git a/pretty/renderable.go b/pretty/renderable.go deleted file mode 100644 index a4757cf..0000000 --- a/pretty/renderable.go +++ /dev/null @@ -1,420 +0,0 @@ -package pretty - -import ( - "bytes" - "fmt" - "io" - "strings" - - velocity "github.com/tensorfoundrylabs/velocity" -) - -// BoxResult holds the configuration for a Box render and implements velocity.Renderable. -type BoxResult struct { - theme *velocity.Theme - title string - content string -} - -// NewBoxResult returns a BoxResult ready to render. -func NewBoxResult(title, content string, theme *velocity.Theme) *BoxResult { - if theme == nil { - theme = velocity.ThemeNightOwl - } - return &BoxResult{title: title, content: content, theme: theme} -} - -// Render writes the bordered box (title + content) to w. -func (r *BoxResult) Render(w io.Writer) error { - buf := velocity.GetBuffer(512) - defer velocity.PutBuffer(buf) - renderBox(buf, r.theme, r.title, r.content) - _, err := buf.WriteTo(w) - return err -} - -func renderBox(buf *bytes.Buffer, theme *velocity.Theme, title, content string) { - lines := strings.Split(content, "\n") - if len(lines) > 0 && lines[len(lines)-1] == "" { - lines = lines[:len(lines)-1] - } - - maxLineRunes := 0 - for _, line := range lines { - if n := len([]rune(line)); n > maxLineRunes { - maxLineRunes = n - } - } - - width := max(maxLineRunes+4, 42) - if titleWidth := len([]rune(title)) + 6; titleWidth > width { - width = titleWidth - } - - topFill := width - 2 - 1 - if title != "" { - topFill -= len([]rune(title)) + 1 - } - - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString("┌─") - if title != "" { - buf.WriteString(title) - buf.WriteString("─") - } - buf.WriteString(strings.Repeat("─", topFill)) - buf.WriteString("┐") - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - - for _, line := range lines { - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString("│ ") - buf.WriteString(velocity.Reset) - buf.WriteString(theme.CachedMessageFg()) - buf.WriteString(padRightRunes(line, width-3)) - buf.WriteString(velocity.Reset) - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString("│") - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - } - - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString("└") - buf.WriteString(strings.Repeat("─", width-2)) - buf.WriteString("┘") - buf.WriteString(velocity.Reset) - buf.WriteString("\n") -} - -// TableResult holds the configuration for a Table render and implements velocity.Renderable. -type TableResult struct { - theme *velocity.Theme - headers []string - rows [][]string -} - -// NewTableResult returns a TableResult ready to render. -func NewTableResult(headers []string, rows [][]string, theme *velocity.Theme) *TableResult { - if theme == nil { - theme = velocity.ThemeNightOwl - } - return &TableResult{headers: headers, rows: rows, theme: theme} -} - -// Render writes the aligned table with auto-sized columns to w. -// Returns nil without writing if headers or rows are empty. -func (r *TableResult) Render(w io.Writer) error { - if len(r.headers) == 0 || len(r.rows) == 0 { - return nil - } - buf := velocity.GetBuffer(1024) - defer velocity.PutBuffer(buf) - renderTable(buf, r.theme, r.headers, r.rows) - _, err := buf.WriteTo(w) - return err -} - -func renderTable(buf *bytes.Buffer, theme *velocity.Theme, headers []string, rows [][]string) { - colWidths := calcColumnWidths(headers, rows) - writeTableTopBorder(buf, theme, colWidths) - writeTableHeaders(buf, theme, headers, colWidths) - writeTableHeaderSeparator(buf, theme, colWidths) - for _, row := range rows { - writeTableRow(buf, theme, row, colWidths) - } - writeTableBottomBorder(buf, theme, colWidths) -} - -func calcColumnWidths(headers []string, rows [][]string) []int { - colWidths := make([]int, len(headers)) - for i, h := range headers { - colWidths[i] = len(h) - } - for _, row := range rows { - for i, cell := range row { - if i < len(colWidths) { - if vl := visibleLen(cell); vl > colWidths[i] { - colWidths[i] = vl - } - } - } - } - return colWidths -} - -func writeTableTopBorder(buf *bytes.Buffer, theme *velocity.Theme, colWidths []int) { - buf.WriteString(theme.CachedFieldKeyFg()) - for i, w := range colWidths { - buf.WriteString(strings.Repeat("─", w+2)) - if i < len(colWidths)-1 { - buf.WriteString("┬") - } - } - buf.WriteString(velocity.Reset) - buf.WriteString("\n") -} - -func writeTableHeaders(buf *bytes.Buffer, theme *velocity.Theme, headers []string, colWidths []int) { - buf.WriteString(theme.CachedFieldKeyFg()) - for i, header := range headers { - if i > 0 { - buf.WriteString("│") - } - buf.WriteString(" ") - buf.WriteString(velocity.Reset) - buf.WriteString(theme.CachedTableHeaderFg()) - buf.WriteString(padRight(header, colWidths[i])) - buf.WriteString(velocity.Reset) - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString(" ") - } - buf.WriteString(velocity.Reset) - buf.WriteString("\n") -} - -func writeTableHeaderSeparator(buf *bytes.Buffer, theme *velocity.Theme, colWidths []int) { - buf.WriteString(theme.CachedFieldKeyFg()) - for i, w := range colWidths { - buf.WriteString(strings.Repeat("─", w+2)) - if i < len(colWidths)-1 { - buf.WriteString("┼") - } - } - buf.WriteString(velocity.Reset) - buf.WriteString("\n") -} - -func writeTableRow(buf *bytes.Buffer, theme *velocity.Theme, row []string, colWidths []int) { - buf.WriteString(theme.CachedFieldKeyFg()) - for i, cell := range row { - if i >= len(colWidths) { - break - } - buf.WriteString(" ") - buf.WriteString(theme.CachedMessageFg()) - buf.WriteString(padRightVisible(cell, colWidths[i])) - buf.WriteString(velocity.Reset) - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString(" ") - if i < len(colWidths)-1 { - buf.WriteString("│") - } - } - buf.WriteString(velocity.Reset) - buf.WriteString("\n") -} - -func writeTableBottomBorder(buf *bytes.Buffer, theme *velocity.Theme, colWidths []int) { - buf.WriteString(theme.CachedFieldKeyFg()) - for i, w := range colWidths { - buf.WriteString(strings.Repeat("─", w+2)) - if i < len(colWidths)-1 { - buf.WriteString("┴") - } - } - buf.WriteString(velocity.Reset) - buf.WriteString("\n") -} - -// BannerResult holds the configuration for a Banner render and implements velocity.Renderable. -type BannerResult struct { - theme *velocity.Theme - text string -} - -// NewBannerResult returns a BannerResult ready to render. -func NewBannerResult(text string, theme *velocity.Theme) *BannerResult { - if theme == nil { - theme = velocity.ThemeNightOwl - } - return &BannerResult{text: text, theme: theme} -} - -// Render writes the double-border banner box to w. -func (r *BannerResult) Render(w io.Writer) error { - buf := velocity.GetBuffer(512) - defer velocity.PutBuffer(buf) - renderBanner(buf, r.theme, r.text) - _, err := buf.WriteTo(w) - return err -} - -func renderBanner(buf *bytes.Buffer, theme *velocity.Theme, text string) { - lines := strings.Split(text, "\n") - - maxLen := 0 - for i, line := range lines { - lines[i] = strings.TrimRight(line, " \t") - if n := len([]rune(lines[i])); n > maxLen { - maxLen = n - } - } - - contentWidth := maxLen - boxWidth := contentWidth + 2 - - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString("╔") - buf.WriteString(strings.Repeat("─", boxWidth)) - buf.WriteString("╗") - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - - for _, line := range lines { - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString("│ ") - buf.WriteString(velocity.Reset) - buf.WriteString(theme.CachedMessageFg()) - buf.WriteString(padRightRunes(line, contentWidth)) - buf.WriteString(velocity.Reset) - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString(" │") - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - } - - buf.WriteString(theme.CachedFieldKeyFg()) - buf.WriteString("╚") - buf.WriteString(strings.Repeat("─", boxWidth)) - buf.WriteString("╝") - buf.WriteString(velocity.Reset) - buf.WriteString("\n") -} - -// TreeResult holds tree nodes for rendering and implements velocity.Renderable. -type TreeResult struct { - theme *velocity.Theme - nodes []TreeItem -} - -// NewTreeResult returns a TreeResult ready to render. -func NewTreeResult(nodes []TreeItem, theme *velocity.Theme) *TreeResult { - if theme == nil { - theme = velocity.ThemeNightOwl - } - return &TreeResult{nodes: nodes, theme: theme} -} - -// Render writes the tree hierarchy with box-drawing connectors to w. -func (r *TreeResult) Render(w io.Writer) error { - buf := velocity.GetBuffer(512) - defer velocity.PutBuffer(buf) - for i, node := range r.nodes { - writePrettyTreeItemInto(buf, r.theme, node, "", i == len(r.nodes)-1) - } - _, err := buf.WriteTo(w) - return err -} - -func writePrettyTreeItemInto(buf *bytes.Buffer, theme *velocity.Theme, node TreeItem, prefix string, isLast bool) { - connector := treeBranch - if isLast { - connector = treeCorner - } - - buf.WriteString(prefix) - buf.WriteString(connector) - buf.WriteString(theme.CachedMessageFg()) - if node.Value != nil { - _, _ = fmt.Fprintf(buf, "%s: %v", node.Key, node.Value) - } else { - buf.WriteString(node.Key) - } - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - - childPrefix := prefix - if isLast { - childPrefix += treeBlank - } else { - childPrefix += treePipe - } - - for i, child := range node.Children { - writePrettyTreeItemInto(buf, theme, child, childPrefix, i == len(node.Children)-1) - } -} - -// KeyValueResult holds a key-value pair for rendering and implements velocity.Renderable. -type KeyValueResult struct { - theme *velocity.Theme - key string - value string -} - -// NewKeyValueResult returns a KeyValueResult ready to render. -func NewKeyValueResult(key, value string, theme *velocity.Theme) *KeyValueResult { - if theme == nil { - theme = velocity.ThemeNightOwl - } - return &KeyValueResult{key: key, value: value, theme: theme} -} - -// Render writes "key: value\n" with theme colouring to w. -func (r *KeyValueResult) Render(w io.Writer) error { - buf := velocity.GetBuffer(128) - defer velocity.PutBuffer(buf) - buf.WriteString(r.theme.CachedFieldKeyFg()) - buf.WriteString(r.key) - buf.WriteString(velocity.Reset) - buf.WriteString(": ") - buf.WriteString(r.theme.CachedFieldValFg()) - buf.WriteString(r.value) - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - _, err := buf.WriteTo(w) - return err -} - -// SystemInfoResult holds system info metadata for rendering and implements velocity.Renderable. -type SystemInfoResult struct { - theme *velocity.Theme - info *SystemInfo -} - -// NewSystemInfoResult returns a SystemInfoResult ready to render. -func NewSystemInfoResult(info *SystemInfo, theme *velocity.Theme) *SystemInfoResult { - if theme == nil { - theme = velocity.ThemeNightOwl - } - return &SystemInfoResult{info: info, theme: theme} -} - -// Render writes the titled block of key-value system info pairs to w. -// Returns nil without writing if info is nil. -func (r *SystemInfoResult) Render(w io.Writer) error { - if r.info == nil { - return nil - } - buf := velocity.GetBuffer(512) - defer velocity.PutBuffer(buf) - - if r.info.Title != "" { - buf.WriteString(r.theme.CachedInfoColourFg()) - buf.WriteString("▓ ") - buf.WriteString(r.info.Title) - if r.info.Version != "" { - buf.WriteString(" v") - buf.WriteString(r.info.Version) - } - buf.WriteString(" ▓") - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - } - - for _, pair := range r.info.Fields { - buf.WriteString(r.theme.CachedFieldKeyFg()) - buf.WriteString(padRight(pair.Key+":", 20)) - buf.WriteString(velocity.Reset) - buf.WriteString(" ") - buf.WriteString(r.theme.CachedMessageFg()) - buf.WriteString(pair.Value) - buf.WriteString(velocity.Reset) - buf.WriteString("\n") - } - - _, err := buf.WriteTo(w) - return err -} diff --git a/pretty/renderable_test.go b/pretty/renderable_test.go deleted file mode 100644 index 0b00f76..0000000 --- a/pretty/renderable_test.go +++ /dev/null @@ -1,232 +0,0 @@ -package pretty_test - -import ( - "bytes" - "strings" - "testing" - - velocity "github.com/tensorfoundrylabs/velocity" - "github.com/tensorfoundrylabs/velocity/pretty" -) - -// Compile-time assertions: every result type must satisfy velocity.Renderable. -var ( - _ velocity.Renderable = (*pretty.BoxResult)(nil) - _ velocity.Renderable = (*pretty.TableResult)(nil) - _ velocity.Renderable = (*pretty.BannerResult)(nil) - _ velocity.Renderable = (*pretty.TreeResult)(nil) - _ velocity.Renderable = (*pretty.KeyValueResult)(nil) - _ velocity.Renderable = (*pretty.SystemInfoResult)(nil) -) - -// TestBoxResult_ParityWithPrettyBox verifies that BoxResult.Render produces the -// same bytes as p.Box so callers can freely choose either form. -func TestBoxResult_ParityWithPrettyBox(t *testing.T) { - t.Parallel() - - var direct, viaResult bytes.Buffer - - p := pretty.New(&direct, velocity.ThemeNightOwl) - p.Box("Title", "some content\nsecond line") - - result := pretty.NewBoxResult("Title", "some content\nsecond line", velocity.ThemeNightOwl) - if err := result.Render(&viaResult); err != nil { - t.Fatalf("BoxResult.Render returned error: %v", err) - } - - if direct.String() != viaResult.String() { - t.Errorf("output mismatch:\n p.Box: %q\n BoxResult: %q", direct.String(), viaResult.String()) - } -} - -// TestTableResult_ParityWithPrettyTable verifies that TableResult.Render matches p.Table. -func TestTableResult_ParityWithPrettyTable(t *testing.T) { - t.Parallel() - - headers := []string{"Name", "Value"} - rows := [][]string{{"alpha", "1"}, {"beta", "2"}} - - var direct, viaResult bytes.Buffer - - p := pretty.New(&direct, velocity.ThemeNightOwl) - p.Table(headers, rows) - - result := pretty.NewTableResult(headers, rows, velocity.ThemeNightOwl) - if err := result.Render(&viaResult); err != nil { - t.Fatalf("TableResult.Render returned error: %v", err) - } - - if direct.String() != viaResult.String() { - t.Errorf("output mismatch:\n p.Table: %q\n TableResult: %q", direct.String(), viaResult.String()) - } -} - -// TestBannerResult_ParityWithPrettyBanner verifies that BannerResult.Render matches p.Banner. -func TestBannerResult_ParityWithPrettyBanner(t *testing.T) { - t.Parallel() - - text := "Welcome\nTo Velocity" - - var direct, viaResult bytes.Buffer - - p := pretty.New(&direct, velocity.ThemeNightOwl) - p.Banner(text) - - result := pretty.NewBannerResult(text, velocity.ThemeNightOwl) - if err := result.Render(&viaResult); err != nil { - t.Fatalf("BannerResult.Render returned error: %v", err) - } - - if direct.String() != viaResult.String() { - t.Errorf("output mismatch:\n p.Banner: %q\n BannerResult:%q", direct.String(), viaResult.String()) - } -} - -// TestTreeResult_ParityWithPrettyTree verifies that TreeResult.Render matches p.Tree. -func TestTreeResult_ParityWithPrettyTree(t *testing.T) { - t.Parallel() - - nodes := []pretty.TreeItem{ - {Key: "root", Children: []pretty.TreeItem{ - {Key: "child1", Value: "v1"}, - {Key: "child2", Value: "v2"}, - }}, - } - - var direct, viaResult bytes.Buffer - - p := pretty.New(&direct, velocity.ThemeNightOwl) - p.Tree(nodes) - - result := pretty.NewTreeResult(nodes, velocity.ThemeNightOwl) - if err := result.Render(&viaResult); err != nil { - t.Fatalf("TreeResult.Render returned error: %v", err) - } - - if direct.String() != viaResult.String() { - t.Errorf("output mismatch:\n p.Tree: %q\n TreeResult: %q", direct.String(), viaResult.String()) - } -} - -// TestKeyValueResult_ParityWithPrettyKeyValue verifies output parity. -func TestKeyValueResult_ParityWithPrettyKeyValue(t *testing.T) { - t.Parallel() - - var direct, viaResult bytes.Buffer - - p := pretty.New(&direct, velocity.ThemeNightOwl) - p.KeyValue("version", "1.2.3") - - result := pretty.NewKeyValueResult("version", "1.2.3", velocity.ThemeNightOwl) - if err := result.Render(&viaResult); err != nil { - t.Fatalf("KeyValueResult.Render returned error: %v", err) - } - - if direct.String() != viaResult.String() { - t.Errorf("output mismatch:\n p.KeyValue: %q\n KeyValueResult:%q", direct.String(), viaResult.String()) - } -} - -// TestSystemInfoResult_ParityWithPrettySystemInfo verifies output parity. -func TestSystemInfoResult_ParityWithPrettySystemInfo(t *testing.T) { - t.Parallel() - - info := &pretty.SystemInfo{ - Title: "TestApp", - Version: "0.1.0", - Fields: []pretty.KeyValuePair{ - {Key: "env", Value: "test"}, - {Key: "region", Value: "ap-southeast-2"}, - }, - } - - var direct, viaResult bytes.Buffer - - p := pretty.New(&direct, velocity.ThemeNightOwl) - p.SystemInfo(info) - - result := pretty.NewSystemInfoResult(info, velocity.ThemeNightOwl) - if err := result.Render(&viaResult); err != nil { - t.Fatalf("SystemInfoResult.Render returned error: %v", err) - } - - if direct.String() != viaResult.String() { - t.Errorf("output mismatch:\n p.SystemInfo: %q\n SystemInfoResult:%q", direct.String(), viaResult.String()) - } -} - -// TestTableResult_EmptyHeaders confirms the early-return path produces no output. -func TestTableResult_EmptyHeaders(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - result := pretty.NewTableResult(nil, nil, velocity.ThemeNightOwl) - if err := result.Render(&buf); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if buf.Len() != 0 { - t.Errorf("expected empty output for nil headers, got: %q", buf.String()) - } -} - -// TestSystemInfoResult_NilInfo confirms the nil-info guard produces no output. -func TestSystemInfoResult_NilInfo(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - result := pretty.NewSystemInfoResult(nil, velocity.ThemeNightOwl) - if err := result.Render(&buf); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if buf.Len() != 0 { - t.Errorf("expected empty output for nil info, got: %q", buf.String()) - } -} - -// TestNewFromLogger_Concurrent verifies that concurrent pretty and logger calls don't interleave -// or race (run with -race to validate). -func TestNewFromLogger_Concurrent(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - - cfg := velocity.DefaultConfig() - cfg.ConsoleOutput = &buf - cfg.ConsoleTheme = velocity.ThemeNightOwl - cfg.StructuredOutput = nil - - log := velocity.NewWithConfig(cfg) - p := pretty.NewFromLogger(log) - - const goroutines = 20 - done := make(chan struct{}) - - for range goroutines / 2 { - go func() { - for range 5 { - log.Info("log line") - } - done <- struct{}{} - }() - } - - for range goroutines / 2 { - go func() { - for range 5 { - p.KeyValue("key", "val") - } - done <- struct{}{} - }() - } - - for range goroutines { - <-done - } - - // If we get here without data race or panic the test passes. - // Output must contain both kinds of content. - out := buf.String() - if !strings.Contains(out, "log line") { - t.Error("expected log output in buffer") - } -} diff --git a/pretty_test.go b/pretty_test.go new file mode 100644 index 0000000..282abd3 --- /dev/null +++ b/pretty_test.go @@ -0,0 +1,137 @@ +package velocity_test + +import ( + "bytes" + "strings" + "testing" + + velocity "github.com/tensorfoundrylabs/velocity/v2" +) + +func TestNewPrettyFromLogger_RoutesToLogger(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + + log := velocity.New( + velocity.WithConsoleOutput(&buf), + velocity.WithTheme(velocity.ThemeNightOwl), + velocity.WithLevel(velocity.LevelDebug), + ) + p := velocity.NewPrettyFromLogger(log) + + if p == nil { + t.Fatal("NewPrettyFromLogger returned nil for non-nil logger") + } + + p.KeyValue("key", "value") + + out := buf.String() + if !strings.Contains(out, "key") || !strings.Contains(out, "value") { + t.Errorf("expected key-value in output, got: %s", out) + } +} + +func TestNewPrettyFromLogger_NilLogger_ReturnsNil(t *testing.T) { + t.Parallel() + + p := velocity.NewPrettyFromLogger(nil) + if p != nil { + t.Error("expected nil Pretty for nil logger") + } +} + +func TestNewPretty_NilWriter_UsesDiscard(t *testing.T) { + t.Parallel() + + // nil writer must not panic — output goes to io.Discard + p := velocity.NewPretty(nil, velocity.ThemeNightOwl) + p.Box("title", "content") + p.Section("section") + p.KeyValue("k", "v") + p.Success("ok") + p.Warn("warn") + p.Error("err") + p.Info("info") + p.Muted("muted") + p.Debug("debug") +} + +func TestPretty_NilReceiver_DoesNotPanic(t *testing.T) { + t.Parallel() + + // Every method must tolerate a nil receiver. + var p *velocity.Pretty + p.Box("t", "c") + p.Table([]string{"h"}, [][]string{{"v"}}) + p.Tree([]velocity.TreeItem{{Key: "k"}}) + p.Banner("b") + p.KeyValue("k", "v") + p.Bullet(0, "text") + p.SystemInfo(&velocity.SystemInfoData{Title: "T"}) + p.Section("s") + p.Render(nil) + p.Panel("title", "body") + p.Raw("raw") + p.Success("ok") + p.Warn("warn") + p.Error("err") + p.Info("info") + p.Muted("muted") + p.Debug("debug") +} + +func TestPretty_Box_WritesToWriter(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + p := velocity.NewPretty(&buf, velocity.ThemeNightOwl) + p.Box("My Title", "line one\nline two") + + out := buf.String() + if !strings.Contains(out, "My Title") { + t.Errorf("expected title in box output, got: %s", out) + } +} + +func TestPretty_Section_WritesUnderline(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + p := velocity.NewPretty(&buf, velocity.ThemeNightOwl) + p.Section("Deployment") + + out := buf.String() + if !strings.Contains(out, "Deployment") { + t.Errorf("expected section title in output, got: %s", out) + } + if !strings.Contains(out, "─") { + t.Errorf("expected dashed underline in output, got: %s", out) + } +} + +func TestPretty_Render_CustomRenderable(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + p := velocity.NewPretty(&buf, nil) + box := velocity.NewBox("custom", "body", nil) + p.Render(box) + + if buf.Len() == 0 { + t.Error("expected output from Render, got nothing") + } +} + +func TestPretty_Muted_UsesTimestampColour(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + p := velocity.NewPretty(&buf, velocity.ThemeNightOwl) + p.Muted("secondary info") + + out := buf.String() + if !strings.Contains(out, "secondary info") { + t.Errorf("expected message in muted output, got: %s", out) + } +} diff --git a/renderable.go b/renderable.go index b88dea4..5eac46d 100644 --- a/renderable.go +++ b/renderable.go @@ -1,6 +1,11 @@ package velocity -import "io" +import ( + "bytes" + "fmt" + "io" + "strings" +) // Renderable is implemented by any value that can write a formatted representation // of itself to an io.Writer. @@ -11,3 +16,543 @@ import "io" type Renderable interface { Render(w io.Writer) error } + +// TTYRenderable is an optional extension to Renderable for types that need to +// know whether the destination is a TTY before choosing between ANSI and plain +// output. Logger.Render checks for this interface and passes the console writer's +// resolved TTY state (which accounts for FORCE_COLOR / NO_COLOR env vars and +// fd-level detection), so rendering decisions are consistent with how the rest +// of the log line was formatted. +// +// Types that implement this interface should NOT call IsTerminalWriter on the +// supplied io.Writer — they should use the isTTY argument instead, because the +// writer is an intermediate buffer, not the final output sink. +type TTYRenderable interface { + Renderable + RenderTTY(w io.Writer, isTTY bool) error +} + +// Box-drawing constants shared by tree, box, and table renderers. +const ( + treeBranch = "├─ " + treeCorner = "└─ " + treePipe = "│ " + treeBlank = " " +) + +// TreeItem represents a node in a hierarchical display tree. +type TreeItem struct { + Key string + Value any + Children []TreeItem +} + +// KeyValuePair is a labelled string value used in SystemInfo display blocks. +type KeyValuePair struct { + Key string + Value string +} + +// SystemInfoData is startup/configuration metadata for display via SystemInfo. +// Renamed from SystemInfo to free that name for the Renderable type. +type SystemInfoData struct { + Title string + Version string + Fields []KeyValuePair +} + +// Box holds the configuration for a bordered box render. +type Box struct { + theme *Theme + title string + content string +} + +// NewBox returns a Box ready to render. +func NewBox(title, content string, theme *Theme) *Box { + if theme == nil { + theme = ThemeNightOwl + } + return &Box{title: title, content: content, theme: theme} +} + +// Render writes the bordered box (title + content) to w. +func (b *Box) Render(w io.Writer) error { + buf := GetBuffer(512) + defer PutBuffer(buf) + renderBox(buf, b.theme, b.title, b.content) + _, err := buf.WriteTo(w) + return err +} + +// String renders the box to a string — useful for tests and capture. +func (b *Box) String() string { + var buf bytes.Buffer + _ = b.Render(&buf) + return buf.String() +} + +func renderBox(buf *bytes.Buffer, theme *Theme, title, content string) { + lines := strings.Split(content, "\n") + if len(lines) > 0 && lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + + maxLineRunes := 0 + for _, line := range lines { + if n := len([]rune(line)); n > maxLineRunes { + maxLineRunes = n + } + } + + width := max(maxLineRunes+4, 42) + if titleWidth := len([]rune(title)) + 6; titleWidth > width { + width = titleWidth + } + + topFill := width - 2 - 1 + if title != "" { + topFill -= len([]rune(title)) + 1 + } + + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString("┌─") + if title != "" { + buf.WriteString(title) + buf.WriteString("─") + } + buf.WriteString(strings.Repeat("─", topFill)) + buf.WriteString("┐") + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") + + for _, line := range lines { + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString("│ ") + buf.WriteString(theme.ResetStr()) + buf.WriteString(theme.CachedMessageFg()) + buf.WriteString(padRightRunes(line, width-3)) + buf.WriteString(theme.ResetStr()) + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString("│") + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") + } + + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString("└") + buf.WriteString(strings.Repeat("─", width-2)) + buf.WriteString("┘") + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") +} + +// Table holds the configuration for a table render. +type Table struct { + theme *Theme + headers []string + rows [][]string +} + +// NewTable returns a Table ready to render. +func NewTable(headers []string, rows [][]string, theme *Theme) *Table { + if theme == nil { + theme = ThemeNightOwl + } + return &Table{headers: headers, rows: rows, theme: theme} +} + +// Render writes the aligned table with auto-sized columns to w. +// Returns nil without writing if headers or rows are empty. +func (t *Table) Render(w io.Writer) error { + if len(t.headers) == 0 || len(t.rows) == 0 { + return nil + } + buf := GetBuffer(1024) + defer PutBuffer(buf) + renderTable(buf, t.theme, t.headers, t.rows) + _, err := buf.WriteTo(w) + return err +} + +// String renders the table to a string — useful for tests and capture. +func (t *Table) String() string { + var buf bytes.Buffer + _ = t.Render(&buf) + return buf.String() +} + +func renderTable(buf *bytes.Buffer, theme *Theme, headers []string, rows [][]string) { + colWidths := calcColumnWidths(headers, rows) + writeTableTopBorder(buf, theme, colWidths) + writeTableHeaders(buf, theme, headers, colWidths) + writeTableHeaderSeparator(buf, theme, colWidths) + for _, row := range rows { + writeTableRow(buf, theme, row, colWidths) + } + writeTableBottomBorder(buf, theme, colWidths) +} + +func calcColumnWidths(headers []string, rows [][]string) []int { + colWidths := make([]int, len(headers)) + for i, h := range headers { + colWidths[i] = len(h) + } + for _, row := range rows { + for i, cell := range row { + if i < len(colWidths) { + if vl := visibleLen(cell); vl > colWidths[i] { + colWidths[i] = vl + } + } + } + } + return colWidths +} + +func writeTableTopBorder(buf *bytes.Buffer, theme *Theme, colWidths []int) { + buf.WriteString(theme.CachedFieldKeyFg()) + for i, w := range colWidths { + buf.WriteString(strings.Repeat("─", w+2)) + if i < len(colWidths)-1 { + buf.WriteString("┬") + } + } + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") +} + +func writeTableHeaders(buf *bytes.Buffer, theme *Theme, headers []string, colWidths []int) { + buf.WriteString(theme.CachedFieldKeyFg()) + for i, header := range headers { + if i > 0 { + buf.WriteString("│") + } + buf.WriteString(" ") + buf.WriteString(theme.ResetStr()) + buf.WriteString(theme.CachedTableHeaderFg()) + buf.WriteString(padRight(header, colWidths[i])) + buf.WriteString(theme.ResetStr()) + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString(" ") + } + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") +} + +func writeTableHeaderSeparator(buf *bytes.Buffer, theme *Theme, colWidths []int) { + buf.WriteString(theme.CachedFieldKeyFg()) + for i, w := range colWidths { + buf.WriteString(strings.Repeat("─", w+2)) + if i < len(colWidths)-1 { + buf.WriteString("┼") + } + } + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") +} + +func writeTableRow(buf *bytes.Buffer, theme *Theme, row []string, colWidths []int) { + buf.WriteString(theme.CachedFieldKeyFg()) + for i, cell := range row { + if i >= len(colWidths) { + break + } + buf.WriteString(" ") + buf.WriteString(theme.CachedMessageFg()) + buf.WriteString(padRightVisible(cell, colWidths[i])) + buf.WriteString(theme.ResetStr()) + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString(" ") + if i < len(colWidths)-1 { + buf.WriteString("│") + } + } + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") +} + +func writeTableBottomBorder(buf *bytes.Buffer, theme *Theme, colWidths []int) { + buf.WriteString(theme.CachedFieldKeyFg()) + for i, w := range colWidths { + buf.WriteString(strings.Repeat("─", w+2)) + if i < len(colWidths)-1 { + buf.WriteString("┴") + } + } + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") +} + +// Banner holds the configuration for a double-border banner box render. +type Banner struct { + theme *Theme + text string +} + +// NewBanner returns a Banner ready to render. +func NewBanner(text string, theme *Theme) *Banner { + if theme == nil { + theme = ThemeNightOwl + } + return &Banner{text: text, theme: theme} +} + +// Render writes the double-border banner box to w. +func (b *Banner) Render(w io.Writer) error { + buf := GetBuffer(512) + defer PutBuffer(buf) + renderBanner(buf, b.theme, b.text) + _, err := buf.WriteTo(w) + return err +} + +// String renders the banner to a string — useful for tests and capture. +func (b *Banner) String() string { + var buf bytes.Buffer + _ = b.Render(&buf) + return buf.String() +} + +func renderBanner(buf *bytes.Buffer, theme *Theme, text string) { + lines := strings.Split(text, "\n") + + maxLen := 0 + for i, line := range lines { + lines[i] = strings.TrimRight(line, " \t") + if n := len([]rune(lines[i])); n > maxLen { + maxLen = n + } + } + + contentWidth := maxLen + boxWidth := contentWidth + 2 + + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString("╔") + buf.WriteString(strings.Repeat("─", boxWidth)) + buf.WriteString("╗") + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") + + for _, line := range lines { + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString("│ ") + buf.WriteString(theme.ResetStr()) + buf.WriteString(theme.CachedMessageFg()) + buf.WriteString(padRightRunes(line, contentWidth)) + buf.WriteString(theme.ResetStr()) + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString(" │") + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") + } + + buf.WriteString(theme.CachedFieldKeyFg()) + buf.WriteString("╚") + buf.WriteString(strings.Repeat("─", boxWidth)) + buf.WriteString("╝") + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") +} + +// Tree holds tree nodes for rendering. +type Tree struct { + theme *Theme + nodes []TreeItem +} + +// NewTree returns a Tree ready to render. +func NewTree(nodes []TreeItem, theme *Theme) *Tree { + if theme == nil { + theme = ThemeNightOwl + } + return &Tree{nodes: nodes, theme: theme} +} + +// Render writes the tree hierarchy with box-drawing connectors to w. +func (t *Tree) Render(w io.Writer) error { + buf := GetBuffer(512) + defer PutBuffer(buf) + for i, node := range t.nodes { + writeTreeItemInto(buf, t.theme, node, "", i == len(t.nodes)-1) + } + _, err := buf.WriteTo(w) + return err +} + +// String renders the tree to a string — useful for tests and capture. +func (t *Tree) String() string { + var buf bytes.Buffer + _ = t.Render(&buf) + return buf.String() +} + +func writeTreeItemInto(buf *bytes.Buffer, theme *Theme, node TreeItem, prefix string, isLast bool) { + connector := treeBranch + if isLast { + connector = treeCorner + } + + buf.WriteString(prefix) + buf.WriteString(connector) + buf.WriteString(theme.CachedMessageFg()) + if node.Value != nil { + _, _ = fmt.Fprintf(buf, "%s: %v", node.Key, node.Value) + } else { + buf.WriteString(node.Key) + } + buf.WriteString(theme.ResetStr()) + buf.WriteString("\n") + + childPrefix := prefix + if isLast { + childPrefix += treeBlank + } else { + childPrefix += treePipe + } + + for i, child := range node.Children { + writeTreeItemInto(buf, theme, child, childPrefix, i == len(node.Children)-1) + } +} + +// KeyValue holds a key-value pair for rendering. +type KeyValue struct { + theme *Theme + key string + value string +} + +// NewKeyValue returns a KeyValue ready to render. +func NewKeyValue(key, value string, theme *Theme) *KeyValue { + if theme == nil { + theme = ThemeNightOwl + } + return &KeyValue{key: key, value: value, theme: theme} +} + +// Render writes "key: value\n" with theme colouring to w. +func (kv *KeyValue) Render(w io.Writer) error { + buf := GetBuffer(128) + defer PutBuffer(buf) + buf.WriteString(kv.theme.CachedFieldKeyFg()) + buf.WriteString(kv.key) + buf.WriteString(kv.theme.ResetStr()) + buf.WriteString(": ") + buf.WriteString(kv.theme.CachedFieldValFg()) + buf.WriteString(kv.value) + buf.WriteString(kv.theme.ResetStr()) + buf.WriteString("\n") + _, err := buf.WriteTo(w) + return err +} + +// String renders the key-value pair to a string — useful for tests and capture. +func (kv *KeyValue) String() string { + var buf bytes.Buffer + _ = kv.Render(&buf) + return buf.String() +} + +// SystemInfo holds system info metadata for rendering. +type SystemInfo struct { + theme *Theme + info *SystemInfoData +} + +// NewSystemInfo returns a SystemInfo ready to render. +func NewSystemInfo(info *SystemInfoData, theme *Theme) *SystemInfo { + if theme == nil { + theme = ThemeNightOwl + } + return &SystemInfo{info: info, theme: theme} +} + +// Render writes the titled block of key-value system info pairs to w. +// Returns nil without writing if info is nil. +func (s *SystemInfo) Render(w io.Writer) error { + if s.info == nil { + return nil + } + buf := GetBuffer(512) + defer PutBuffer(buf) + + if s.info.Title != "" { + buf.WriteString(s.theme.CachedInfoColourFg()) + buf.WriteString("▓ ") + buf.WriteString(s.info.Title) + if s.info.Version != "" { + buf.WriteString(" v") + buf.WriteString(s.info.Version) + } + buf.WriteString(" ▓") + buf.WriteString(s.theme.ResetStr()) + buf.WriteString("\n") + } + + for _, pair := range s.info.Fields { + buf.WriteString(s.theme.CachedFieldKeyFg()) + buf.WriteString(padRight(pair.Key+":", 20)) + buf.WriteString(s.theme.ResetStr()) + buf.WriteString(" ") + buf.WriteString(s.theme.CachedMessageFg()) + buf.WriteString(pair.Value) + buf.WriteString(s.theme.ResetStr()) + buf.WriteString("\n") + } + + _, err := buf.WriteTo(w) + return err +} + +// String renders the system info block to a string — useful for tests and capture. +func (s *SystemInfo) String() string { + var buf bytes.Buffer + _ = s.Render(&buf) + return buf.String() +} + +// visibleLen returns the number of visible runes in s, ignoring ANSI escape sequences. +func visibleLen(s string) int { + n := 0 + inEscape := false + for _, r := range s { + if inEscape { + if r == 'm' { + inEscape = false + } + continue + } + if r == '\033' { + inEscape = true + continue + } + n++ + } + return n +} + +// padRightVisible pads s to width based on visible rune count, accounting for ANSI codes. +func padRightVisible(s string, width int) string { + visible := visibleLen(s) + if visible >= width { + return s + } + return s + strings.Repeat(" ", width-visible) +} + +func padRight(s string, length int) string { + if len(s) >= length { + return s + } + return s + strings.Repeat(" ", length-len(s)) +} + +func padRightRunes(s string, length int) string { + runeLen := len([]rune(s)) + if runeLen >= length { + return s + } + return s + strings.Repeat(" ", length-runeLen) +} diff --git a/pretty/banner_test.go b/renderable_banner_test.go similarity index 89% rename from pretty/banner_test.go rename to renderable_banner_test.go index ba5173a..304f78e 100644 --- a/pretty/banner_test.go +++ b/renderable_banner_test.go @@ -1,16 +1,16 @@ -package pretty +package velocity_test import ( "bytes" "strings" "testing" - velocity "github.com/tensorfoundrylabs/velocity" + velocity "github.com/tensorfoundrylabs/velocity/v2" ) func TestBanner_SingleLine(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) p.Banner("Hello World") @@ -39,7 +39,7 @@ func TestBanner_SingleLine(t *testing.T) { func TestBanner_MultiLine(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) p.Banner("First line\nSecond line\nThird line") @@ -54,8 +54,9 @@ func TestBanner_MultiLine(t *testing.T) { } } +// removeANSI strips terminal control codes from s. State machine parsing preserves +// UTF-8 while stripping escape sequences — used across banner and box tests. func removeANSI(s string) string { - // State machine parsing preserves UTF-8 while stripping terminal control codes var result strings.Builder i := 0 bs := []byte(s) @@ -78,7 +79,7 @@ func removeANSI(s string) string { func TestBanner_EmptyLine(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) p.Banner("") @@ -94,7 +95,7 @@ func TestBanner_EmptyLine(t *testing.T) { func TestBanner_VaryingLineLengths(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) p.Banner("Short\nThis is a much longer line\nMid") @@ -123,7 +124,7 @@ func TestBanner_VaryingLineLengths(t *testing.T) { func TestBanner_TrailingWhitespace(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) // Test with trailing spaces (simulating ASCII art logo issue) // The trailing spaces should be stripped, making the box tight @@ -164,7 +165,7 @@ func TestBanner_TrailingWhitespace(t *testing.T) { func TestBanner_Unicode(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) // Test with Unicode characters (like the ASCII art logo) banner := "███████╗ ██████╗\n██╔════╝██╔════╝" diff --git a/pretty/box_test.go b/renderable_box_test.go similarity index 89% rename from pretty/box_test.go rename to renderable_box_test.go index f610c19..3dc083f 100644 --- a/pretty/box_test.go +++ b/renderable_box_test.go @@ -1,11 +1,11 @@ -package pretty +package velocity_test import ( "bytes" "strings" "testing" - velocity "github.com/tensorfoundrylabs/velocity" + velocity "github.com/tensorfoundrylabs/velocity/v2" ) // borderLen returns the visible character count of a line stripped of ANSI codes. @@ -17,7 +17,7 @@ func borderLen(line string) int { func TestBox_LongTitle(t *testing.T) { t.Helper() buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) // Title longer than 36 bytes with short content previously caused a panic // in strings.Repeat when the repeat count went negative. @@ -26,7 +26,7 @@ func TestBox_LongTitle(t *testing.T) { func TestBox_BorderAlignment(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) p.Box("Title", "Some content line") @@ -51,7 +51,7 @@ func TestBox_BorderAlignment(t *testing.T) { func TestBox_EmptyContent(t *testing.T) { t.Helper() buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) // Must not panic; empty content produces only borders. p.Box("Title", "") @@ -61,7 +61,7 @@ func TestBox_EmptyContent(t *testing.T) { // not silently dropped. func TestBox_EmptyLines(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) p.Box("Title", "line1\n\nline3") @@ -77,7 +77,7 @@ func TestBox_EmptyLines(t *testing.T) { // TestBox_Unicode verifies that box borders align correctly when content contains multi-byte runes. func TestBox_Unicode(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) p.Box("", "日本語\nASCII\nCafe") @@ -108,7 +108,7 @@ func TestBox_Unicode(t *testing.T) { func TestBox_EmptyTitle(t *testing.T) { buf := &bytes.Buffer{} - p := New(buf, velocity.ThemeNightOwl) + p := velocity.NewPretty(buf, velocity.ThemeNightOwl) // Must not panic; empty title uses plain border. p.Box("", "Some content") diff --git a/renderable_parity_test.go b/renderable_parity_test.go new file mode 100644 index 0000000..549fb4f --- /dev/null +++ b/renderable_parity_test.go @@ -0,0 +1,228 @@ +package velocity_test + +import ( + "bytes" + "testing" + + velocity "github.com/tensorfoundrylabs/velocity/v2" +) + +// Compile-time assertions: every renderable type must satisfy velocity.Renderable. +var ( + _ velocity.Renderable = (*velocity.Box)(nil) + _ velocity.Renderable = (*velocity.Table)(nil) + _ velocity.Renderable = (*velocity.Banner)(nil) + _ velocity.Renderable = (*velocity.Tree)(nil) + _ velocity.Renderable = (*velocity.KeyValue)(nil) + _ velocity.Renderable = (*velocity.SystemInfo)(nil) +) + +// TestBoxResult_ParityWithPrettyBox verifies that Box.Render and Pretty.Box +// produce identical output so callers can freely choose either form. +func TestBoxResult_ParityWithPrettyBox(t *testing.T) { + t.Parallel() + + var direct, viaResult bytes.Buffer + + p := velocity.NewPretty(&direct, velocity.ThemeNightOwl) + p.Box("Title", "some content\nsecond line") + + result := velocity.NewBox("Title", "some content\nsecond line", velocity.ThemeNightOwl) + if err := result.Render(&viaResult); err != nil { + t.Fatalf("Box.Render returned error: %v", err) + } + + if direct.String() != viaResult.String() { + t.Errorf("output mismatch:\n p.Box: %q\n Box.Render: %q", direct.String(), viaResult.String()) + } +} + +// TestTableResult_ParityWithPrettyTable verifies that Table.Render and Pretty.Table produce identical output. +func TestTableResult_ParityWithPrettyTable(t *testing.T) { + t.Parallel() + + headers := []string{"Name", "Value"} + rows := [][]string{{"alpha", "1"}, {"beta", "2"}} + + var direct, viaResult bytes.Buffer + + p := velocity.NewPretty(&direct, velocity.ThemeNightOwl) + p.Table(headers, rows) + + result := velocity.NewTable(headers, rows, velocity.ThemeNightOwl) + if err := result.Render(&viaResult); err != nil { + t.Fatalf("Table.Render returned error: %v", err) + } + + if direct.String() != viaResult.String() { + t.Errorf("output mismatch:\n p.Table: %q\n Table.Render:%q", direct.String(), viaResult.String()) + } +} + +// TestBannerResult_ParityWithPrettyBanner verifies that Banner.Render and Pretty.Banner produce identical output. +func TestBannerResult_ParityWithPrettyBanner(t *testing.T) { + t.Parallel() + + text := "Welcome\nTo Velocity" + + var direct, viaResult bytes.Buffer + + p := velocity.NewPretty(&direct, velocity.ThemeNightOwl) + p.Banner(text) + + result := velocity.NewBanner(text, velocity.ThemeNightOwl) + if err := result.Render(&viaResult); err != nil { + t.Fatalf("Banner.Render returned error: %v", err) + } + + if direct.String() != viaResult.String() { + t.Errorf("output mismatch:\n p.Banner: %q\n Banner.Render:%q", direct.String(), viaResult.String()) + } +} + +// TestTreeResult_ParityWithPrettyTree verifies that Tree.Render and Pretty.Tree produce identical output. +func TestTreeResult_ParityWithPrettyTree(t *testing.T) { + t.Parallel() + + nodes := []velocity.TreeItem{ + {Key: "root", Children: []velocity.TreeItem{ + {Key: "child1", Value: "v1"}, + {Key: "child2", Value: "v2"}, + }}, + } + + var direct, viaResult bytes.Buffer + + p := velocity.NewPretty(&direct, velocity.ThemeNightOwl) + p.Tree(nodes) + + result := velocity.NewTree(nodes, velocity.ThemeNightOwl) + if err := result.Render(&viaResult); err != nil { + t.Fatalf("Tree.Render returned error: %v", err) + } + + if direct.String() != viaResult.String() { + t.Errorf("output mismatch:\n p.Tree: %q\n Tree.Render:%q", direct.String(), viaResult.String()) + } +} + +// TestKeyValueResult_ParityWithPrettyKeyValue verifies that KeyValue.Render and Pretty.KeyValue produce identical output. +func TestKeyValueResult_ParityWithPrettyKeyValue(t *testing.T) { + t.Parallel() + + var direct, viaResult bytes.Buffer + + p := velocity.NewPretty(&direct, velocity.ThemeNightOwl) + p.KeyValue("version", "1.2.3") + + result := velocity.NewKeyValue("version", "1.2.3", velocity.ThemeNightOwl) + if err := result.Render(&viaResult); err != nil { + t.Fatalf("KeyValue.Render returned error: %v", err) + } + + if direct.String() != viaResult.String() { + t.Errorf("output mismatch:\n p.KeyValue: %q\n KeyValue.Render:%q", direct.String(), viaResult.String()) + } +} + +// TestSystemInfoResult_ParityWithPrettySystemInfo verifies that SystemInfo.Render and Pretty.SystemInfo produce identical output. +func TestSystemInfoResult_ParityWithPrettySystemInfo(t *testing.T) { + t.Parallel() + + info := &velocity.SystemInfoData{ + Title: "TestApp", + Version: "0.1.0", + Fields: []velocity.KeyValuePair{ + {Key: "env", Value: "test"}, + {Key: "region", Value: "ap-southeast-2"}, + }, + } + + var direct, viaResult bytes.Buffer + + p := velocity.NewPretty(&direct, velocity.ThemeNightOwl) + p.SystemInfo(info) + + result := velocity.NewSystemInfo(info, velocity.ThemeNightOwl) + if err := result.Render(&viaResult); err != nil { + t.Fatalf("SystemInfo.Render returned error: %v", err) + } + + if direct.String() != viaResult.String() { + t.Errorf("output mismatch:\n p.SystemInfo: %q\n SystemInfo.Render:%q", direct.String(), viaResult.String()) + } +} + +// TestTableResult_EmptyHeaders confirms that nil headers produce no output. +func TestTableResult_EmptyHeaders(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + result := velocity.NewTable(nil, nil, velocity.ThemeNightOwl) + if err := result.Render(&buf); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if buf.Len() != 0 { + t.Errorf("expected empty output for nil headers, got: %q", buf.String()) + } +} + +// TestSystemInfoResult_NilInfo confirms that nil SystemInfoData produces no output. +func TestSystemInfoResult_NilInfo(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + result := velocity.NewSystemInfo(nil, velocity.ThemeNightOwl) + if err := result.Render(&buf); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if buf.Len() != 0 { + t.Errorf("expected empty output for nil info, got: %q", buf.String()) + } +} + +// TestNewPrettyFromLogger_Concurrent verifies that concurrent pretty and logger calls don't +// interleave or race (run with -race to validate). +func TestNewPrettyFromLogger_Concurrent(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + + log := velocity.New( + velocity.WithConsoleOutput(&buf), + velocity.WithTheme(velocity.ThemeNightOwl), + ) + p := velocity.NewPrettyFromLogger(log) + + const goroutines = 20 + done := make(chan struct{}) + + for range goroutines / 2 { + go func() { + for range 5 { + log.Info("log line") + } + done <- struct{}{} + }() + } + + for range goroutines / 2 { + go func() { + for range 5 { + p.KeyValue("key", "val") + } + done <- struct{}{} + }() + } + + for range goroutines { + <-done + } + + // If we get here without data race or panic the test passes. + // Output must contain both kinds of content. + out := buf.String() + if out == "" { + t.Error("expected output in buffer") + } +} diff --git a/ringbuffer.go b/ringbuffer.go index f21e548..abae629 100644 --- a/ringbuffer.go +++ b/ringbuffer.go @@ -102,7 +102,9 @@ func NewRingBuffer(writer io.Writer, size int) *RingBuffer { // afterSequenceSpinHook is called in tests between the sequence-spin exit and // the CAS claim to simulate preemption and expose double-claim races. -var afterSequenceSpinHook func() +// Stored as an atomic pointer so concurrent test goroutines can read and clear +// it without a data race (package-level vars are shared across parallel tests). +var afterSequenceSpinHook atomic.Pointer[func()] // Write adds a log entry to the ring buffer. // Returns false if the buffer is full and the message was dropped. @@ -136,8 +138,8 @@ func (rb *RingBuffer) Write(data []byte) bool { // Allow tests to inject a pause between spin exit and the claim CAS, // reproducing the preemption window the fix targets. - if afterSequenceSpinHook != nil { - afterSequenceSpinHook() + if h := afterSequenceSpinHook.Load(); h != nil { + (*h)() } // Atomically claim the write section by advancing expected from head to diff --git a/ringbuffer_test.go b/ringbuffer_test.go index 7a34cdd..7fa2bed 100644 --- a/ringbuffer_test.go +++ b/ringbuffer_test.go @@ -77,7 +77,7 @@ func TestRingBuffer_WriterPreemptedBeforeClaim(t *testing.T) { t.Parallel() // Restore the hook after this test so parallel tests are unaffected. - t.Cleanup(func() { afterSequenceSpinHook = nil }) + t.Cleanup(func() { afterSequenceSpinHook.Store(nil) }) buf := &safeBuffer{} @@ -90,11 +90,12 @@ func TestRingBuffer_WriterPreemptedBeforeClaim(t *testing.T) { hookFired := make(chan struct{}) releaseA := make(chan struct{}) - afterSequenceSpinHook = func() { - afterSequenceSpinHook = nil // fire exactly once + fn := func() { + afterSequenceSpinHook.Store(nil) // fire exactly once close(hookFired) <-releaseA } + afterSequenceSpinHook.Store(&fn) var writerAOK atomic.Bool writerADone := make(chan struct{}) diff --git a/secure.go b/secure.go new file mode 100644 index 0000000..fd56586 --- /dev/null +++ b/secure.go @@ -0,0 +1,48 @@ +package velocity + +import "strings" + +const ( + secureOpen = "" + secureClose = "" +) + +// redactSecureTags replaces all ... spans in s with mark. +// Called only when entry.maybeSecure is true so the string scan is pay-for-use. +// One allocation per affected message (strings.Builder). +func redactSecureTags(s, mark string) string { + if !strings.Contains(s, secureOpen) { + return s + } + var b strings.Builder + for { + start := strings.Index(s, secureOpen) + if start < 0 { + b.WriteString(s) + break + } + b.WriteString(s[:start]) + s = s[start+len(secureOpen):] + end := strings.Index(s, secureClose) + if end < 0 { + // Unclosed tag — emit the mark and stop. + b.WriteString(mark) + break + } + b.WriteString(mark) + s = s[end+len(secureClose):] + } + return b.String() +} + +// stripSecureTags removes the and markers from s, leaving +// the content between them intact. Used by trusted TTY console writers to show +// plaintext while stripping the markup. +func stripSecureTags(s string) string { + if !strings.Contains(s, secureOpen) { + return s + } + s = strings.ReplaceAll(s, secureOpen, "") + s = strings.ReplaceAll(s, secureClose, "") + return s +} diff --git a/secure_test.go b/secure_test.go new file mode 100644 index 0000000..b322ab1 --- /dev/null +++ b/secure_test.go @@ -0,0 +1,463 @@ +package velocity + +import ( + "io" + "strings" + "testing" + "time" +) + +// ---- Field constructor tests ------------------------------------------------- + +func TestSecureField_PlainAndRedacted(t *testing.T) { + t.Parallel() + + f := Secure("token", "supersecret") + if f.Type != FieldTypeSecure { + t.Fatalf("expected FieldTypeSecure, got %v", f.Type) + } + if got := SecurePlain(f); got != "supersecret" { + t.Errorf("plain: want %q, got %q", "supersecret", got) + } + if got := SecureRedacted(f); got != redactedMark { + t.Errorf("redacted: want %q, got %q", redactedMark, got) + } +} + +func TestSecureField_WriteFormatted_Untrusted(t *testing.T) { + t.Parallel() + + f := Secure("token", "supersecret") + var buf strings.Builder + f.writeFormatted(&buf) + if got := buf.String(); got != redactedMark { + t.Errorf("untrusted default: want %q, got %q", redactedMark, got) + } +} + +func TestSecureField_WriteFormattedTrusted(t *testing.T) { + t.Parallel() + + f := Secure("token", "supersecret") + var buf strings.Builder + f.writeFormattedTrusted(&buf) + if got := buf.String(); got != "supersecret" { + t.Errorf("trusted: want %q, got %q", "supersecret", got) + } +} + +func TestSecureURLField_PasswordRedacted(t *testing.T) { + t.Parallel() + + const rawURL = "redis://user:secret@localhost:6379/0" //nolint:gosec // G101: test placeholder + f := SecureURL("redis", rawURL) + if f.Type != FieldTypeSecureURL { + t.Fatalf("expected FieldTypeSecureURL, got %v", f.Type) + } + plain := SecurePlain(f) + if plain != rawURL { + t.Errorf("plain: want original URL, got %q", plain) + } + redacted := SecureRedacted(f) + if strings.Contains(redacted, "secret") { + t.Errorf("redacted URL must not contain password: %q", redacted) + } + if !strings.Contains(redacted, "user") { + t.Errorf("redacted URL should preserve username: %q", redacted) + } + // The URL sentinel is "REDACTED" (no brackets) to avoid URL-encoding of [ and ]. + if !strings.Contains(redacted, "REDACTED") { + t.Errorf("redacted URL should contain REDACTED sentinel: %q", redacted) + } +} + +func TestSecureURLField_NoPassword(t *testing.T) { + t.Parallel() + + // URL without password — both forms should be identical. + f := SecureURL("db", "postgres://localhost:5432/mydb") + plain := SecurePlain(f) + redacted := SecureRedacted(f) + if plain != redacted { + t.Errorf("no-password URL: plain %q != redacted %q", plain, redacted) + } +} + +func TestSecureURLField_InvalidURL(t *testing.T) { + t.Parallel() + + // Invalid URLs fall back to treating the raw string as the plain form. + f := SecureURL("bad", "not a url ://") + if f.Type != FieldTypeSecureURL { + t.Fatalf("expected FieldTypeSecureURL, got %v", f.Type) + } + // Both forms should be non-empty; we just check it doesn't panic. + _ = SecurePlain(f) + _ = SecureRedacted(f) +} + +func TestRedactedField(t *testing.T) { + t.Parallel() + + f := Redacted("api_key") + if f.Type != FieldTypeRedacted { + t.Fatalf("expected FieldTypeRedacted, got %v", f.Type) + } + // Trusted writers still see [REDACTED] — Redacted is unconditional. + var buf strings.Builder + f.writeFormattedTrusted(&buf) + if got := buf.String(); got != redactedMark { + t.Errorf("trusted Redacted field must still show %q, got %q", redactedMark, got) + } +} + +func TestTruncatedField_Fits(t *testing.T) { + t.Parallel() + + f := Truncated("tok", "short", 16) + if f.Type != FieldTypeTruncated { + t.Fatalf("expected FieldTypeTruncated, got %v", f.Type) + } + var buf strings.Builder + f.writeFormatted(&buf) + if got := buf.String(); got != "short" { + t.Errorf("want %q, got %q", "short", got) + } +} + +func TestTruncatedField_Clipped(t *testing.T) { + t.Parallel() + + f := Truncated("tok", "Bearer eyJhbGciOiJSUzI1NiJ9.longpayload", 16) + var buf strings.Builder + f.writeFormatted(&buf) + got := buf.String() + if strings.Contains(got, "longpayload") { + t.Errorf("clipped value must not contain trimmed portion, got %q", got) + } + if !strings.HasSuffix(got, "…") { + t.Errorf("clipped value must end with ellipsis, got %q", got) + } +} + +func TestTruncatedField_ZeroMaxLen(t *testing.T) { + t.Parallel() + + f := Truncated("tok", "anything", 0) + var buf strings.Builder + f.writeFormatted(&buf) + // Zero maxLen returns empty. + if got := buf.String(); got != "" { + t.Errorf("zero maxLen: want empty, got %q", got) + } +} + +// ---- tag scanner tests --------------------------------------------- + +func TestRedactSecureTags_Replaced(t *testing.T) { + t.Parallel() + + cases := []struct { + in, mark, want string + }{ + { + in: "connecting to redis://user:pass@host", + mark: redactedMark, + want: "connecting to " + redactedMark, + }, + { + in: "a x b y c", + mark: "***", + want: "a *** b *** c", + }, + { + in: "no tags here", + mark: redactedMark, + want: "no tags here", + }, + { + // Unclosed tag — emit the mark and stop. + in: "prefix unclosed", + mark: redactedMark, + want: "prefix " + redactedMark, + }, + } + + for _, tc := range cases { + got := redactSecureTags(tc.in, tc.mark) + if got != tc.want { + t.Errorf("redactSecureTags(%q, %q) = %q, want %q", tc.in, tc.mark, got, tc.want) + } + } +} + +func TestStripSecureTags(t *testing.T) { + t.Parallel() + + cases := []struct { + in, want string + }{ + { + in: "connecting to redis://user:pass@host", + want: "connecting to redis://user:pass@host", + }, + { + in: "no tags", + want: "no tags", + }, + { + in: "only", + want: "only", + }, + } + + for _, tc := range cases { + got := stripSecureTags(tc.in) + if got != tc.want { + t.Errorf("stripSecureTags(%q) = %q, want %q", tc.in, tc.want, got) + } + } +} + +// ---- scanSecure flag auto-recompute tests ----------------------------------- + +func TestScanSecure_FalseWithSecureTagsDisabled(t *testing.T) { + t.Parallel() + + // WithSecureTags(false) keeps scanSecure permanently false. + l := New(WithSecureTags(false)) + if l.writers.scanSecure.Load() { + t.Error("expected scanSecure=false when WithSecureTags(false) is applied") + } +} + +func TestScanSecure_TrueWithNonTTYConsole(t *testing.T) { + t.Parallel() + + // safeBuffer is not a TTY, so the console writer is non-TTY — scan should be on. + cfg := defaultConfig() + cfg.ConsoleOutput = &safeBuffer{} // non-TTY + cfg.StructuredOutput = nil + l := newFromConfig(cfg) + if !l.writers.scanSecure.Load() { + t.Error("expected scanSecure=true for non-TTY console writer") + } +} + +func TestScanSecure_FalseWhenNoOutputs(t *testing.T) { + t.Parallel() + + // Nop logger — no writers at all, nothing to redact for. + l := New(WithNop()) + // scanSecure: JSON writer is io.Discard (cfg path gives nil jsonWriter), + // console is io.Discard (also nil). No writers — nothing to redact for. + // The nop logger has ConsoleOutput=io.Discard which newFromConfig skips + // (it checks != io.Discard), so consoleWriter == nil and jsonWriter == nil. + if l.writers.scanSecure.Load() { + t.Error("expected scanSecure=false for nop logger with no real writers") + } +} + +func TestScanSecure_TrueWhenJSONWriterPresent(t *testing.T) { + t.Parallel() + + // JSON writer is always untrusted — scan must be on. + cfg := defaultConfig() + cfg.ConsoleOutput = &safeBuffer{} + cfg.StructuredOutput = &safeBuffer{} + l := newFromConfig(cfg) + if !l.writers.scanSecure.Load() { + t.Error("expected scanSecure=true when JSON writer is present") + } +} + +func TestScanSecure_RecomputedOnAddRemoveWriter(t *testing.T) { + t.Parallel() + + // Start with a nop logger (no real writers, scanSecure=false). + l := New(WithNop()) + if l.writers.scanSecure.Load() { + t.Fatal("precondition: scanSecure should be false for nop logger") + } + + // Adding an untrusted writer must flip the flag. + l.AddWriter("sink", &NoOpWriter{}) + if !l.writers.scanSecure.Load() { + t.Error("expected scanSecure=true after AddWriter (untrusted)") + } + + // Adding a trusted writer alongside the untrusted one must leave flag true. + l.AddWriter("trusted-sink", &NoOpWriter{}, WriterTrusted()) + if !l.writers.scanSecure.Load() { + t.Error("scanSecure must stay true while untrusted writer exists") + } + + // Remove the untrusted writer — flag should drop back to false. + _ = l.RemoveWriter("sink") + if l.writers.scanSecure.Load() { + t.Error("expected scanSecure=false after removing the last untrusted writer") + } +} + +func TestScanSecure_WithSecureTagsFalse(t *testing.T) { + t.Parallel() + + // WithSecureTags(false) must keep scanSecure permanently false regardless of writers. + l := New(WithStructuredOutput(&safeBuffer{}), WithSecureTags(false)) + if l.writers.scanSecure.Load() { + t.Error("expected scanSecure=false when WithSecureTags(false) is set") + } + + // Adding an untrusted writer must NOT flip the flag. + l.AddWriter("sink", &NoOpWriter{}) + if l.writers.scanSecure.Load() { + t.Error("expected scanSecure=false after AddWriter when WithSecureTags(false)") + } +} + +// ---- Integration: trusted vs untrusted writer output ------------------------- + +func TestSecureField_TrustedWriterSeesPlaintext(t *testing.T) { + t.Parallel() + + trusted := &safeBuffer{} + untrusted := &safeBuffer{} + + l := New( + WithConsoleOutput(&safeBuffer{}), // discard console + WithStructuredOutput(untrusted), + ) + // Register a trusted additional writer. + trustedJSON := NewJSONWriter(trusted) + l.AddWriter("audit", trustedJSON, WriterTrusted()) + + l.Info("connecting", Secure("session", "abc123")) + waitFor(t, func() bool { + return trusted.Len() > 0 + }, 2*time.Second, 5*time.Millisecond, "trusted writer receives entry") + + if strings.Contains(untrusted.String(), "abc123") { + t.Error("untrusted writer must not contain plaintext: abc123") + } + if !strings.Contains(untrusted.String(), redactedMark) { + t.Errorf("untrusted writer must contain %q, got: %s", redactedMark, untrusted.String()) + } + if !strings.Contains(trusted.String(), "abc123") { + t.Errorf("trusted writer must contain plaintext abc123, got: %s", trusted.String()) + } +} + +func TestSecureField_RedactedIsAlwaysHidden(t *testing.T) { + t.Parallel() + + trusted := &safeBuffer{} + l := New(WithStructuredOutput(trusted)) + trustedJSON := NewJSONWriter(&safeBuffer{}) + l.AddWriter("trusted", trustedJSON, WriterTrusted()) + + l.Info("request", Redacted("api_key")) + waitFor(t, func() bool { + return trusted.Len() > 0 + }, 2*time.Second, 5*time.Millisecond, "structured writer receives entry") + + // Even on the trusted path, Redacted must never show plaintext. + if strings.Contains(trusted.String(), "plaintext") { + t.Error("Redacted field must never show plaintext") + } + if !strings.Contains(trusted.String(), redactedMark) { + t.Errorf("Redacted field must show %q, got: %s", redactedMark, trusted.String()) + } +} + +func TestSecureTag_UntrustedJSONRedacts(t *testing.T) { + t.Parallel() + + buf := &safeBuffer{} + l := New(WithStructuredOutput(buf)) + l.Info("endpoint: https://admin:pass@internal.host") + waitFor(t, func() bool { + return buf.Len() > 0 + }, 2*time.Second, 5*time.Millisecond, "JSON writer receives entry") + + out := buf.String() + if strings.Contains(out, "admin") || strings.Contains(out, "pass") { + t.Errorf("JSON writer must redact content, got: %s", out) + } + if !strings.Contains(out, redactedMark) { + t.Errorf("JSON writer must emit %q for content, got: %s", redactedMark, out) + } +} + +func TestSecureTag_WriterRedactionMark(t *testing.T) { + t.Parallel() + + buf := &safeBuffer{} + l := New(WithConsoleOutput(&safeBuffer{})) + l.AddWriter("sink", NewJSONWriter(buf), WriterRedactionMark("***HIDDEN***")) + l.Info("key: topsecret") + + waitFor(t, func() bool { + return buf.Len() > 0 + }, 2*time.Second, 5*time.Millisecond, "additional writer receives entry") + + out := buf.String() + if strings.Contains(out, "topsecret") { + t.Errorf("must redact with custom mark, got: %s", out) + } + if !strings.Contains(out, "***HIDDEN***") { + t.Errorf("must use custom redaction mark, got: %s", out) + } +} + +// TestSecureTag_ChildLoggerSeesWriterAddedAfterCreation is a regression test for the bug +// where child loggers created before AddWriter was called kept a stale scanSecure=false. +// Because scanSecure was per-Logger (copied at With() time), the child's flag was never +// updated when the parent gained an untrusted writer — so tags leaked in plaintext. +func TestSecureTag_ChildLoggerSeesWriterAddedAfterCreation(t *testing.T) { + t.Parallel() + + sink := &safeBuffer{} + + // Parent has only a console to io.Discard — no structured writer, so scanSecure=false. + parent := New(WithConsoleOutput(io.Discard)) + // Child is created before the untrusted writer is added. + child := parent.With(String("child", "yes")) + + // Now add an untrusted JSON writer to the parent. + parent.AddWriter("json", NewJSONWriter(sink)) + + // Child logs a message with a tag — it must NOT appear in plaintext. + child.Info("token secret") + + waitFor(t, func() bool { + return sink.Len() > 0 + }, 2*time.Second, 5*time.Millisecond, "json writer receives entry from child") + + out := sink.String() + if strings.Contains(out, "secret") { + t.Errorf("child logger leaked secure content; got: %s", out) + } + if !strings.Contains(out, redactedMark) { + t.Errorf("expected redaction mark %q in output; got: %s", redactedMark, out) + } +} + +// TestSecureTag_TrustedWriterAddedAfterChildDoesNotFlipScan verifies that adding a +// TRUSTED writer after child creation does not enable secure-tag scanning. Trusted +// writers see plaintext by design — no scan needed for their sake. +func TestSecureTag_TrustedWriterAddedAfterChildDoesNotFlipScan(t *testing.T) { + t.Parallel() + + // Start with no outputs — scanSecure must stay false. + parent := New(WithNop()) + child := parent.With(String("child", "yes")) + + trustedSink := &safeBuffer{} + parent.AddWriter("trusted", NewJSONWriter(trustedSink), WriterTrusted()) + + // Neither parent nor child should have scanSecure enabled: the only writer is trusted. + if parent.writers.scanSecure.Load() { + t.Error("parent: scanSecure must be false when only writer is trusted") + } + _ = child // child shares the same writerSet — same assertion holds +} diff --git a/slog/doc.go b/slog/doc.go deleted file mode 100644 index 501f671..0000000 --- a/slog/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package velocityslog bridges log/slog to a velocity Logger. -package velocityslog diff --git a/slogbridge/doc.go b/slogbridge/doc.go new file mode 100644 index 0000000..810a806 --- /dev/null +++ b/slogbridge/doc.go @@ -0,0 +1,2 @@ +// Package slogbridge bridges log/slog to a velocity Logger. +package slogbridge diff --git a/slog/handler.go b/slogbridge/handler.go similarity index 98% rename from slog/handler.go rename to slogbridge/handler.go index 24c00d5..e813a26 100644 --- a/slog/handler.go +++ b/slogbridge/handler.go @@ -1,4 +1,4 @@ -package velocityslog +package slogbridge import ( "context" @@ -6,7 +6,7 @@ import ( "strings" "time" - velocity "github.com/tensorfoundrylabs/velocity" + velocity "github.com/tensorfoundrylabs/velocity/v2" ) // Handler bridges log/slog to a velocity Logger. diff --git a/slog/handler_test.go b/slogbridge/handler_test.go similarity index 73% rename from slog/handler_test.go rename to slogbridge/handler_test.go index d4ce9d7..4894763 100644 --- a/slog/handler_test.go +++ b/slogbridge/handler_test.go @@ -1,27 +1,32 @@ -package velocityslog_test +package slogbridge_test import ( "bytes" "context" + "io" "log/slog" "strings" "sync" "testing" "time" - velocity "github.com/tensorfoundrylabs/velocity" - velocityslog "github.com/tensorfoundrylabs/velocity/slog" + velocity "github.com/tensorfoundrylabs/velocity/v2" + slogbridge "github.com/tensorfoundrylabs/velocity/v2/slogbridge" ) -// newTestLogger creates a logger writing JSON to buf for easy assertion. +// newTestLogger creates a logger writing to buf with colour disabled for easy assertion. func newTestLogger(buf *bytes.Buffer) *velocity.Logger { - return velocity.NewForTesting(buf) + return velocity.New( + velocity.WithConsoleOutput(buf), + velocity.WithColour(false), + velocity.WithLevel(velocity.LevelDebug), + ) } func TestSlogHandler_NilLogger(t *testing.T) { t.Parallel() - h := velocityslog.NewHandler(nil) + h := slogbridge.NewHandler(nil) ctx := context.Background() if h.Enabled(ctx, slog.LevelInfo) { @@ -42,7 +47,7 @@ func TestSlogHandler_BasicLogging(t *testing.T) { t.Parallel() buf := &bytes.Buffer{} l := newTestLogger(buf) - sl := velocityslog.NewLogger(l) + sl := slogbridge.NewLogger(l) sl.Info("hello world") out := buf.String() @@ -55,7 +60,7 @@ func TestSlogHandler_WithAttrs(t *testing.T) { t.Parallel() buf := &bytes.Buffer{} l := newTestLogger(buf) - sl := velocityslog.NewLogger(l) + sl := slogbridge.NewLogger(l) sl = sl.With("component", "auth", "version", 3) sl.Info("login") @@ -73,7 +78,7 @@ func TestSlogHandler_WithGroup(t *testing.T) { t.Parallel() buf := &bytes.Buffer{} l := newTestLogger(buf) - sl := velocityslog.NewLogger(l) + sl := slogbridge.NewLogger(l) sl = sl.WithGroup("server").With("host", "localhost") sl.Info("starting") @@ -92,7 +97,7 @@ func TestSlogHandler_LevelFiltering(t *testing.T) { buf := &bytes.Buffer{} l := newTestLogger(buf) l.SetLevel(velocity.LevelInfo) - sl := velocityslog.NewLogger(l) + sl := slogbridge.NewLogger(l) sl.Debug("should be filtered") @@ -110,7 +115,7 @@ func TestSlogHandler_FieldTypes(t *testing.T) { t.Parallel() buf := &bytes.Buffer{} l := newTestLogger(buf) - sl := velocityslog.NewLogger(l) + sl := slogbridge.NewLogger(l) now := time.Now() sl.Info("typed fields", @@ -132,9 +137,9 @@ func TestSlogHandler_FieldTypes(t *testing.T) { func TestSlogHandler_Enabled(t *testing.T) { t.Parallel() - l := velocity.New(nil) + l := velocity.New(velocity.WithNop()) l.SetLevel(velocity.LevelWarn) - h := velocityslog.NewHandler(l) + h := slogbridge.NewHandler(l) ctx := context.Background() if h.Enabled(ctx, slog.LevelDebug) { @@ -155,7 +160,7 @@ func TestSlogHandler_NestedGroups(t *testing.T) { t.Parallel() buf := &bytes.Buffer{} l := newTestLogger(buf) - sl := velocityslog.NewLogger(l) + sl := slogbridge.NewLogger(l) sl = sl.WithGroup("a").WithGroup("b") sl.Info("nested", slog.String("key", "val")) @@ -170,7 +175,7 @@ func TestSlogHandler_EmptyAttr(t *testing.T) { t.Parallel() buf := &bytes.Buffer{} l := newTestLogger(buf) - h := velocityslog.NewHandler(l) + h := slogbridge.NewHandler(l) // WithAttrs with empty slice should return same handler. h2 := h.WithAttrs(nil) @@ -196,7 +201,7 @@ func TestSlogHandler_ConcurrentHandle(t *testing.T) { buf := &bytes.Buffer{} l := newTestLogger(buf) - sl := velocityslog.NewLogger(l) + sl := slogbridge.NewLogger(l) var wg sync.WaitGroup for range 100 { @@ -209,9 +214,35 @@ func TestSlogHandler_ConcurrentHandle(t *testing.T) { wg.Wait() } +// TestSlogHandler_SecureTagsRedacted is a regression test for the bug where +// messages routed through slogbridge bypassed the tag scanner. When the +// output is untrusted (JSON writer), the tagged content must be redacted. +func TestSlogHandler_SecureTagsRedacted(t *testing.T) { + t.Parallel() + + var jsonBuf bytes.Buffer + l := velocity.New( + velocity.WithConsoleOutput(io.Discard), + velocity.WithStructuredOutput(&jsonBuf), + ) + sl := slogbridge.NewLogger(l) + + // The token between the tags must not appear in the JSON output. + sl.Info("token supersecret logged") + + out := jsonBuf.String() + if strings.Contains(out, "supersecret") { + t.Errorf("secure tag content leaked into JSON output: %q", out) + } + if !strings.Contains(out, "token") { + t.Errorf("expected message prefix 'token' in output, got: %q", out) + } +} + func BenchmarkSlogHandler_Info(b *testing.B) { - l := velocity.New(nil) // nil discards console output - sl := velocityslog.NewLogger(l) + // WithNop discards all output so I/O cost doesn't dominate the measurement. + l := velocity.New(velocity.WithNop()) + sl := slogbridge.NewLogger(l) b.ReportAllocs() b.ResetTimer() diff --git a/status.go b/status.go new file mode 100644 index 0000000..0953f84 --- /dev/null +++ b/status.go @@ -0,0 +1,284 @@ +package velocity + +import ( + "bytes" + "io" +) + +// StatusKind identifies the semantic outcome of an operation for StatusItem rendering. +type StatusKind uint8 + +const ( + StatusOK StatusKind = iota // positive outcome + StatusFail // failure / error + StatusWarn // degraded / warning + StatusInfo // informational + StatusPending // in-progress / not yet resolved + StatusSkipped // intentionally bypassed +) + +// String returns the badge label for a StatusKind. All labels are 4 chars so +// badges render at uniform width matching the level badges ([INFO], [WARN]). +// JSON output uses statusJSONValue() instead, which returns canonical lowercase +// names (ok, pending, etc.) for queryability. +func (k StatusKind) String() string { + switch k { + case StatusOK: + return "OKAY" + case StatusFail: + return "FAIL" + case StatusWarn: + return "WARN" + case StatusInfo: + return "INFO" + case StatusPending: + return "WAIT" + case StatusSkipped: + return "SKIP" + default: + return "INFO" + } +} + +// statusJSONValue returns the lowercase JSON field value for the status. +func (k StatusKind) statusJSONValue() string { + switch k { + case StatusOK: + return statusJSONOK + case StatusFail: + return statusJSONFail + case StatusWarn: + return statusJSONWarn + case StatusInfo: + return statusJSONInfo + case StatusPending: + return statusJSONPending + case StatusSkipped: + return statusJSONSkip + default: + return statusJSONInfo + } +} + +// Slot maps a StatusKind to the theme StyleSlot used to colour the badge text. +// StatusPending reuses SlotStatusInfo (no dedicated slot — close semantic fit). +// StatusSkipped reuses SlotMuted (de-emphasised, intentionally bypassed). +func (k StatusKind) Slot() StyleSlot { + switch k { + case StatusOK: + return SlotStatusOK + case StatusFail: + return SlotStatusFail + case StatusWarn: + return SlotStatusWarn + case StatusInfo: + return SlotStatusInfo + case StatusPending: + return SlotStatusInfo + case StatusSkipped: + return SlotMuted + default: + return SlotStatusInfo + } +} + +// statusJSONOK etc. are kept as constants to satisfy the goconst linter — these +// values appear across String(), statusJSONValue(), and Slot() switch arms. +const ( + statusJSONOK = "ok" + statusJSONFail = "fail" + statusJSONWarn = "warn" + statusJSONInfo = "info" + statusJSONPending = "pending" + statusJSONSkip = "skip" +) + +// statusKindNone is the sentinel stored in Entry.statusKind when no Status call was made. +// Using 0xFF rather than a zero value lets StatusOK (0) remain a valid status. +// The sentinel is package-private — callers never see it. +const statusKindNone StatusKind = 0xFF + +// statusBadgeSep is the single space between the badge and the message text, +// matching the level-badge spacing in the standard log line template. +const statusBadgeSep = " " + +// StatusItem is a Renderable that displays an outcome badge followed by a message +// and optional structured fields. On TTY it renders a coloured [OKAY] style badge; +// on non-TTY the badge becomes plain text — no ANSI escapes in pipes or files. +// TTY detection happens at Render time via IsTerminalWriter, so the same StatusItem +// may be rendered to both a terminal and a file correctly. +type StatusItem struct { + theme *Theme + msg string + fields []Field + kind StatusKind +} + +// NewStatusItem constructs a StatusItem. theme may be nil (falls back to ThemeNightOwl). +// TTY detection is deferred to Render time — callers do not need to pass isTTY. +func NewStatusItem(kind StatusKind, msg string, theme *Theme, fields ...Field) *StatusItem { + if theme == nil { + theme = ThemeNightOwl + } + s := &StatusItem{ + kind: kind, + msg: msg, + theme: theme, + fields: make([]Field, len(fields)), + } + copy(s.fields, fields) + return s +} + +// Render writes the status item to w. TTY is detected from w at call time: +// when w is a real terminal the coloured badge form is used; otherwise plain text. +// The trailing newline is always written so consecutive StatusItems align without +// the caller having to manage spacing. +// +// Note: when called via Logger.Render the writer is an intermediate buffer, not the +// final output sink. Logger.Render detects this and calls RenderTTY instead, passing +// the console writer's resolved TTY state. Callers that hold the writer directly +// (e.g. writing a StatusItem directly to os.Stdout) should call RenderTTY and pass +// IsTerminalWriter(w) themselves if they need accurate TTY detection on Windows. +func (s *StatusItem) Render(w io.Writer) error { + if s == nil { + return nil + } + return s.RenderTTY(w, IsTerminalWriter(w)) +} + +// RenderTTY writes the status item to w with explicit TTY state. Use this instead +// of Render when the caller already knows the TTY state of the destination (e.g. +// Logger.Render passes the console writer's resolved isTTY flag so that FORCE_COLOR +// and fd detection are respected even though the intermediate writer is a buffer). +func (s *StatusItem) RenderTTY(w io.Writer, isTTY bool) error { + if s == nil { + return nil + } + + var buf bytes.Buffer + if isTTY { + renderStatusItemTTY(&buf, s.kind, s.msg, s.theme, s.fields) + } else { + renderStatusItemPlain(&buf, s.kind, s.msg, s.fields) + } + _, err := w.Write(buf.Bytes()) + return err +} + +// String renders the status item to a string. Useful in tests and for capture. +func (s *StatusItem) String() string { + if s == nil { + return "" + } + var buf bytes.Buffer + _ = s.Render(&buf) + return buf.String() +} + +// renderStatusItemTTY builds the ANSI badge line into buf. +// Format: '[' + + ']' + " " + message + fields +func renderStatusItemTTY(buf *bytes.Buffer, kind StatusKind, msg string, theme *Theme, fields []Field) { + token := kind.String() + slot := kind.Slot() + + // Unstyled left bracket. + buf.WriteByte('[') + + // Coloured token, no padding — width follows the natural token length. + prefix, suffix := theme.Wrap(slot) + buf.WriteString(prefix) + buf.WriteString(token) + buf.WriteString(suffix) + + // Unstyled right bracket + separator. + buf.WriteByte(']') + buf.WriteString(statusBadgeSep) + + // Message. + msgCode := theme.CachedMessageFg() + if msgCode != "" { + buf.WriteString(msgCode) + } + buf.WriteString(msg) + if msgCode != "" { + buf.WriteString(theme.ResetStr()) + } + + // Fields rendered inline with key/value colours from the theme. + // TTY console writers are trusted — Secure fields show plaintext on terminal. + writeStatusFields(buf, fields, theme, true, true) + + buf.WriteByte('\n') +} + +// renderStatusItemPlain builds the non-ANSI form. The badge becomes plain text +// so the output is grep-friendly in pipes and log files. +func renderStatusItemPlain(buf *bytes.Buffer, kind StatusKind, msg string, fields []Field) { + token := kind.String() + buf.WriteByte('[') + buf.WriteString(token) + buf.WriteByte(']') + buf.WriteString(statusBadgeSep) + buf.WriteString(msg) + // Non-TTY output is untrusted — Secure fields are redacted. + writeStatusFields(buf, fields, nil, false, false) + buf.WriteByte('\n') +} + +// writeStatusFields appends inline key=value pairs to buf. +// Strings and errors are quoted for readability; numerics are written raw. +// When themed and useColours is true, field keys and values are coloured. +// trusted controls whether Secure/SecureURL field plaintext is shown; +// pass isTTY for console output (matches the trust model used by ConsoleWriter). +func writeStatusFields(buf *bytes.Buffer, fields []Field, theme *Theme, useColours, trusted bool) { + for _, f := range fields { + buf.WriteByte(' ') + + keyCode := "" + valCode := "" + if useColours && theme != nil { + keyCode = theme.CachedFieldKeyFg() + if f.Type == FieldTypeError { + valCode = theme.cachedErrorValFgStr() + } else { + valCode = theme.CachedFieldValFg() + } + } + + if keyCode != "" { + buf.WriteString(keyCode) + } + buf.WriteString(f.Key) + if keyCode != "" { + buf.WriteString(theme.ResetStr()) + } + + buf.WriteByte('=') + + if valCode != "" { + buf.WriteString(valCode) + } + + // Quote string-like types to match the console writer convention. + // Secure fields use the trust-aware path: plaintext on TTY, redacted otherwise. + switch f.Type { + case FieldTypeString, FieldTypeError, FieldTypeStringer, FieldTypeTruncated: + buf.WriteByte('"') + f.writeFormatted(buf) + buf.WriteByte('"') + case FieldTypeSecure, FieldTypeSecureURL: + if trusted { + f.writeFormattedTrusted(buf) + } else { + f.writeFormatted(buf) + } + default: + f.writeFormatted(buf) + } + + if valCode != "" { + buf.WriteString(theme.ResetStr()) + } + } +} diff --git a/status_test.go b/status_test.go new file mode 100644 index 0000000..08ec039 --- /dev/null +++ b/status_test.go @@ -0,0 +1,471 @@ +package velocity + +import ( + "bytes" + "io" + "strings" + "testing" +) + +// --- StatusKind.String() --- + +func TestStatusKindString(t *testing.T) { + t.Parallel() + + cases := []struct { + kind StatusKind + want string + }{ + {StatusOK, "OKAY"}, + {StatusFail, "FAIL"}, + {StatusWarn, "WARN"}, + {StatusInfo, "INFO"}, + {StatusPending, "WAIT"}, + {StatusSkipped, "SKIP"}, + // Unknown value falls back to "INFO". + {StatusKind(200), "INFO"}, + } + + for _, tc := range cases { + t.Run(tc.want, func(t *testing.T) { + t.Parallel() + if got := tc.kind.String(); got != tc.want { + t.Errorf("StatusKind(%d).String() = %q, want %q", tc.kind, got, tc.want) + } + }) + } +} + +// --- StatusKind.Slot() --- + +func TestStatusKindSlot(t *testing.T) { + t.Parallel() + + cases := []struct { + kind StatusKind + want StyleSlot + }{ + {StatusOK, SlotStatusOK}, + {StatusFail, SlotStatusFail}, + {StatusWarn, SlotStatusWarn}, + {StatusInfo, SlotStatusInfo}, + // Pending reuses Info slot (no dedicated slot). + {StatusPending, SlotStatusInfo}, + // Skipped reuses Muted slot (de-emphasised / not an outcome). + {StatusSkipped, SlotMuted}, + } + + for _, tc := range cases { + t.Run(tc.kind.String(), func(t *testing.T) { + t.Parallel() + if got := tc.kind.Slot(); got != tc.want { + t.Errorf("StatusKind(%d).Slot() = %v, want %v", tc.kind, got, tc.want) + } + }) + } +} + +// --- StatusItem.Render (TTY path, tested via internal helper) --- + +// TestStatusItemRenderTTY exercises the TTY render path via renderStatusItemTTY +// directly, since bytes.Buffer is not a terminal and Render(w) auto-detects TTY +// from w. ThemeMono is used so assertions don't need to strip ANSI codes. +func TestStatusItemRenderTTY(t *testing.T) { + t.Parallel() + + item := NewStatusItem(StatusOK, "user signed in", ThemeMono, + Int("user_id", 42), + Duration("took", 18*1000*1000), // 18ms + ) + + var buf bytes.Buffer + renderStatusItemTTY(&buf, item.kind, item.msg, item.theme, item.fields) + + out := buf.String() + // Badge must be present with correct token. + if !strings.Contains(out, "[OKAY]") { + t.Errorf("expected badge [OKAY], got: %q", out) + } + if !strings.Contains(out, "user signed in") { + t.Errorf("expected message in output, got: %q", out) + } + if !strings.Contains(out, "user_id=42") { + t.Errorf("expected user_id field in output, got: %q", out) + } +} + +// TestStatusItemBadgeCompact verifies each status kind produces its own compact +// bracketed token without padding (e.g. [OKAY] not [OK ]). Variable widths are +// expected and intentional, matching the level-badge style. +func TestStatusItemBadgeCompact(t *testing.T) { + t.Parallel() + + cases := map[StatusKind]string{ + StatusOK: "[OKAY]", + StatusFail: "[FAIL]", + StatusWarn: "[WARN]", + StatusInfo: "[INFO]", + StatusPending: "[WAIT]", + StatusSkipped: "[SKIP]", + } + for k, want := range cases { + item := NewStatusItem(k, "msg", ThemeMono) + var buf bytes.Buffer + renderStatusItemTTY(&buf, item.kind, item.msg, item.theme, item.fields) + if !strings.Contains(buf.String(), want) { + t.Errorf("kind %s: expected badge %q in output, got %q", k.String(), want, buf.String()) + } + } +} + +// --- StatusItem.Render (plain / non-TTY path) --- + +// TestStatusItemRenderPlain exercises the non-TTY render path. bytes.Buffer is +// not a terminal so Render(w) uses the plain form automatically. +func TestStatusItemRenderPlain(t *testing.T) { + t.Parallel() + + item := NewStatusItem(StatusFail, "payment refused", ThemeMono, + String("reason", "card expired"), + ) + + var buf bytes.Buffer + if err := item.Render(&buf); err != nil { + t.Fatalf("Render() error: %v", err) + } + + out := buf.String() + if !strings.Contains(out, "[FAIL]") { + t.Errorf("expected badge [FAIL], got: %q", out) + } + if !strings.Contains(out, "payment refused") { + t.Errorf("expected message in output, got: %q", out) + } + if !strings.Contains(out, `reason="card expired"`) { + t.Errorf("expected reason field in output, got: %q", out) + } +} + +// --- StatusItem.String() --- + +func TestStatusItemString(t *testing.T) { + t.Parallel() + + item := NewStatusItem(StatusWarn, "slow query", ThemeMono) + s := item.String() + if !strings.Contains(s, "slow query") { + t.Errorf("String() missing message: %q", s) + } + // String() calls Render(&bytes.Buffer) — non-TTY path — badge still present. + if !strings.Contains(s, "[WARN]") { + t.Errorf("String() missing badge: %q", s) + } +} + +// --- StatusItem: nil receiver --- + +func TestStatusItemNilReceiver(t *testing.T) { + t.Parallel() + + var item *StatusItem + if s := item.String(); s != "" { + t.Errorf("nil.String() = %q, want empty", s) + } + var buf bytes.Buffer + if err := item.Render(&buf); err != nil { + t.Errorf("nil.Render() error: %v", err) + } +} + +// --- StatusItem: no fields --- + +func TestStatusItemNoFields(t *testing.T) { + t.Parallel() + + item := NewStatusItem(StatusPending, "waiting for upstream", ThemeMono) + var buf bytes.Buffer + _ = item.Render(&buf) + out := buf.String() + if !strings.Contains(out, "waiting for upstream") { + t.Errorf("expected message: %q", out) + } + // No stray field separators. + if strings.Contains(out, "=") { + t.Errorf("unexpected '=' (field) in no-field output: %q", out) + } +} + +// --- StatusItem: five fields --- + +func TestStatusItemFiveFields(t *testing.T) { + t.Parallel() + + item := NewStatusItem(StatusInfo, "ready", ThemeMono, + String("svc", "auth"), + Int("port", 8080), + Bool("tls", true), + Duration("startup", 120*1000*1000), + String("env", "prod"), + ) + var buf bytes.Buffer + _ = item.Render(&buf) + out := buf.String() + for _, want := range []string{"svc=", "port=", "tls=", "startup=", "env="} { + if !strings.Contains(out, want) { + t.Errorf("expected field %q in output: %q", want, out) + } + } +} + +// --- Logger.Status routing: console writer --- + +func TestLoggerStatusConsole(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(&buf), + WithColour(false), + ) + + log.Status(LevelInfo, StatusOK, "database connected", String("host", "localhost")) + + out := buf.String() + // Status renders inline via Render — expect the badge and message. + if !strings.Contains(out, "database connected") { + t.Errorf("expected message in console output: %q", out) + } + if !strings.Contains(out, "host") { + t.Errorf("expected host field in console output: %q", out) + } + // Badge must be present — Render uses the plain form on non-TTY. + if !strings.Contains(out, "[OKAY]") { + t.Errorf("expected badge [OKAY] in console output: %q", out) + } +} + +// --- Logger.Status routing: JSON writer --- + +func TestLoggerStatusJSON(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + + log.Status(LevelInfo, StatusOK, "service started", Int("pid", 1234)) + + out := buf.String() + if !strings.Contains(out, `"status":"ok"`) { + t.Errorf("expected status field in JSON output: %q", out) + } + if !strings.Contains(out, `"message":"service started"`) { + t.Errorf("expected message in JSON output: %q", out) + } + if !strings.Contains(out, `"pid":1234`) { + t.Errorf("expected pid field in JSON output: %q", out) + } +} + +// TestLoggerStatusAllKindsJSON ensures all six kinds serialise correctly as JSON. +func TestLoggerStatusAllKindsJSON(t *testing.T) { + t.Parallel() + + cases := []struct { + kind StatusKind + wantField string + }{ + {StatusOK, `"status":"ok"`}, + {StatusFail, `"status":"fail"`}, + {StatusWarn, `"status":"warn"`}, + {StatusInfo, `"status":"info"`}, + {StatusPending, `"status":"pending"`}, + {StatusSkipped, `"status":"skip"`}, + } + + for _, tc := range cases { + t.Run(tc.kind.String(), func(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + log.Status(LevelInfo, tc.kind, "test", String("k", "v")) + out := buf.String() + if !strings.Contains(out, tc.wantField) { + t.Errorf("kind %s: want %q in %q", tc.kind.String(), tc.wantField, out) + } + }) + } +} + +// --- Logger.Status: nil logger fallback --- + +func TestLoggerStatusNil(t *testing.T) { + t.Parallel() + + // Must not panic. Output goes to stderr; we can't capture it here but the + // nil path is safe. + var log *Logger + log.Status(LevelInfo, StatusOK, "should not panic") +} + +// --- Logger.Status: level filtering --- + +func TestLoggerStatusLevelFilter(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithStructuredOutput(&buf), + // Raise both levels so Info entries are dropped. + WithLevel(LevelError), + WithStructuredLevel(LevelError), + ) + + // Status at Info should be filtered out. + log.Status(LevelInfo, StatusOK, "this should not appear") + + if out := buf.String(); out != "" { + t.Errorf("expected no output when filtered, got: %q", out) + } +} + +// TestStatusJSONNoMessageBadge ensures the badge text never appears in the JSON message. +func TestStatusJSONNoMessageBadge(t *testing.T) { + t.Parallel() + + var buf safeBuffer + log := New( + WithConsoleOutput(io.Discard), + WithStructuredOutput(&buf), + ) + log.Status(LevelInfo, StatusFail, "clean message", String("code", "404")) + out := buf.String() + + // The FAIL token must not be embedded in the message string. + if strings.Contains(out, `"message":"[FAIL`) { + t.Errorf("badge text leaked into JSON message: %q", out) + } +} + +// --- Logger.Status: sampler gate --- + +// TestLoggerStatus_SamplerGate verifies that the sampler runs before the console +// render path, not just before the structured path. A sampled-out call must produce +// no console output at all. +func TestLoggerStatus_SamplerGate(t *testing.T) { + t.Parallel() + + var consoleBuf safeBuffer + // CountSampler(5, 0): allow first 5, then drop everything. + log := New( + WithConsoleOutput(&consoleBuf), + WithColour(false), + WithSampler(NewCountSampler(5, 0)), + ) + defer func() { _ = log.Close() }() + + // Log 10 status calls — only the first 5 should produce output. + for range 10 { + log.Status(LevelInfo, StatusOK, "sampled") + } + + out := consoleBuf.String() + count := strings.Count(out, "[OKAY]") + if count > 5 { + t.Errorf("sampler did not gate Status console output: got %d badges, want ≤5", count) + } + if count == 0 { + t.Error("expected at least some Status output before sampler kicked in, got none") + } +} + +// TestLoggerStatus_BaseFieldsPropagated verifies that baseFields set via With() +// appear in both the console badge line and the JSON structured record. +func TestLoggerStatus_BaseFieldsPropagated(t *testing.T) { + t.Parallel() + + var consoleBuf safeBuffer + var jsonBuf safeBuffer + + parent := New( + WithConsoleOutput(&consoleBuf), + WithColour(false), + WithStructuredOutput(&jsonBuf), + ) + defer func() { _ = parent.Close() }() + + child := parent.With(String("request_id", "req-123")) + child.Status(LevelInfo, StatusOK, "processed") + + // Console output must include the base field. + consoleOut := consoleBuf.String() + if !strings.Contains(consoleOut, "request_id") { + t.Errorf("base field missing from Status console output: %q", consoleOut) + } + + // JSON output must also include the base field. + jsonOut := jsonBuf.String() + if !strings.Contains(jsonOut, "request_id") { + t.Errorf("base field missing from Status JSON output: %q", jsonOut) + } +} + +// TestStatusItem_SecureFieldRedactedOnNonTTY verifies that Secure fields passed to a +// StatusItem are redacted when rendered via renderStatusItemPlain (non-TTY path), +// and shown as plaintext via renderStatusItemTTY (trusted TTY path). +// Regression guard for the bug where writeStatusFields always called writeFormatted +// regardless of trust, causing Secure fields to be redacted on trusted TTY output too. +func TestStatusItem_SecureFieldRedactedOnNonTTY(t *testing.T) { + t.Parallel() + + item := NewStatusItem(StatusOK, "login", ThemeNightOwl, Secure("password", "hunter2")) + + // Non-TTY (plain) path: Secure field must be redacted. + var plain strings.Builder + _ = item.RenderTTY(&plain, false) + plainOut := plain.String() + if strings.Contains(plainOut, "hunter2") { + t.Errorf("Secure field plaintext leaked in plain Status render: %q", plainOut) + } + if !strings.Contains(plainOut, "[REDACTED]") { + t.Errorf("expected [REDACTED] in plain Status render, got: %q", plainOut) + } + + // TTY (trusted) path: Secure field must show plaintext. + var tty strings.Builder + _ = item.RenderTTY(&tty, true) + ttyOut := tty.String() + if !strings.Contains(ttyOut, "hunter2") { + t.Errorf("Secure field plaintext missing from trusted TTY Status render: %q", ttyOut) + } +} + +// TestLoggerStatus_SecureTagRedactedOnNonTTY verifies that tags in the +// Status message are redacted when the console writer is non-TTY (a bytes.Buffer). +func TestLoggerStatus_SecureTagRedactedOnNonTTY(t *testing.T) { + t.Parallel() + + var consoleBuf safeBuffer + log := New( + WithConsoleOutput(&consoleBuf), + WithColour(false), + WithStructuredOutput(io.Discard), + ) + defer func() { _ = log.Close() }() + + log.Status(LevelInfo, StatusOK, "token supersecret ok") + + out := consoleBuf.String() + if strings.Contains(out, "supersecret") { + t.Errorf("secure tag content leaked in non-TTY console output: %q", out) + } + if !strings.Contains(out, "[REDACTED]") { + t.Errorf("expected [REDACTED] in non-TTY console output, got: %q", out) + } +} diff --git a/template.go b/template.go index 6684fd0..4ce40d2 100644 --- a/template.go +++ b/template.go @@ -103,8 +103,8 @@ func initTemplate(t *Template) *Template { return t } -// buildWithTimezone converts UTC timestamps to the display timezone before rendering. -func (t *Template) buildWithTimezone(buf *bytes.Buffer, entry *Entry, theme *Theme, displayTimezone *time.Location) { +// buildWithTimezoneSecure is the trust-aware template rendering path. +func (t *Template) buildWithTimezoneSecure(buf *bytes.Buffer, entry *Entry, theme *Theme, displayTimezone *time.Location, trusted bool, redactionMark string) { if t.showTime && !entry.Time.IsZero() { t.writeTimestampWithTimezone(buf, entry, theme, displayTimezone) } @@ -117,13 +117,13 @@ func (t *Template) buildWithTimezone(buf *bytes.Buffer, entry *Entry, theme *The if buf.Len() > 0 { _ = buf.WriteByte(' ') } - t.writeMessage(buf, entry, theme) + t.writeMessageSecure(buf, entry, theme, trusted, redactionMark) } if entry.Caller != "" { _ = buf.WriteByte(' ') if t.useColours && theme != nil { - buf.WriteString(theme.cachedTimestampFg) + buf.WriteString(theme.cachedTimestampFgStr()) } buf.WriteString(entry.Caller) _ = buf.WriteByte(':') @@ -138,7 +138,7 @@ func (t *Template) buildWithTimezone(buf *bytes.Buffer, entry *Entry, theme *The if buf.Len() > 0 { _ = buf.WriteByte(' ') } - t.writeFields(buf, entry, theme) + t.writeFieldsSecure(buf, entry, theme, trusted, redactionMark) } if buf.Len() == 0 || buf.Bytes()[buf.Len()-1] != '\n' { @@ -148,7 +148,7 @@ func (t *Template) buildWithTimezone(buf *bytes.Buffer, entry *Entry, theme *The func (t *Template) writeTimestampWithTimezone(buf *bytes.Buffer, entry *Entry, theme *Theme, displayTimezone *time.Location) { if t.useColours && theme != nil { - buf.WriteString(theme.cachedTimestampFg) + buf.WriteString(theme.cachedTimestampFgStr()) } displayTime := entry.Time.In(displayTimezone) @@ -166,10 +166,7 @@ func (t *Template) writeLevel(buf *bytes.Buffer, entry *Entry, theme *Theme) { var levelCode string if t.useColours && theme != nil { - lvl := entry.Level - if lvl >= 0 && int(lvl) < len(theme.cachedLevelFg) { - levelCode = theme.cachedLevelFg[lvl] - } + levelCode = theme.cachedLevelCode(entry.Level) } switch t.levelStyle { @@ -204,35 +201,43 @@ func (t *Template) writeLevel(buf *bytes.Buffer, entry *Entry, theme *Theme) { } } -func (t *Template) writeMessage(buf *bytes.Buffer, entry *Entry, theme *Theme) { +func (t *Template) writeMessageSecure(buf *bytes.Buffer, entry *Entry, theme *Theme, trusted bool, redactionMark string) { if t.useColours && theme != nil { - buf.WriteString(theme.cachedMessageFg) + buf.WriteString(theme.cachedMessageFgStr()) } - buf.WriteString(entry.Message) + msg := entry.Message + if entry.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + buf.WriteString(msg) if t.useColours && theme != nil { buf.WriteString(Reset) } } -func (t *Template) writeFields(buf *bytes.Buffer, entry *Entry, theme *Theme) { +func (t *Template) writeFieldsSecure(buf *bytes.Buffer, entry *Entry, theme *Theme, trusted bool, redactionMark string) { // Check if tree display is forced on the entry, otherwise use template's mode if entry.forceTreeDisplay || t.fieldDisplayMode == FieldDisplayTree { - t.writeFieldsTree(buf, entry, theme) + t.writeFieldsTreeSecure(buf, entry, theme, trusted, redactionMark) } else { - t.writeFieldsInline(buf, entry, theme) + t.writeFieldsInlineSecure(buf, entry, theme, trusted, redactionMark) } } -func (t *Template) writeFieldsInline(buf *bytes.Buffer, entry *Entry, theme *Theme) { +func (t *Template) writeFieldsInlineSecure(buf *bytes.Buffer, entry *Entry, theme *Theme, trusted bool, redactionMark string) { for i, field := range entry.Fields { if i > 0 { buf.WriteString(t.fieldSep) } if t.useColours && theme != nil { - buf.WriteString(theme.cachedFieldKeyFg) + buf.WriteString(theme.cachedFieldKeyFgStr()) } buf.WriteString(field.Key) if t.useColours && theme != nil { @@ -242,12 +247,16 @@ func (t *Template) writeFieldsInline(buf *bytes.Buffer, entry *Entry, theme *The buf.WriteString(t.fieldPairSep) if t.useColours && field.Type == FieldTypeError && theme != nil { - buf.WriteString(theme.cachedErrorValFg) + buf.WriteString(theme.cachedErrorValFgStr()) } else if t.useColours && theme != nil { - buf.WriteString(theme.cachedFieldValFg) + buf.WriteString(theme.cachedFieldValFgStr()) } - field.writeFormatted(buf) + if trusted { + field.writeFormattedTrusted(buf) + } else { + field.writeFormattedWithMark(buf, redactionMark) + } if t.useColours && theme != nil { buf.WriteString(Reset) @@ -255,8 +264,7 @@ func (t *Template) writeFieldsInline(buf *bytes.Buffer, entry *Entry, theme *The } } -// writeFieldsTree renders fields in a tree structure aligned with the message. -func (t *Template) writeFieldsTree(buf *bytes.Buffer, entry *Entry, theme *Theme) { +func (t *Template) writeFieldsTreeSecure(buf *bytes.Buffer, entry *Entry, theme *Theme, trusted bool, redactionMark string) { // Badge style has a fixed prefix width regardless of level, so we use the // pre-built indent string. Other styles vary by level and must compute per call. var indentStr string @@ -280,7 +288,7 @@ func (t *Template) writeFieldsTree(buf *bytes.Buffer, entry *Entry, theme *Theme buf.WriteString(treeChar) if t.useColours && theme != nil { - buf.WriteString(theme.cachedFieldKeyFg) + buf.WriteString(theme.cachedFieldKeyFgStr()) } buf.WriteString(field.Key) if t.useColours && theme != nil { @@ -290,12 +298,16 @@ func (t *Template) writeFieldsTree(buf *bytes.Buffer, entry *Entry, theme *Theme buf.WriteString(t.fieldPairSep) if t.useColours && field.Type == FieldTypeError && theme != nil { - buf.WriteString(theme.cachedErrorValFg) + buf.WriteString(theme.cachedErrorValFgStr()) } else if t.useColours && theme != nil { - buf.WriteString(theme.cachedFieldValFg) + buf.WriteString(theme.cachedFieldValFgStr()) } - field.writeFormatted(buf) + if trusted { + field.writeFormattedTrusted(buf) + } else { + field.writeFormattedWithMark(buf, redactionMark) + } if t.useColours && theme != nil { buf.WriteString(Reset) diff --git a/testutil_test.go b/testutil_test.go index 1f523e4..2bbf61f 100644 --- a/testutil_test.go +++ b/testutil_test.go @@ -2,11 +2,32 @@ package velocity import ( "bytes" + "io" "sync" "testing" "time" ) +// newForTesting builds a logger that writes to w with colour disabled, debug +// level, and no structured output. Used in tests that need a real console writer +// without a *testing.T to hand (use WithTesting(t) when t is available). +func newForTesting(w io.Writer) *Logger { + if w == nil { + w = io.Discard + } + cfg := defaultConfig() + cfg.ConsoleOutput = w + cfg.ConsoleTheme = nil + cfg.ConsoleLevel = LevelDebug + cfg.StructuredOutput = nil + cfg.StructuredLevel = LevelOff + cfg.BufferSize = 512 + cfg.FieldPoolSize = 25 + cfg.DisableColour = true + cfg.TimeFormat = "15:04:05.000" + return newFromConfig(cfg) +} + // waitFor polls condition until it returns true or timeout expires. func waitFor(t *testing.T, condition func() bool, timeout, interval time.Duration, msg string) { t.Helper() diff --git a/theme.go b/theme.go index d38cad2..4914068 100644 --- a/theme.go +++ b/theme.go @@ -1,22 +1,20 @@ package velocity -import "sync" +import "io" +// Colour represents an ANSI terminal colour, either 256-colour or true-colour RGB. type Colour struct { colour256 int r, g, b uint8 isRGB bool } +// RGB constructs a true-colour (24-bit) Colour value. func RGB(r, g, b uint8) Colour { - return Colour{ - r: r, - g: g, - b: b, - isRGB: true, - } + return Colour{r: r, g: g, b: b, isRGB: true} } +// Colour256 constructs a 256-colour Colour value. Values outside [0,255] clamp to 0. func Colour256(c int) Colour { if c < 0 || c > 255 { c = 0 @@ -24,7 +22,7 @@ func Colour256(c int) Colour { return Colour{colour256: c} } -// ANSI generates the escape sequence for foreground or background colours. +// ANSI generates the foreground or background escape sequence for this colour. func (c Colour) ANSI(foreground bool) string { if c.isRGB { if foreground { @@ -38,272 +36,489 @@ func (c Colour) ANSI(foreground bool) string { return "\033[48;5;" + itoa(c.colour256) + "m" } +// isZero reports whether the colour is the zero value (no colour set). +func (c Colour) isZero() bool { + return !c.isRGB && c.colour256 == 0 +} + +// Reset is the ANSI sequence that clears all colour and style attributes. const Reset = "\033[0m" -// Theme holds colour configuration for a logger. Colour fields are treated as immutable -// after the first call to Cache() or EnsureCached(). To switch themes at runtime, build a -// new Theme and pass it to SetTheme — mutating colour fields on an active Theme after caching -// produces undefined cached values. +// StyleSlot is a semantic slot in a Theme. Callers use slots rather than raw colours +// so themes can be swapped without updating every call site. +type StyleSlot uint8 + +const ( + SlotGood StyleSlot = iota // success / positive outcome + SlotBad // error / failure + SlotWarn // warning / degraded + SlotInfo // informational + SlotMuted // secondary / de-emphasised text + SlotStrong // emphasis / bold-equivalent + SlotHeading // section headings + SlotEndpoint // service/URL labels + SlotHyperlink // clickable URLs (OSC 8) + SlotContinuation // │ glyph on continuation lines + SlotCount // count/numeric badge + SlotSecure // masked/secure field indicator + SlotStatusOK // [ OK ] badge + SlotStatusFail // [FAIL] badge + SlotStatusWarn // [WARN] badge + SlotStatusInfo // [INFO] badge + SlotTableHeader // table column header text + slotCount // sentinel — must stay last and unexported +) + +// ThemeOption configures a Theme during construction. +type ThemeOption func(*themeBuilder) + +// themeBuilder accumulates colours before the Theme is constructed. +type themeBuilder struct { + levelColours [6]Colour + slotColours [slotCount]Colour + + // Core log-line slots map to named fields for clarity. + timestampColour Colour + messageColour Colour + fieldKeyColour Colour + fieldValColour Colour + errorValColour Colour + + // hasAnyColour is set by any option that sets at least one colour, so that + // buildTheme can distinguish "no options passed" (Mono) from "options set colour + // only on some fields". Without this flag, a theme with only level colours would + // incorrectly be treated as noColour because the named colour fields are zero. + hasAnyColour bool +} + +// WithLevelColour sets the foreground colour for a specific log level. +func WithLevelColour(level Level, c Colour) ThemeOption { + return func(b *themeBuilder) { + if level >= 0 && int(level) < len(b.levelColours) { + b.levelColours[level] = c + b.hasAnyColour = true + } + } +} + +// WithLevelColours sets foreground colours for all five log levels in one call. +func WithLevelColours(debug, info, warn, errr, fatal Colour) ThemeOption { + return func(b *themeBuilder) { + b.levelColours[LevelDebug] = debug + b.levelColours[LevelInfo] = info + b.levelColours[LevelWarn] = warn + b.levelColours[LevelError] = errr + b.levelColours[LevelFatal] = fatal + // LevelOff inherits Info. + b.levelColours[LevelOff] = info + b.hasAnyColour = true + } +} + +// WithStyleSlot sets the foreground colour for a semantic style slot. +func WithStyleSlot(slot StyleSlot, c Colour) ThemeOption { + return func(b *themeBuilder) { + if slot < slotCount { + b.slotColours[slot] = c + b.hasAnyColour = true + } + } +} + +// WithMessageColour sets the foreground colour for log message text. +func WithMessageColour(c Colour) ThemeOption { + return func(b *themeBuilder) { b.messageColour = c; b.hasAnyColour = true } +} + +// WithFieldColours sets the foreground colours for field keys, values, and error values. +func WithFieldColours(key, value, errorVal Colour) ThemeOption { + return func(b *themeBuilder) { + b.fieldKeyColour = key + b.fieldValColour = value + b.errorValColour = errorVal + b.hasAnyColour = true + } +} + +// WithTimestampColour sets the foreground colour for the log timestamp. +func WithTimestampColour(c Colour) ThemeOption { + return func(b *themeBuilder) { b.timestampColour = c; b.hasAnyColour = true } +} + +// WithBracketColour sets the foreground colour used for structural chrome (borders, brackets). +// Equivalent to calling WithFieldColours with the same key colour; exists as a named shorthand +// for callers who only want to theme the chrome without touching value colours. +func WithBracketColour(c Colour) ThemeOption { + return func(b *themeBuilder) { b.fieldKeyColour = c; b.hasAnyColour = true } +} + +// Theme is an immutable colour palette constructed via NewTheme. +// All ANSI escape codes are pre-computed at construction — there is no lazy caching. +// After NewTheme returns, no field on Theme is ever mutated. type Theme struct { - // Per-level foreground codes, indexed by Level constant. - cachedLevelFg [6]string - Name string + name string - // Pre-computed ANSI escape sequences for hot-path rendering. + // Pre-computed ANSI sequences for the hot-path log line renderer. cachedTimestampFg string cachedMessageFg string cachedFieldKeyFg string cachedFieldValFg string cachedErrorValFg string - // Cached codes used by the pretty package to avoid per-render allocs. - cachedTableHeaderFg string - cachedInfoColourFg string - - // cacheOnce ensures Cache() populates the cached fields exactly once, - // even when called concurrently. - cacheOnce sync.Once - - DebugColour Colour - InfoColour Colour - WarnColour Colour - ErrorColour Colour - FatalColour Colour - - TimestampColour Colour - MessageColour Colour - FieldKeyColour Colour - FieldValColour Colour - ErrorValColour Colour - - // Status indicator colours for operation results - StatusOKColour Colour - StatusFailColour Colour - StatusWarnColour Colour - StatusInfoColour Colour - - // Table header colour - TableHeader Colour -} - -// Cache pre-computes ANSI sequences for all colours used in hot-path rendering. -// Idempotent and concurrent-safe: the body runs exactly once regardless of how many goroutines -// call Cache() simultaneously. Subsequent calls are no-ops. -// Built-in themes call this automatically via cachedTheme. Logger constructors and SetTheme -// call EnsureCached internally, so explicit Cache() calls are not required when themes enter -// through those paths. -func (t *Theme) Cache() { - if t == nil { - return + // Per-level foreground codes, indexed by Level constant. + cachedLevelFg [6]string + + // Pre-computed per-slot foreground codes. + cachedSlotFg [slotCount]string + + // noColour is true when all slots are intentionally empty (ThemeMono). + noColour bool +} + +// NewTheme constructs an immutable Theme with all ANSI codes pre-computed. +// Panics if an invalid option is passed (no current option can produce this, +// but guards future additions). The name is informational only. +func NewTheme(name string, opts ...ThemeOption) *Theme { + b := &themeBuilder{} + for _, opt := range opts { + if opt != nil { + opt(b) + } } - t.cacheOnce.Do(func() { - t.cachedTimestampFg = t.TimestampColour.ANSI(true) - t.cachedMessageFg = t.MessageColour.ANSI(true) - t.cachedFieldKeyFg = t.FieldKeyColour.ANSI(true) - t.cachedFieldValFg = t.FieldValColour.ANSI(true) - t.cachedErrorValFg = t.ErrorValColour.ANSI(true) - t.cachedLevelFg[LevelDebug] = t.DebugColour.ANSI(true) - t.cachedLevelFg[LevelInfo] = t.InfoColour.ANSI(true) - t.cachedLevelFg[LevelWarn] = t.WarnColour.ANSI(true) - t.cachedLevelFg[LevelError] = t.ErrorColour.ANSI(true) - t.cachedLevelFg[LevelFatal] = t.FatalColour.ANSI(true) - t.cachedLevelFg[LevelOff] = t.InfoColour.ANSI(true) - // Extra codes consumed by the pretty package. - t.cachedTableHeaderFg = t.TableHeader.ANSI(true) - t.cachedInfoColourFg = t.InfoColour.ANSI(true) - }) -} - -// CachedTableHeaderFg returns the pre-computed ANSI foreground for the table header colour. -func (t *Theme) CachedTableHeaderFg() string { return t.cachedTableHeaderFg } + return buildTheme(name, b) +} -// CachedInfoColourFg returns the pre-computed ANSI foreground for the info colour. -func (t *Theme) CachedInfoColourFg() string { return t.cachedInfoColourFg } +func buildTheme(name string, b *themeBuilder) *Theme { + t := &Theme{name: name} -// CachedMessageFg returns the pre-computed ANSI foreground for the message colour. -func (t *Theme) CachedMessageFg() string { return t.cachedMessageFg } + if !b.hasAnyColour { + // No colour options were applied — this is a mono/passthrough theme. + // All cached strings remain empty strings; Format returns the input unchanged. + t.noColour = true + return t + } -// CachedFieldKeyFg returns the pre-computed ANSI foreground for field keys. -func (t *Theme) CachedFieldKeyFg() string { return t.cachedFieldKeyFg } + // Only generate ANSI strings for colours that were explicitly set. + // Zero-value Colour (Colour256(0)) would produce a valid but unintended escape sequence. + if !b.timestampColour.isZero() { + t.cachedTimestampFg = b.timestampColour.ANSI(true) + } + if !b.messageColour.isZero() { + t.cachedMessageFg = b.messageColour.ANSI(true) + } + if !b.fieldKeyColour.isZero() { + t.cachedFieldKeyFg = b.fieldKeyColour.ANSI(true) + } + if !b.fieldValColour.isZero() { + t.cachedFieldValFg = b.fieldValColour.ANSI(true) + } + if !b.errorValColour.isZero() { + t.cachedErrorValFg = b.errorValColour.ANSI(true) + } -// CachedFieldValFg returns the pre-computed ANSI foreground for field values. -func (t *Theme) CachedFieldValFg() string { return t.cachedFieldValFg } + for i, c := range b.levelColours { + if !c.isZero() { + t.cachedLevelFg[i] = c.ANSI(true) + } + } + + for i, c := range b.slotColours { + if !c.isZero() { + t.cachedSlotFg[i] = c.ANSI(true) + } + } -// cachedTheme calls Cache on t and returns it, used for package-level theme initialisation. -func cachedTheme(t *Theme) *Theme { - t.Cache() return t } -// ensureCached calls Cache on t in-place and returns the same pointer. -// Safe to call concurrently: sync.Once inside Cache() guarantees at-most-once execution. -func ensureCached(t *Theme) *Theme { +// Name returns the theme's display name. +func (t *Theme) Name() string { if t == nil { - return nil + return "" } - t.Cache() - return t + return t.name } -// EnsureCached caches the theme's ANSI sequences in-place and returns the receiver. -// Idempotent and concurrent-safe. Use this from external packages (e.g. pretty) that -// cannot access the internal helper. -func (t *Theme) EnsureCached() *Theme { - return ensureCached(t) -} - -var ThemeNightOwl = cachedTheme(&Theme{ - Name: "Night Owl", - DebugColour: RGB(0xC7, 0x92, 0xEA), // #C792EA - InfoColour: RGB(0x82, 0xAA, 0xFF), // #82AAFF - WarnColour: RGB(0xFF, 0xCB, 0x6B), // #FFCB6B - ErrorColour: RGB(0xFF, 0x55, 0x72), // #FF5572 - FatalColour: RGB(0xFF, 0x00, 0x00), - TimestampColour: RGB(0x7E, 0x8E, 0xA6), - MessageColour: RGB(0xE0, 0xE0, 0xE0), - FieldKeyColour: RGB(0x7E, 0x8E, 0xA6), - FieldValColour: RGB(0xD3, 0xD3, 0xD3), - ErrorValColour: RGB(0xFF, 0x55, 0x72), - StatusOKColour: RGB(0x80, 0xD4, 0xAA), // Green #80D4AA - StatusFailColour: RGB(0xFF, 0x55, 0x72), // Red #FF5572 - StatusWarnColour: RGB(0xFF, 0xCB, 0x6B), // Yellow #FFCB6B - StatusInfoColour: RGB(0x82, 0xAA, 0xFF), // Blue #82AAFF - TableHeader: RGB(0x7F, 0xD3, 0xFF), // Teal #7FD3FF -}) - -var ThemeSolarized = cachedTheme(&Theme{ - Name: "Solarized", - DebugColour: Colour256(61), - InfoColour: Colour256(33), - WarnColour: Colour256(136), - ErrorColour: Colour256(160), - FatalColour: Colour256(124), - TimestampColour: Colour256(8), - MessageColour: Colour256(7), - FieldKeyColour: Colour256(8), - FieldValColour: Colour256(7), - ErrorValColour: Colour256(160), - StatusOKColour: Colour256(64), // Green - StatusFailColour: Colour256(160), // Red - StatusWarnColour: Colour256(136), // Yellow - StatusInfoColour: Colour256(33), // Blue - TableHeader: Colour256(37), // Cyan -}) - -var ThemeDracula = cachedTheme(&Theme{ - Name: "Dracula", - DebugColour: Colour256(141), - InfoColour: Colour256(81), - WarnColour: Colour256(228), - ErrorColour: Colour256(212), - FatalColour: Colour256(196), - TimestampColour: Colour256(59), - MessageColour: Colour256(231), - FieldKeyColour: Colour256(59), - FieldValColour: Colour256(188), - ErrorValColour: Colour256(212), - StatusOKColour: Colour256(84), // Green - StatusFailColour: Colour256(212), // Red - StatusWarnColour: Colour256(228), // Yellow - StatusInfoColour: Colour256(81), // Blue - TableHeader: Colour256(87), // Cyan -}) - -var ThemeNord = cachedTheme(&Theme{ - Name: "Nord", - DebugColour: Colour256(139), - InfoColour: Colour256(109), - WarnColour: Colour256(180), - ErrorColour: Colour256(191), - FatalColour: Colour256(167), - TimestampColour: Colour256(59), - MessageColour: Colour256(216), - FieldKeyColour: Colour256(59), - FieldValColour: Colour256(188), - ErrorValColour: Colour256(191), - StatusOKColour: Colour256(108), // Green - StatusFailColour: Colour256(167), // Red - StatusWarnColour: Colour256(180), // Yellow - StatusInfoColour: Colour256(109), // Blue - TableHeader: Colour256(110), // Frost Blue -}) - -func (t *Theme) GetColourForLevel(level Level) Colour { - switch level { - case LevelDebug: - return t.DebugColour - case LevelInfo: - return t.InfoColour - case LevelWarn: - return t.WarnColour - case LevelError: - return t.ErrorColour - case LevelFatal: - return t.FatalColour - case LevelOff: - return t.InfoColour +// Format wraps s with the ANSI foreground code for slot and the Reset sequence. +// When the theme has no colour configured for that slot (or the theme is Mono), +// s is returned unchanged with zero allocations. +func (t *Theme) Format(slot StyleSlot, s string) string { + if t == nil || t.noColour || slot >= slotCount { + return s } - return t.InfoColour + code := t.cachedSlotFg[slot] + if code == "" { + return s + } + return code + s + Reset +} + +// Wrap returns the ANSI prefix and suffix strings for the given slot. +// Callers building strings around their own formatting can embed these directly. +// Both strings are empty when the theme has no colour for the slot. +func (t *Theme) Wrap(slot StyleSlot) (prefix, suffix string) { + if t == nil || t.noColour || slot >= slotCount { + return "", "" + } + code := t.cachedSlotFg[slot] + if code == "" { + return "", "" + } + return code, Reset } -// StatusFormatter provides colour-aware status indicator formatting for operation results. -// Pre-caches ANSI codes at initialization for zero-allocation formatting. -type StatusFormatter struct { - okCode string - failCode string - warnCode string - infoCode string - resetCode string // Reset to message colour instead of terminal default - enabled bool +// Stylish reports whether the writer is ANSI-capable (i.e. a real terminal). +// Useful when callers want to decide whether to use Format before building a string. +// The theme is not consulted; only the writer matters. +func (*Theme) Stylish(w io.Writer) bool { + return IsTerminalWriter(w) } -// NewStatusFormatter creates a formatter that respects terminal capabilities and theme. -// Pass nil theme to disable colours. -func NewStatusFormatter(theme *Theme, isTTY bool) *StatusFormatter { - sf := &StatusFormatter{ - enabled: isTTY && theme != nil, +// --- Internal accessors used by template.go, renderable.go, and pretty.go --- +// These return empty strings on mono/nil themes, so callers need not nil-check. + +func (t *Theme) cachedTimestampFgStr() string { + if t == nil { + return "" } + return t.cachedTimestampFg +} - if sf.enabled { - sf.okCode = theme.StatusOKColour.ANSI(true) - sf.failCode = theme.StatusFailColour.ANSI(true) - sf.warnCode = theme.StatusWarnColour.ANSI(true) - sf.infoCode = theme.StatusInfoColour.ANSI(true) - // Reset to message colour instead of terminal default to maintain log colour consistency - sf.resetCode = theme.MessageColour.ANSI(true) +func (t *Theme) cachedMessageFgStr() string { + if t == nil { + return "" } + return t.cachedMessageFg +} - return sf +func (t *Theme) cachedFieldKeyFgStr() string { + if t == nil { + return "" + } + return t.cachedFieldKeyFg } -// Okay formats an OK status with green colour when enabled. -func (sf *StatusFormatter) Okay(text string) string { - if !sf.enabled { - return text +func (t *Theme) cachedFieldValFgStr() string { + if t == nil { + return "" } - return sf.okCode + text + sf.resetCode + return t.cachedFieldValFg } -// Fail formats a FAIL status with red colour when enabled. -func (sf *StatusFormatter) Fail(text string) string { - if !sf.enabled { - return text +func (t *Theme) cachedErrorValFgStr() string { + if t == nil { + return "" } - return sf.failCode + text + sf.resetCode + return t.cachedErrorValFg } -// Warn formats a WARN status with yellow colour when enabled. -func (sf *StatusFormatter) Warn(text string) string { - if !sf.enabled { - return text +func (t *Theme) cachedTableHeaderFgStr() string { + if t == nil { + return "" } - return sf.warnCode + text + sf.resetCode + return t.cachedSlotFg[SlotTableHeader] } -// Info formats an INFO status with blue colour when enabled. -func (sf *StatusFormatter) Info(text string) string { - if !sf.enabled { - return text +func (t *Theme) cachedInfoColourFgStr() string { + if t == nil { + return "" } - return sf.infoCode + text + sf.resetCode + // SlotInfo maps to the info colour used in SystemInfo headers. + return t.cachedSlotFg[SlotInfo] } + +// cachedLevelCode returns the pre-computed ANSI code for a log level. +func (t *Theme) cachedLevelCode(level Level) string { + if t == nil || level < 0 || int(level) >= len(t.cachedLevelFg) { + return "" + } + return t.cachedLevelFg[level] +} + +// levelColourForStatus returns the ANSI code for a slot used by status colouring. +func (t *Theme) slotCode(slot StyleSlot) string { + if t == nil || slot >= slotCount { + return "" + } + return t.cachedSlotFg[slot] +} + +// --- Public compatibility accessors used by Pretty.printStyled and external callers --- + +// LevelColour returns the ANSI foreground escape for the given level. +// Empty string when no colour is configured or the theme is nil. +func (t *Theme) LevelColour(level Level) string { + return t.cachedLevelCode(level) +} + +// SlotColour returns the ANSI foreground escape for the given slot. +// Empty string when no colour is configured or the theme is nil. +func (t *Theme) SlotColour(slot StyleSlot) string { + return t.slotCode(slot) +} + +// --- Public accessor methods (replaces the old exported Cached* methods) --- + +// CachedFieldKeyFg returns the pre-computed ANSI foreground for field keys. +// Retained for callers (renderable.go, template.go) that access it by method. +func (t *Theme) CachedFieldKeyFg() string { return t.cachedFieldKeyFgStr() } + +// CachedFieldValFg returns the pre-computed ANSI foreground for field values. +func (t *Theme) CachedFieldValFg() string { return t.cachedFieldValFgStr() } + +// CachedMessageFg returns the pre-computed ANSI foreground for message text. +func (t *Theme) CachedMessageFg() string { return t.cachedMessageFgStr() } + +// CachedTableHeaderFg returns the pre-computed ANSI foreground for table headers. +func (t *Theme) CachedTableHeaderFg() string { return t.cachedTableHeaderFgStr() } + +// CachedInfoColourFg returns the pre-computed ANSI foreground for the info colour. +func (t *Theme) CachedInfoColourFg() string { return t.cachedInfoColourFgStr() } + +// ResetStr returns the ANSI reset sequence for colour themes, or an empty string +// for colour-free (mono) themes. Use this in renderers instead of the bare Reset +// constant so that NO_COLOR and ThemeMono suppress resets along with the colour codes. +func (t *Theme) ResetStr() string { + if t == nil || t.noColour { + return "" + } + return Reset +} + +// --- Built-in themes --- + +// ThemeNightOwl is a dark, high-contrast palette inspired by the Night Owl VS Code theme. +var ThemeNightOwl = NewTheme("Night Owl", + WithLevelColours( + RGB(0xC7, 0x92, 0xEA), // debug: purple + RGB(0x82, 0xAA, 0xFF), // info: blue + RGB(0xFF, 0xCB, 0x6B), // warn: amber + RGB(0xFF, 0x55, 0x72), // error: red + RGB(0xFF, 0x00, 0x00), // fatal: bright red + ), + WithTimestampColour(RGB(0x7E, 0x8E, 0xA6)), + WithMessageColour(RGB(0xE0, 0xE0, 0xE0)), + WithFieldColours( + RGB(0x7E, 0x8E, 0xA6), // key: muted steel + RGB(0xD3, 0xD3, 0xD3), // value: light grey + RGB(0xFF, 0x55, 0x72), // error value: red + ), + WithStyleSlot(SlotGood, RGB(0x80, 0xD4, 0xAA)), + WithStyleSlot(SlotBad, RGB(0xFF, 0x55, 0x72)), + WithStyleSlot(SlotWarn, RGB(0xFF, 0xCB, 0x6B)), + WithStyleSlot(SlotInfo, RGB(0x82, 0xAA, 0xFF)), + WithStyleSlot(SlotMuted, RGB(0x7E, 0x8E, 0xA6)), + WithStyleSlot(SlotStrong, RGB(0xE0, 0xE0, 0xE0)), + WithStyleSlot(SlotHeading, RGB(0x7F, 0xD3, 0xFF)), + WithStyleSlot(SlotEndpoint, RGB(0x82, 0xAA, 0xFF)), + WithStyleSlot(SlotHyperlink, RGB(0x7F, 0xD3, 0xFF)), + WithStyleSlot(SlotContinuation, RGB(0x7E, 0x8E, 0xA6)), + WithStyleSlot(SlotCount, RGB(0xC7, 0x92, 0xEA)), + WithStyleSlot(SlotSecure, RGB(0xFF, 0xCB, 0x6B)), + WithStyleSlot(SlotStatusOK, RGB(0x80, 0xD4, 0xAA)), + WithStyleSlot(SlotStatusFail, RGB(0xFF, 0x55, 0x72)), + WithStyleSlot(SlotStatusWarn, RGB(0xFF, 0xCB, 0x6B)), + WithStyleSlot(SlotStatusInfo, RGB(0x82, 0xAA, 0xFF)), + WithStyleSlot(SlotTableHeader, RGB(0x7F, 0xD3, 0xFF)), +) + +// ThemeSolarized is a classic Solarized 256-colour palette. +var ThemeSolarized = NewTheme("Solarized", + WithLevelColours( + Colour256(61), // debug + Colour256(33), // info + Colour256(136), // warn + Colour256(160), // error + Colour256(124), // fatal + ), + WithTimestampColour(Colour256(8)), + WithMessageColour(Colour256(7)), + WithFieldColours(Colour256(8), Colour256(7), Colour256(160)), + WithStyleSlot(SlotGood, Colour256(64)), + WithStyleSlot(SlotBad, Colour256(160)), + WithStyleSlot(SlotWarn, Colour256(136)), + WithStyleSlot(SlotInfo, Colour256(33)), + WithStyleSlot(SlotMuted, Colour256(8)), + WithStyleSlot(SlotStrong, Colour256(7)), + WithStyleSlot(SlotHeading, Colour256(37)), + WithStyleSlot(SlotEndpoint, Colour256(33)), + WithStyleSlot(SlotHyperlink, Colour256(37)), + WithStyleSlot(SlotContinuation, Colour256(8)), + WithStyleSlot(SlotCount, Colour256(61)), + WithStyleSlot(SlotSecure, Colour256(136)), + WithStyleSlot(SlotStatusOK, Colour256(64)), + WithStyleSlot(SlotStatusFail, Colour256(160)), + WithStyleSlot(SlotStatusWarn, Colour256(136)), + WithStyleSlot(SlotStatusInfo, Colour256(33)), + WithStyleSlot(SlotTableHeader, Colour256(37)), +) + +// ThemeDracula is the Dracula 256-colour palette. +var ThemeDracula = NewTheme("Dracula", + WithLevelColours( + Colour256(141), // debug: purple + Colour256(81), // info: cyan + Colour256(228), // warn: yellow + Colour256(212), // error: pink + Colour256(196), // fatal: red + ), + WithTimestampColour(Colour256(59)), + WithMessageColour(Colour256(231)), + WithFieldColours(Colour256(59), Colour256(188), Colour256(212)), + WithStyleSlot(SlotGood, Colour256(84)), + WithStyleSlot(SlotBad, Colour256(212)), + WithStyleSlot(SlotWarn, Colour256(228)), + WithStyleSlot(SlotInfo, Colour256(81)), + WithStyleSlot(SlotMuted, Colour256(59)), + WithStyleSlot(SlotStrong, Colour256(231)), + WithStyleSlot(SlotHeading, Colour256(87)), + WithStyleSlot(SlotEndpoint, Colour256(81)), + WithStyleSlot(SlotHyperlink, Colour256(87)), + WithStyleSlot(SlotContinuation, Colour256(59)), + WithStyleSlot(SlotCount, Colour256(141)), + WithStyleSlot(SlotSecure, Colour256(228)), + WithStyleSlot(SlotStatusOK, Colour256(84)), + WithStyleSlot(SlotStatusFail, Colour256(212)), + WithStyleSlot(SlotStatusWarn, Colour256(228)), + WithStyleSlot(SlotStatusInfo, Colour256(81)), + WithStyleSlot(SlotTableHeader, Colour256(87)), +) + +// ThemeNord is the Nord 256-colour palette, cool and arctic. +var ThemeNord = NewTheme("Nord", + WithLevelColours( + Colour256(139), // debug + Colour256(109), // info + Colour256(180), // warn + Colour256(191), // error + Colour256(167), // fatal + ), + WithTimestampColour(Colour256(59)), + WithMessageColour(Colour256(216)), + WithFieldColours(Colour256(59), Colour256(188), Colour256(191)), + WithStyleSlot(SlotGood, Colour256(108)), + WithStyleSlot(SlotBad, Colour256(167)), + WithStyleSlot(SlotWarn, Colour256(180)), + WithStyleSlot(SlotInfo, Colour256(109)), + WithStyleSlot(SlotMuted, Colour256(59)), + WithStyleSlot(SlotStrong, Colour256(216)), + WithStyleSlot(SlotHeading, Colour256(110)), + WithStyleSlot(SlotEndpoint, Colour256(109)), + WithStyleSlot(SlotHyperlink, Colour256(110)), + WithStyleSlot(SlotContinuation, Colour256(59)), + WithStyleSlot(SlotCount, Colour256(139)), + WithStyleSlot(SlotSecure, Colour256(180)), + WithStyleSlot(SlotStatusOK, Colour256(108)), + WithStyleSlot(SlotStatusFail, Colour256(167)), + WithStyleSlot(SlotStatusWarn, Colour256(180)), + WithStyleSlot(SlotStatusInfo, Colour256(109)), + WithStyleSlot(SlotTableHeader, Colour256(110)), +) + +// ThemeMono is a colour-free theme. Format always returns the input unchanged. +// Use it when piping output to files or other tools that don't interpret ANSI. +var ThemeMono = NewTheme("Mono") + +// noColourTheme is the fallback for Logger.Style() when colour is disabled or there +// is no console writer. Equivalent to ThemeMono: Format returns the input unchanged. +var noColourTheme = NewTheme("none") diff --git a/theme_test.go b/theme_test.go index 4600b64..fcf8759 100644 --- a/theme_test.go +++ b/theme_test.go @@ -3,106 +3,251 @@ package velocity import ( "bytes" "strings" - "sync" "testing" ) -// TestTheme_Cache_Idempotent confirms that calling Cache() twice produces identical -// cached strings. Entry points rely on this to safely re-cache already-cached themes. -func TestTheme_Cache_Idempotent(t *testing.T) { +// TestNewTheme_Name confirms the name accessor returns what was passed to NewTheme. +func TestNewTheme_Name(t *testing.T) { t.Parallel() - theme := &Theme{ - DebugColour: RGB(0xC7, 0x92, 0xEA), - InfoColour: RGB(0x82, 0xAA, 0xFF), - WarnColour: RGB(0xFF, 0xCB, 0x6B), - ErrorColour: RGB(0xFF, 0x55, 0x72), - FatalColour: RGB(0xFF, 0x00, 0x00), - TimestampColour: RGB(0x7E, 0x8E, 0xA6), - MessageColour: RGB(0xE0, 0xE0, 0xE0), - FieldKeyColour: RGB(0x7E, 0x8E, 0xA6), - FieldValColour: RGB(0xD3, 0xD3, 0xD3), - ErrorValColour: RGB(0xFF, 0x55, 0x72), - TableHeader: RGB(0x7F, 0xD3, 0xFF), + th := NewTheme("Test Theme") + if got := th.Name(); got != "Test Theme" { + t.Errorf("Name() = %q, want %q", got, "Test Theme") } +} - theme.Cache() +// TestNewTheme_NilName confirms nil theme returns empty string from Name. +func TestNewTheme_NilName(t *testing.T) { + t.Parallel() - // Snapshot field values after the first call. - tsAfterFirst := theme.cachedTimestampFg - msgAfterFirst := theme.cachedMessageFg - keyAfterFirst := theme.cachedFieldKeyFg - valAfterFirst := theme.cachedFieldValFg - errAfterFirst := theme.cachedErrorValFg - tblAfterFirst := theme.cachedTableHeaderFg - infoAfterFirst := theme.cachedInfoColourFg - lvlAfterFirst := theme.cachedLevelFg + var th *Theme + if got := th.Name(); got != "" { + t.Errorf("nil.Name() = %q, want empty", got) + } +} - // Second call must be a no-op — sync.Once prevents re-execution. - theme.Cache() +// TestThemeMono_Format confirms ThemeMono (no colour options) returns input unchanged. +func TestThemeMono_Format(t *testing.T) { + t.Parallel() - if theme.cachedTimestampFg != tsAfterFirst { - t.Errorf("cachedTimestampFg changed: %q vs %q", tsAfterFirst, theme.cachedTimestampFg) + const input = "hello" + for _, slot := range []StyleSlot{SlotGood, SlotBad, SlotWarn, SlotStatusOK, SlotTableHeader} { + got := ThemeMono.Format(slot, input) + if got != input { + t.Errorf("ThemeMono.Format(%d, %q) = %q, want input unchanged", slot, input, got) + } } - if theme.cachedMessageFg != msgAfterFirst { - t.Errorf("cachedMessageFg changed: %q vs %q", msgAfterFirst, theme.cachedMessageFg) +} + +// TestTheme_Format_EmitsANSI confirms Format wraps with ANSI codes on a coloured theme. +func TestTheme_Format_EmitsANSI(t *testing.T) { + t.Parallel() + + th := NewTheme("test", + WithStyleSlot(SlotGood, RGB(0x00, 0xFF, 0x00)), + ) + + got := th.Format(SlotGood, "OK") + if !strings.HasPrefix(got, "\033[") { + t.Errorf("Format() did not emit ANSI prefix: %q", got) + } + if !strings.Contains(got, "OK") { + t.Errorf("Format() dropped the content: %q", got) + } + if !strings.HasSuffix(got, Reset) { + t.Errorf("Format() did not append Reset: %q", got) + } +} + +// TestTheme_Format_UnsetSlot confirms Format returns input unchanged for a slot with no colour. +func TestTheme_Format_UnsetSlot(t *testing.T) { + t.Parallel() + + // Only SlotGood is set; SlotBad should passthrough. + th := NewTheme("partial", + WithStyleSlot(SlotGood, RGB(0x00, 0xFF, 0x00)), + ) + + in := "FAIL" + got := th.Format(SlotBad, in) + if got != in { + t.Errorf("Format() for unset slot = %q, want %q", got, in) + } +} + +// TestTheme_Format_NilTheme confirms nil theme returns input unchanged. +func TestTheme_Format_NilTheme(t *testing.T) { + t.Parallel() + + const input = "hello" + var th *Theme + got := th.Format(SlotGood, input) + if got != input { + t.Errorf("nil.Format() = %q, want %q", got, input) + } +} + +// TestTheme_Wrap_EmitsCodesForSetSlot confirms Wrap returns non-empty prefix/suffix for a set slot. +func TestTheme_Wrap_EmitsCodesForSetSlot(t *testing.T) { + t.Parallel() + + th := NewTheme("test", + WithStyleSlot(SlotStatusOK, RGB(0x80, 0xD4, 0xAA)), + ) + + prefix, suffix := th.Wrap(SlotStatusOK) + if prefix == "" { + t.Error("Wrap() prefix is empty for set slot") + } + if suffix != Reset { + t.Errorf("Wrap() suffix = %q, want %q", suffix, Reset) + } +} + +// TestTheme_Wrap_EmptyForUnsetSlot confirms Wrap returns empty strings for an unset slot. +func TestTheme_Wrap_EmptyForUnsetSlot(t *testing.T) { + t.Parallel() + + th := NewTheme("partial", + WithStyleSlot(SlotGood, RGB(0x00, 0xFF, 0x00)), + ) + + prefix, suffix := th.Wrap(SlotBad) + if prefix != "" || suffix != "" { + t.Errorf("Wrap() for unset slot = (%q, %q), want both empty", prefix, suffix) } - if theme.cachedFieldKeyFg != keyAfterFirst { - t.Errorf("cachedFieldKeyFg changed: %q vs %q", keyAfterFirst, theme.cachedFieldKeyFg) +} + +// TestTheme_Wrap_NilTheme confirms nil theme returns empty strings from Wrap. +func TestTheme_Wrap_NilTheme(t *testing.T) { + t.Parallel() + + var th *Theme + prefix, suffix := th.Wrap(SlotGood) + if prefix != "" || suffix != "" { + t.Errorf("nil.Wrap() = (%q, %q), want both empty", prefix, suffix) } - if theme.cachedFieldValFg != valAfterFirst { - t.Errorf("cachedFieldValFg changed: %q vs %q", valAfterFirst, theme.cachedFieldValFg) +} + +// TestTheme_Stylish_BufferIsNotTTY confirms Stylish returns false for a bytes.Buffer. +func TestTheme_Stylish_BufferIsNotTTY(t *testing.T) { + t.Parallel() + + th := ThemeNightOwl + if th.Stylish(&bytes.Buffer{}) { + t.Error("Stylish(bytes.Buffer) should return false — buffer is not a TTY") } - if theme.cachedErrorValFg != errAfterFirst { - t.Errorf("cachedErrorValFg changed: %q vs %q", errAfterFirst, theme.cachedErrorValFg) +} + +// TestWithLevelColours_SetsAllLevels confirms WithLevelColours populates all five levels. +func TestWithLevelColours_SetsAllLevels(t *testing.T) { + t.Parallel() + + th := NewTheme("levels", + WithLevelColours( + RGB(0x11, 0x11, 0x11), // debug + RGB(0x22, 0x22, 0x22), // info + RGB(0x33, 0x33, 0x33), // warn + RGB(0x44, 0x44, 0x44), // error + RGB(0x55, 0x55, 0x55), // fatal + ), + ) + + for _, lvl := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + code := th.cachedLevelCode(lvl) + if code == "" { + t.Errorf("level %v has empty ANSI code", lvl) + } + if !strings.HasPrefix(code, "\033[") { + t.Errorf("level %v code does not start with ESC: %q", lvl, code) + } } - if theme.cachedTableHeaderFg != tblAfterFirst { - t.Errorf("cachedTableHeaderFg changed: %q vs %q", tblAfterFirst, theme.cachedTableHeaderFg) +} + +// TestWithLevelColour_Single confirms WithLevelColour sets exactly one level. +func TestWithLevelColour_Single(t *testing.T) { + t.Parallel() + + th := NewTheme("single", + WithLevelColour(LevelError, RGB(0xFF, 0x55, 0x72)), + ) + + code := th.cachedLevelCode(LevelError) + if code == "" { + t.Error("LevelError code is empty after WithLevelColour") } - if theme.cachedInfoColourFg != infoAfterFirst { - t.Errorf("cachedInfoColourFg changed: %q vs %q", infoAfterFirst, theme.cachedInfoColourFg) + // Other levels should be empty. + if th.cachedLevelCode(LevelDebug) != "" { + t.Error("LevelDebug should be empty when not set") } - for lvl := LevelDebug; lvl <= LevelOff; lvl++ { - if theme.cachedLevelFg[lvl] != lvlAfterFirst[lvl] { - t.Errorf("cachedLevelFg[%d] changed: %q vs %q", lvl, lvlAfterFirst[lvl], theme.cachedLevelFg[lvl]) +} + +// TestBuiltInThemes_HaveAllSlots confirms each built-in theme populates the key slots. +func TestBuiltInThemes_HaveAllSlots(t *testing.T) { + t.Parallel() + + themes := []*Theme{ThemeNightOwl, ThemeSolarized, ThemeDracula, ThemeNord} + criticalSlots := []StyleSlot{SlotStatusOK, SlotStatusFail, SlotStatusWarn, SlotStatusInfo, SlotTableHeader} + + for _, th := range themes { + for _, slot := range criticalSlots { + code := th.slotCode(slot) + if code == "" { + t.Errorf("theme %q: slot %d has no ANSI code", th.Name(), slot) + } } } } -// TestLogger_SetTheme_CachesUncachedTheme verifies that a user-defined theme constructed via -// struct literal (no explicit Cache() call) produces ANSI-coloured output after passing through -// SetTheme. EnsureCached populates the theme in-place via sync.Once, so the writer has fully -// populated colour codes even though the caller never called Cache(). -func TestLogger_SetTheme_CachesUncachedTheme(t *testing.T) { +// TestBuiltInThemes_LevelCodesPresent confirms all built-in themes have level codes. +func TestBuiltInThemes_LevelCodesPresent(t *testing.T) { t.Parallel() - // Build a custom theme without calling Cache() — simulates what a user does. - customTheme := &Theme{ - Name: "Custom", - DebugColour: RGB(0x11, 0x22, 0x33), - InfoColour: RGB(0x44, 0x55, 0x66), - WarnColour: RGB(0x77, 0x88, 0x99), - ErrorColour: RGB(0xAA, 0xBB, 0xCC), - FatalColour: RGB(0xDD, 0xEE, 0xFF), - TimestampColour: RGB(0x10, 0x20, 0x30), - MessageColour: RGB(0x40, 0x50, 0x60), - FieldKeyColour: RGB(0x70, 0x80, 0x90), - FieldValColour: RGB(0xA0, 0xB0, 0xC0), - ErrorValColour: RGB(0xD0, 0xE0, 0xF0), - TableHeader: RGB(0x12, 0x34, 0x56), + themes := []*Theme{ThemeNightOwl, ThemeSolarized, ThemeDracula, ThemeNord} + for _, th := range themes { + for _, lvl := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + if th.cachedLevelCode(lvl) == "" { + t.Errorf("theme %q: level %v has no ANSI code", th.Name(), lvl) + } + } } +} + +// TestLogger_SetTheme_WithNewTheme verifies that a user-defined theme built via +// NewTheme produces ANSI-coloured output after passing through SetTheme when the +// writer is in TTY mode. +func TestLogger_SetTheme_WithNewTheme(t *testing.T) { + t.Parallel() + + customTheme := NewTheme("Custom", + WithLevelColours( + RGB(0x11, 0x22, 0x33), + RGB(0x44, 0x55, 0x66), + RGB(0x77, 0x88, 0x99), + RGB(0xAA, 0xBB, 0xCC), + RGB(0xDD, 0xEE, 0xFF), + ), + WithTimestampColour(RGB(0x10, 0x20, 0x30)), + WithMessageColour(RGB(0x40, 0x50, 0x60)), + WithFieldColours(RGB(0x70, 0x80, 0x90), RGB(0xA0, 0xB0, 0xC0), RGB(0xD0, 0xE0, 0xF0)), + WithStyleSlot(SlotTableHeader, RGB(0x12, 0x34, 0x56)), + ) var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.StructuredOutput = nil - cfg.ConsoleTheme = ThemeNightOwl // start with a known good theme + cfg.ConsoleTheme = ThemeNightOwl + + log := newFromConfig(cfg) - log := NewWithConfig(cfg) - log.SetTheme(customTheme) // must auto-cache the theme internally + // Simulate a real TTY so SetTheme re-enables ANSI. On a buffer (which is what + // test code uses), isTTY=false at construction because no fd is available. + // Setting isTTY=true before SetTheme mirrors the production path where the user + // calls SetTheme on a logger whose stdout is a real terminal. + log.consoleWriter.isTTY = true + log.SetTheme(customTheme) - // Log something and confirm ANSI escape codes appear in output. - // If the theme were not cached the colour strings would be empty and no ANSI escapes written. log.Info("testing custom theme") out := buf.String() @@ -111,58 +256,54 @@ func TestLogger_SetTheme_CachesUncachedTheme(t *testing.T) { } } -// TestTheme_Cache_PreservesPointerIdentity verifies that EnsureCached returns the original -// pointer — not a clone. The sync.Once refactor makes in-place mutation safe, so callers -// that compare theme pointers for identity (e.g. logger_settheme_test.go) must not break. -func TestTheme_Cache_PreservesPointerIdentity(t *testing.T) { +// TestTheme_Format_AllBuiltInSlots exercises Format for every named slot on NightOwl. +func TestTheme_Format_AllBuiltInSlots(t *testing.T) { t.Parallel() - original := &Theme{ - MessageColour: RGB(0xE0, 0xE0, 0xE0), - InfoColour: RGB(0x82, 0xAA, 0xFF), + th := ThemeNightOwl + slots := []StyleSlot{ + SlotGood, SlotBad, SlotWarn, SlotInfo, SlotMuted, SlotStrong, SlotHeading, + SlotEndpoint, SlotHyperlink, SlotContinuation, SlotCount, SlotSecure, + SlotStatusOK, SlotStatusFail, SlotStatusWarn, SlotStatusInfo, SlotTableHeader, } - got := original.EnsureCached() - if got != original { - t.Errorf("EnsureCached returned a different pointer: want %p, got %p", original, got) + for _, slot := range slots { + got := th.Format(slot, "text") + // Every slot must contain the original text. + if !strings.Contains(got, "text") { + t.Errorf("slot %d: Format dropped content: %q", slot, got) + } + // Every slot on NightOwl should have a colour code. + if !strings.HasPrefix(got, "\033[") { + t.Errorf("slot %d: Format did not emit ANSI prefix: %q", slot, got) + } } } -// TestTheme_Cache_Concurrent verifies that Cache() called from many goroutines simultaneously -// produces no data race. Correctness is validated by the -race flag; this test exercises the path. -func TestTheme_Cache_Concurrent(t *testing.T) { +// TestNoColourTheme_Format confirms the package-level noColourTheme returns input unchanged. +func TestNoColourTheme_Format(t *testing.T) { t.Parallel() - theme := &Theme{ - DebugColour: RGB(0xC7, 0x92, 0xEA), - InfoColour: RGB(0x82, 0xAA, 0xFF), - WarnColour: RGB(0xFF, 0xCB, 0x6B), - ErrorColour: RGB(0xFF, 0x55, 0x72), - FatalColour: RGB(0xFF, 0x00, 0x00), - TimestampColour: RGB(0x7E, 0x8E, 0xA6), - MessageColour: RGB(0xE0, 0xE0, 0xE0), - FieldKeyColour: RGB(0x7E, 0x8E, 0xA6), - FieldValColour: RGB(0xD3, 0xD3, 0xD3), - ErrorValColour: RGB(0xFF, 0x55, 0x72), - TableHeader: RGB(0x7F, 0xD3, 0xFF), + got := noColourTheme.Format(SlotStatusOK, "OK") + if got != "OK" { + t.Errorf("noColourTheme.Format() = %q, want %q", got, "OK") } +} - const goroutines = 50 - var wg sync.WaitGroup - wg.Add(goroutines) +// TestTheme_CachedFieldKeyFg_Empty confirms Mono theme returns empty cached fields. +func TestTheme_CachedFieldKeyFg_Empty(t *testing.T) { + t.Parallel() - for range goroutines { - go func() { - defer wg.Done() - theme.Cache() - // Read a cached field to exercise the memory model under -race. - _ = theme.CachedMessageFg() - }() + if got := ThemeMono.CachedFieldKeyFg(); got != "" { + t.Errorf("ThemeMono.CachedFieldKeyFg() = %q, want empty", got) } +} - wg.Wait() +// TestTheme_CachedTableHeaderFg_NightOwl confirms NightOwl has a non-empty table header code. +func TestTheme_CachedTableHeaderFg_NightOwl(t *testing.T) { + t.Parallel() - if theme.CachedMessageFg() == "" { - t.Error("expected CachedMessageFg to be populated after concurrent Cache() calls") + if got := ThemeNightOwl.CachedTableHeaderFg(); got == "" { + t.Error("ThemeNightOwl.CachedTableHeaderFg() is empty") } } diff --git a/with_test.go b/with_test.go index 1fa4c0c..43bca35 100644 --- a/with_test.go +++ b/with_test.go @@ -11,7 +11,7 @@ func TestWith_FieldAppearsInOutput(t *testing.T) { t.Parallel() var buf bytes.Buffer - parent := NewForTesting(&buf) + parent := newForTesting(&buf) child := parent.With(String("svc", "gateway")) child.Info("hello") @@ -26,7 +26,7 @@ func TestWith_ChainedFieldsBothAppear(t *testing.T) { t.Parallel() var buf bytes.Buffer - parent := NewForTesting(&buf) + parent := newForTesting(&buf) child := parent.With(String("a", "alpha")).With(String("b", "beta")) child.Info("chained") @@ -44,7 +44,7 @@ func TestWith_ParentUnaffected(t *testing.T) { t.Parallel() var buf bytes.Buffer - parent := NewForTesting(&buf) + parent := newForTesting(&buf) _ = parent.With(String("child_field", "x")) buf.Reset() @@ -70,18 +70,18 @@ func TestWith_EmptyFields_ReturnsSelf(t *testing.T) { t.Parallel() var buf bytes.Buffer - parent := NewForTesting(&buf) + parent := newForTesting(&buf) child := parent.With() if child != parent { t.Error("expected same logger when no fields passed to With()") } } -func TestWithTemplate_PreservesParentState(t *testing.T) { +func TestWith_PreservesParentState(t *testing.T) { t.Parallel() var buf bytes.Buffer - parent := NewForTesting(&buf) + parent := newForTesting(&buf) // Give the parent a sampler and a base field. sampler := NewCountSampler(10, 5) @@ -96,16 +96,14 @@ func TestWithTemplate_PreservesParentState(t *testing.T) { return nil })) - child := withField.WithTemplate(nil) - - // Sampler must be the same instance. - if child.sampler != sampler { + // Sampler must be the same instance on the child. + if withField.sampler != sampler { t.Error("expected child sampler to match parent sampler") } // baseFields must contain the "svc" field. found := false - for _, f := range child.baseFields { + for _, f := range withField.baseFields { if f.Key == "svc" { found = true break @@ -115,10 +113,10 @@ func TestWithTemplate_PreservesParentState(t *testing.T) { t.Error("expected child baseFields to contain 'svc' field") } - child.Info("test message") + withField.Info("test message") // Close to flush the async MultiWriter before asserting. - if err := child.Close(); err != nil { + if err := withField.Close(); err != nil { t.Fatalf("close error: %v", err) } @@ -126,3 +124,60 @@ func TestWithTemplate_PreservesParentState(t *testing.T) { t.Error("expected additional writer to receive at least one entry") } } + +func TestWithComponent_SetsField(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + parent := newForTesting(&buf) + child := parent.WithComponent("auth") + + child.Info("login") + + out := buf.String() + if !strings.Contains(out, "auth") { + t.Errorf("expected 'auth' in output from WithComponent, got: %s", out) + } +} + +func TestWithComponent_ParentUnaffected(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + parent := newForTesting(&buf) + _ = parent.WithComponent("auth") + + buf.Reset() + parent.Info("parent log") + + out := buf.String() + if strings.Contains(out, "auth") { + t.Errorf("parent log should not contain component field, got: %s", out) + } +} + +func TestWithRequest_SetsField(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + parent := newForTesting(&buf) + child := parent.WithRequest("req-abc-123") + + child.Info("handling") + + out := buf.String() + if !strings.Contains(out, "req-abc-123") { + t.Errorf("expected 'req-abc-123' in output from WithRequest, got: %s", out) + } +} + +func TestWithComponent_NilLogger(t *testing.T) { + t.Parallel() + + var l *Logger + // Nil With() returns nil, so nil.WithComponent is safe. + child := l.WithComponent("x") + if child != nil { + t.Error("expected nil for nil.WithComponent()") + } +} diff --git a/writer.go b/writer.go index af4b7c8..5e2b308 100644 --- a/writer.go +++ b/writer.go @@ -1,3 +1,17 @@ +// Package velocity provides a high-performance, structured logging library with +// rich terminal output and a composable writer pipeline. +// +// # Close ownership +// +// The rule is: whichever side constructs the underlying io.Writer owns its Close. +// +// - WithConsoleOutput(os.Stdout) — caller owns Stdout, caller closes +// - WithStructuredOutput(rotator) — caller owns rotator, caller closes +// - AddWriter("file", NewJSONWriter(f)) — caller constructed f, caller closes f +// - AddWriter("ring", NewRingBufferWriter(...)) — logger constructs internally, logger closes +// +// Logger.Close() flushes and drains writer pipeline state (channels, ring buffers) +// but never calls Close on a caller-supplied io.Writer. package velocity // Reused to avoid a heap allocation per write through the io.Writer interface. @@ -6,13 +20,100 @@ var newlineByte = []byte{'\n'} // Writer defines the interface for log output writers. // Implementations must be thread-safe and handle formatting independently. type Writer interface { - // Write writes an entry to the output. - // The entry must not be modified after this call. + // Write delivers an entry to the writer. + // The entry must not be modified after this call returns. Write(e *Entry) error Close() error } +// ThemedWriter is the optional interface for writers that support runtime theme changes. +// Implemented by ConsoleWriter and ConsoleWriterRB. +type ThemedWriter interface { + SetTheme(*Theme) +} + +// LeveledWriter is the optional interface for writers that filter by level independently. +// Useful for sinks that need a different minimum level than the parent logger. +type LeveledWriter interface { + Level() Level + SetLevel(Level) +} + +// FlushableWriter is the optional interface for writers with an internal buffer +// that can be flushed without closing the writer. +// Implemented by JSONWriter. +type FlushableWriter interface { + Flush() error +} + +// TrustedWriter is the optional interface for writers that self-report trust. +// Prefer writerOptions.isTrusted (set at AddWriter time) over this interface — +// the stored bool is cheaper than a type assertion on the hot path. +// This interface exists for writers that need to declare trust from their constructor. +type TrustedWriter interface { + IsTrusted() bool +} + +// SecureWriter is an optional capability interface for writers that handle +// field-level redaction and tag processing themselves. +// When a MultiWriter worker's underlying writer implements SecureWriter, +// WriteSecure is called instead of Write, passing the per-worker trust state +// and redaction mark without mutating the shared Entry. +// +// Built-in writers (ConsoleWriter, JSONWriter, RingBufferWriter) implement this. +// Third-party writers that don't implement it receive the entry unmodified — +// they are treated as if they are trusted (they see plaintext). +type SecureWriter interface { + WriteSecure(e *Entry, trusted bool, redactionMark string) error +} + +// WriterOption configures per-writer behaviour at AddWriter time. +type WriterOption func(*writerOptions) + +type writerOptions struct { + redactionMark string // overrides "[REDACTED]" when non-empty + isTrusted bool +} + +// WriterTrusted marks a writer as trusted. +// Trusted writers receive unredacted field values; untrusted writers receive +// [REDACTED] for Secure fields (wired in Phase 4). Default: untrusted. +// Trust is cached at AddWriter time as a bool — no type assertion per write. +func WriterTrusted() WriterOption { + return func(o *writerOptions) { + o.isTrusted = true + } +} + +// WriterRedactionMark sets the string used to replace redacted values for this +// writer. Default: "[REDACTED]". Applies to Secure/SecureURL fields and +// ... message content when the writer is untrusted. +func WriterRedactionMark(mark string) WriterOption { + return func(o *writerOptions) { + o.redactionMark = mark + } +} + +// applyWriterOptions applies opts and returns the resulting options struct. +func applyWriterOptions(opts []WriterOption) writerOptions { + var o writerOptions + for _, opt := range opts { + if opt != nil { + opt(&o) + } + } + return o +} + +// effectiveRedactionMark returns the custom mark or the default "[REDACTED]". +func (o writerOptions) effectiveRedactionMark() string { + if o.redactionMark != "" { + return o.redactionMark + } + return "[REDACTED]" +} + type NoOpWriter struct{} func (*NoOpWriter) Write(_ *Entry) error { diff --git a/writer_capability_test.go b/writer_capability_test.go new file mode 100644 index 0000000..7a63554 --- /dev/null +++ b/writer_capability_test.go @@ -0,0 +1,146 @@ +package velocity + +import ( + "bytes" + "testing" + "time" +) + +// Compile-time interface satisfaction checks — these fail to build if a writer +// loses a capability it should provide. + +var ( + _ ThemedWriter = (*ConsoleWriter)(nil) + _ ThemedWriter = (*ConsoleWriterRB)(nil) + _ FlushableWriter = (*JSONWriter)(nil) +) + +// TestWriterTrusted_DefaultUntrusted verifies that AddWriter without options is untrusted. +func TestWriterTrusted_DefaultUntrusted(t *testing.T) { + t.Parallel() + + log := New(WithConsoleOutput(&bytes.Buffer{})) + log.AddWriter("sink", &NoOpWriter{}) + defer func() { _ = log.Close() }() + + log.writers.mu.RLock() + trusted := log.writers.mw.IsTrusted("sink") + log.writers.mu.RUnlock() + + if trusted { + t.Error("writer added without WriterTrusted() should be untrusted by default") + } +} + +// TestWriterTrusted_ExplicitTrust verifies WriterTrusted() sets the flag. +func TestWriterTrusted_ExplicitTrust(t *testing.T) { + t.Parallel() + + log := New(WithConsoleOutput(&bytes.Buffer{})) + log.AddWriter("sink", &NoOpWriter{}, WriterTrusted()) + defer func() { _ = log.Close() }() + + log.writers.mu.RLock() + trusted := log.writers.mw.IsTrusted("sink") + log.writers.mu.RUnlock() + + if !trusted { + t.Error("writer added with WriterTrusted() should be trusted") + } +} + +// TestWriterTrusted_MixedWriters verifies trust is tracked per-writer, not globally. +func TestWriterTrusted_MixedWriters(t *testing.T) { + t.Parallel() + + log := New(WithConsoleOutput(&bytes.Buffer{})) + log.AddWriter("trusted-sink", &NoOpWriter{}, WriterTrusted()) + log.AddWriter("untrusted-sink", &NoOpWriter{}) + defer func() { _ = log.Close() }() + + log.writers.mu.RLock() + trustedYes := log.writers.mw.IsTrusted("trusted-sink") + trustedNo := log.writers.mw.IsTrusted("untrusted-sink") + log.writers.mu.RUnlock() + + if !trustedYes { + t.Error("trusted-sink should be trusted") + } + if trustedNo { + t.Error("untrusted-sink should not be trusted") + } +} + +// TestRemoveWriter_ReturnsWriter verifies the removed writer is returned to the caller. +func TestRemoveWriter_ReturnsWriter(t *testing.T) { + t.Parallel() + + log := New(WithConsoleOutput(&bytes.Buffer{})) + noop := &NoOpWriter{} + log.AddWriter("sink", noop) + + removed := log.RemoveWriter("sink") + if removed == nil { + t.Fatal("RemoveWriter should return the removed writer, got nil") + } + + // Allow the worker goroutine to drain before comparing. + waitFor(t, func() bool { return true }, 100*time.Millisecond, 10*time.Millisecond, "drain") + + // The returned value should be the same writer we added. + if removed != noop { + t.Error("RemoveWriter returned a different writer than was registered") + } +} + +// TestRemoveWriter_UnknownName verifies nil is returned for an unregistered name. +func TestRemoveWriter_UnknownName(t *testing.T) { + t.Parallel() + + log := New(WithConsoleOutput(&bytes.Buffer{})) + defer func() { _ = log.Close() }() + + if got := log.RemoveWriter("nonexistent"); got != nil { + t.Errorf("RemoveWriter for unknown name should return nil, got %T", got) + } +} + +// TestWriter_Accessor verifies Logger.Writer returns the registered writer. +func TestWriter_Accessor(t *testing.T) { + t.Parallel() + + log := New(WithConsoleOutput(&bytes.Buffer{})) + noop := &NoOpWriter{} + log.AddWriter("sink", noop) + defer func() { _ = log.Close() }() + + got := log.Writer("sink") + if got == nil { + t.Fatal("Logger.Writer should return the registered writer") + } + if got != noop { + t.Error("Logger.Writer returned wrong writer") + } +} + +// TestWriter_AccessorUnknown verifies nil for unregistered name. +func TestWriter_AccessorUnknown(t *testing.T) { + t.Parallel() + + log := New(WithConsoleOutput(&bytes.Buffer{})) + defer func() { _ = log.Close() }() + + if got := log.Writer("nope"); got != nil { + t.Errorf("Logger.Writer for unknown name should be nil, got %T", got) + } +} + +// TestJSONWriter_Flush verifies Flush compiles and runs without error on a plain buffer. +func TestJSONWriter_Flush(t *testing.T) { + t.Parallel() + + jw := NewJSONWriter(&bytes.Buffer{}) + if err := jw.Flush(); err != nil { + t.Errorf("Flush on plain buffer should be no-op: %v", err) + } +} diff --git a/writer_console.go b/writer_console.go index 5b5e2b8..dd63a3b 100644 --- a/writer_console.go +++ b/writer_console.go @@ -1,14 +1,14 @@ package velocity import ( + "bytes" "fmt" "io" "math" "strconv" + "strings" "sync" "time" - - "golang.org/x/term" ) type ConsoleWriter struct { @@ -37,22 +37,28 @@ func NewConsoleWriterWithTimezone(out io.Writer, theme *Theme, displayTimezone * } func NewConsoleWriterWithOptions(out io.Writer, theme *Theme, displayTimezone *time.Location, fieldDisplayMode FieldDisplayMode) *ConsoleWriter { - // Track if theme was explicitly nil for disabling colors - useColours := true + // nil theme means "use the default" — not "disable colour". + // Colour is disabled by passing noColourTheme explicitly (see newFromConfig). if theme == nil { theme = ThemeNightOwl - useColours = false // Explicitly disable colors when theme is nil - } else { - // Ensure ANSI sequences are populated for user-defined themes constructed via struct - // literal. ensureCached populates in-place via sync.Once, so it is safe to call - // concurrently and always returns the same pointer. - theme = ensureCached(theme) } + themeHasColour := !theme.noColour if displayTimezone == nil { displayTimezone = time.Local } + // Resolve whether this writer should emit ANSI sequences. This checks + // NO_COLOR / FORCE_COLOR first, then falls back to fd-level detection. + // On Windows, terminal emulators often proxy stdout as a named pipe; + // FORCE_COLOR=1 is the escape hatch for those environments. + isTTY := resolveColourForWriter(out) + + // useColours is true only when both the writer can render colour AND the + // theme actually carries colour slots. A no-colour theme (noColourTheme, + // ThemeMono) always produces plain output regardless of TTY state. + useColours := isTTY && themeHasColour + templateCopy := *TemplateDefault templateCopy.fieldDisplayMode = fieldDisplayMode templateCopy.useColours = useColours @@ -68,20 +74,18 @@ func NewConsoleWriterWithOptions(out io.Writer, theme *Theme, displayTimezone *t timeFunc: time.Now, bufPool: NewBufferPool(), displayTimezone: displayTimezone, + isTTY: isTTY, } - if f, ok := out.(interface{ Fd() uintptr }); ok { - w.isTTY = term.IsTerminal(int(f.Fd())) //nolint:gosec // G115: uintptr fd fits in int on all supported platforms - } - - if w.isTTY && useColours { + if useColours { w.cacheLevelColours() } return w } -// cacheLevelColours pre-computes ANSI codes to avoid allocation during log writes +// cacheLevelColours pre-computes ANSI codes to avoid allocation during log writes. +// The theme carries pre-cached strings from construction, so this is a straight copy. func (w *ConsoleWriter) cacheLevelColours() { if w.theme == nil { return @@ -89,9 +93,156 @@ func (w *ConsoleWriter) cacheLevelColours() { levels := []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} for _, lvl := range levels { - c := w.theme.GetColourForLevel(lvl) - w.levelColours[lvl] = c.ANSI(true) + w.levelColours[lvl] = w.theme.cachedLevelCode(lvl) + } +} + +// WriteStatus renders a status-badged log line for entries produced by Logger.Status. +// The badge replaces the normal level label on TTY; on non-TTY it falls back to the +// standard formatEntrySecure path so the output remains readable without ANSI. +func (w *ConsoleWriter) WriteStatus(e *Entry) error { + return w.WriteStatusSecure(e, w.isTTY, "[REDACTED]") +} + +// WriteStatusSecure is the trust-aware status write path, mirroring WriteSecure. +func (w *ConsoleWriter) WriteStatusSecure(e *Entry, trusted bool, redactionMark string) error { + w.mu.Lock() + if w.closed { + w.mu.Unlock() + return ErrWriterClosed + } + tmpl := w.template + theme := w.theme + tz := w.displayTimezone + isTTY := w.isTTY + w.mu.Unlock() + + tempBuf := GetTemplateBuffer() + defer PutTemplateBuffer(tempBuf) + + switch { + case isTTY && tmpl != nil: + buildStatusLine(tempBuf, e, theme, tz, trusted, redactionMark) + case tmpl != nil: + // Non-TTY: use the standard path so the output is undecorated but complete. + tmpl.buildWithTimezoneSecure(tempBuf, e, theme, tz, trusted, redactionMark) + default: + // Fallback: no template, produce a minimal status line. + fmt.Fprintf(tempBuf, "[%s] %s\n", e.statusKind.String(), e.Message) } + + w.mu.Lock() + _, err := w.out.Write(tempBuf.Bytes()) + w.mu.Unlock() + return err +} + +// buildStatusLine builds the TTY status line into buf: +// timestamp + " " + badge + " " + message + fields + "\n" +// The badge format is '[' + coloured-padded-token + ']' at fixed width. +func buildStatusLine(buf *bytes.Buffer, e *Entry, theme *Theme, tz *time.Location, trusted bool, redactionMark string) { + // Timestamp (reuses AppendFormat to avoid intermediate string alloc). + if !e.Time.IsZero() { + if theme != nil { + buf.WriteString(theme.cachedTimestampFgStr()) + } + displayTime := e.Time.In(tz) + buf.Write(displayTime.AppendFormat(buf.AvailableBuffer(), time.RFC3339)) + if theme != nil { + buf.WriteString(Reset) + } + _ = buf.WriteByte(' ') + } + + // Status badge: '[' + coloured token + ']' — variable width, no padding. + token := e.statusKind.String() + slot := e.statusKind.Slot() + _ = buf.WriteByte('[') + if theme != nil { + prefix, suffix := theme.Wrap(slot) + buf.WriteString(prefix) + buf.WriteString(token) + buf.WriteString(suffix) + } else { + buf.WriteString(token) + } + _ = buf.WriteByte(']') + buf.WriteString(statusBadgeSep) + + // Message (with secure-tag handling). + msg := e.Message + if e.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + if theme != nil { + buf.WriteString(theme.cachedMessageFgStr()) + } + buf.WriteString(msg) + if theme != nil { + buf.WriteString(Reset) + } + + // Caller (if present). + if e.Caller != "" { + _ = buf.WriteByte(' ') + _ = buf.WriteByte('(') + buf.WriteString(e.Caller) + _ = buf.WriteByte(':') + buf.Write(strconv.AppendInt(nil, int64(e.Line), 10)) + _ = buf.WriteByte(')') + } + + // Fields rendered key=value inline. + for _, f := range e.Fields { + _ = buf.WriteByte(' ') + keyCode := "" + valCode := "" + if theme != nil { + keyCode = theme.CachedFieldKeyFg() + if f.Type == FieldTypeError { + valCode = theme.cachedErrorValFgStr() + } else { + valCode = theme.CachedFieldValFg() + } + } + if keyCode != "" { + buf.WriteString(keyCode) + } + buf.WriteString(f.Key) + if keyCode != "" { + buf.WriteString(Reset) + } + _ = buf.WriteByte('=') + if valCode != "" { + buf.WriteString(valCode) + } + // Quote string-like types to match console writer convention. + switch f.Type { + case FieldTypeString, FieldTypeError, FieldTypeStringer, FieldTypeTruncated: + _ = buf.WriteByte('"') + if trusted { + f.writeFormattedTrusted(buf) + } else { + f.writeFormattedWithMark(buf, redactionMark) + } + _ = buf.WriteByte('"') + default: + if trusted { + f.writeFormattedTrusted(buf) + } else { + f.writeFormattedWithMark(buf, redactionMark) + } + } + if valCode != "" { + buf.WriteString(Reset) + } + } + + _ = buf.WriteByte('\n') } func (w *ConsoleWriter) SetTemplate(t *Template) { @@ -106,6 +257,15 @@ func (w *ConsoleWriter) SetTemplate(t *Template) { } func (w *ConsoleWriter) Write(e *Entry) error { + // TTY console writers are trusted by context — they render to a human-facing + // terminal session, not a file or pipeline. Non-TTY consoles are untrusted. + return w.WriteSecure(e, w.isTTY, "[REDACTED]") +} + +// WriteSecure implements SecureWriter. When trusted is true, Secure field +// plaintext is shown and markers are stripped. When false, both are +// replaced with redactionMark. +func (w *ConsoleWriter) WriteSecure(e *Entry, trusted bool, redactionMark string) error { // Snapshot mutable state under a brief lock so formatting runs unlocked. w.mu.Lock() if w.closed { @@ -116,12 +276,11 @@ func (w *ConsoleWriter) Write(e *Entry) error { theme := w.theme tz := w.displayTimezone lvlColours := w.levelColours - isTTY := w.isTTY w.mu.Unlock() if tmpl != nil { tempBuf := GetTemplateBuffer() - tmpl.buildWithTimezone(tempBuf, e, theme, tz) + tmpl.buildWithTimezoneSecure(tempBuf, e, theme, tz, trusted, redactionMark) w.mu.Lock() _, err := w.out.Write(tempBuf.Bytes()) @@ -133,7 +292,7 @@ func (w *ConsoleWriter) Write(e *Entry) error { rawBuf := w.bufPool.Get(HintConsoleLog) buf := NewBytesBuffer(rawBuf) - w.formatEntryWithSnap(buf, e, theme, tz, lvlColours, isTTY) + w.formatEntrySecure(buf, e, theme, tz, lvlColours, trusted, redactionMark) //nolint:staticcheck // intentional: lvlColours unused when not TTY w.mu.Lock() _, err := w.out.Write(buf.Bytes()) @@ -149,25 +308,35 @@ func (w *ConsoleWriter) Write(e *Entry) error { return nil } -// formatEntryWithSnap formats using snapshotted state, safe to call without the mutex. -func (w *ConsoleWriter) formatEntryWithSnap(buf *BytesBuffer, e *Entry, theme *Theme, tz *time.Location, lvlColours [6]string, isTTY bool) { +// formatEntrySecure formats an entry using snapshotted state, applying redaction +// when trusted is false. Safe to call without the mutex. +func (w *ConsoleWriter) formatEntrySecure(buf *BytesBuffer, e *Entry, theme *Theme, tz *time.Location, lvlColours [6]string, trusted bool, redactionMark string) { buf.WriteString("[") displayTime := e.Time.In(tz) buf.AppendTime(displayTime, time.RFC3339) buf.WriteString("] ") _ = buf.WriteByte('[') - if isTTY && theme != nil && e.Level >= 0 && int(e.Level) < len(lvlColours) { + if trusted && theme != nil && e.Level >= 0 && int(e.Level) < len(lvlColours) { buf.WriteString(lvlColours[e.Level]) } buf.WriteString(e.Level.ConciseLabel()) - if isTTY && theme != nil && e.Level >= 0 && int(e.Level) < len(lvlColours) { + if trusted && theme != nil && e.Level >= 0 && int(e.Level) < len(lvlColours) { buf.WriteString(Reset) } _ = buf.WriteByte(']') buf.WriteString(" ") - buf.WriteString(e.Message) + + msg := e.Message + if e.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + buf.WriteString(msg) if e.Caller != "" { buf.WriteString(" (") @@ -178,7 +347,7 @@ func (w *ConsoleWriter) formatEntryWithSnap(buf *BytesBuffer, e *Entry, theme *T } if len(e.Fields) > 0 { - w.formatFields(buf, e.Fields) + w.formatFieldsSecure(buf, e.Fields, trusted, redactionMark) } } @@ -198,16 +367,50 @@ func (w *ConsoleWriter) formatLevel(buf *BytesBuffer, level Level) { _ = buf.WriteByte(']') } -func (w *ConsoleWriter) formatFields(buf *BytesBuffer, fields []Field) { +func (w *ConsoleWriter) formatFieldsSecure(buf *BytesBuffer, fields []Field, trusted bool, redactionMark string) { for _, f := range fields { _ = buf.WriteByte(' ') buf.WriteString(f.Key) buf.WriteString(": ") - w.formatValue(buf, f) + w.formatValueSecure(buf, f, trusted, redactionMark) } } -func (*ConsoleWriter) formatValue(buf *BytesBuffer, f Field) { +func (*ConsoleWriter) formatValueSecure(buf *BytesBuffer, f Field, trusted bool, redactionMark string) { + switch f.Type { + case FieldTypeSecure, FieldTypeSecureURL: + if trusted && f.value != nil { + _ = buf.WriteByte('"') + buf.WriteString((*secureValue)(f.value).plain) + _ = buf.WriteByte('"') + } else { + // Emit field-level redacted form (e.g. URL with password replaced) + // rather than the generic writer mark, so structured context is preserved. + if f.value != nil { + _ = buf.WriteByte('"') + buf.WriteString((*secureValue)(f.value).redacted) + _ = buf.WriteByte('"') + } else { + buf.WriteString(redactionMark) + } + } + return + case FieldTypeRedacted: + buf.WriteString(redactionMark) + return + case FieldTypeTruncated: + if f.value != nil { + _ = buf.WriteByte('"') + buf.WriteString(*(*string)(f.value)) + _ = buf.WriteByte('"') + } + return + default: + consoleFormatValueCore(buf, f) + } +} + +func consoleFormatValueCore(buf *BytesBuffer, f Field) { switch f.Type { case FieldTypeString: v := *(*string)(f.value) @@ -287,11 +490,245 @@ func (*ConsoleWriter) formatValue(buf *BytesBuffer, f Field) { // that fmt.Sprintf("%v", v) would produce. _, _ = fmt.Fprintf(buf, "%v", v) + case FieldTypeSecure, FieldTypeSecureURL, FieldTypeRedacted, FieldTypeTruncated: + // Handled upstream by formatValueSecure before consoleFormatValueCore is called. + + case FieldTypeGroupItems: + // Group items are rendered by WriteGroup; in generic paths emit a hint. + if f.value != nil { + items := *(*[]GroupItem)(f.value) + var tmp [20]byte + n := formatInt(tmp[:], int64(len(items))) + _ = buf.WriteByte('[') + _, _ = buf.Write(tmp[:n]) + buf.WriteString(" items]") + } + + case FieldTypeContinuationLines: + // Continuation lines are rendered by WriteContinue; in generic paths emit a hint. + if f.value != nil { + lines := *(*[]string)(f.value) + var tmp [20]byte + n := formatInt(tmp[:], int64(len(lines))) + _ = buf.WriteByte('[') + _, _ = buf.Write(tmp[:n]) + buf.WriteString(" lines]") + } + case FieldTypeUnknown: // Unknown field type - write nothing } } +// WriteGroup renders a Group log entry. On TTY it emits the coloured count header +// followed by indented item lines; on non-TTY it falls back to the standard +// template path for the header and appends plain item lines. +func (w *ConsoleWriter) WriteGroup(e *Entry, items []GroupItem) error { + return w.WriteGroupSecure(e, items, w.isTTY, "[REDACTED]") +} + +// WriteGroupSecure is the trust-aware group write path. +func (w *ConsoleWriter) WriteGroupSecure(e *Entry, items []GroupItem, trusted bool, redactionMark string) error { + w.mu.Lock() + if w.closed { + w.mu.Unlock() + return ErrWriterClosed + } + tmpl := w.template + theme := w.theme + tz := w.displayTimezone + isTTY := w.isTTY + w.mu.Unlock() + + tempBuf := GetTemplateBuffer() + defer PutTemplateBuffer(tempBuf) + + switch { + case isTTY && tmpl != nil: + // On TTY: coloured level + count-coloured message header, then indented item lines. + buildGroupLineTTY(tempBuf, e, theme, tz, trusted, redactionMark, items, tmpl) + case tmpl != nil: + // Non-TTY: standard template for the header (level + plain message with count), + // then plain item lines appended directly. + tmpl.buildWithTimezoneSecure(tempBuf, e, theme, tz, trusted, redactionMark) + // The template appends a trailing '\n'; item lines follow without extra spacing. + writeGroupConsoleItems(tempBuf, items) + default: + fmt.Fprintf(tempBuf, "%s\n", e.Message) + writeGroupConsoleItems(tempBuf, items) + } + + w.mu.Lock() + _, err := w.out.Write(tempBuf.Bytes()) + w.mu.Unlock() + return err +} + +// buildGroupLineTTY builds the full TTY group block: timestamp + level + coloured +// message+count header, then indented+coloured item lines. +func buildGroupLineTTY(buf *bytes.Buffer, e *Entry, theme *Theme, tz *time.Location, trusted bool, redactionMark string, items []GroupItem, tmpl *Template) { + // Timestamp. + if !e.Time.IsZero() { + if theme != nil { + buf.WriteString(theme.cachedTimestampFgStr()) + } + displayTime := e.Time.In(tz) + buf.Write(displayTime.AppendFormat(buf.AvailableBuffer(), time.RFC3339)) + if theme != nil { + buf.WriteString(Reset) + } + buf.WriteByte(' ') + } + + // Level badge "[INFO]". + if theme != nil { + buf.WriteString(theme.cachedLevelCode(e.Level)) + } + buf.WriteByte('[') + buf.WriteString(e.Level.ConciseLabel()) + buf.WriteByte(']') + if theme != nil { + buf.WriteString(Reset) + } + buf.WriteByte(' ') + + // Message (secure-aware, with count rendered in SlotCount colour). + msg := e.Message + if e.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + + // msg here is already "text (N)" — we need to split out the " (N)" suffix and + // re-render it with the SlotCount colour. Find the last " (" which we inserted. + // This is safe because groupMsgWithCount always appends " (N)". + if idx := strings.LastIndex(msg, " ("); idx >= 0 { + head := msg[:idx] + tail := msg[idx+2 : len(msg)-1] // extract the count digits only + if theme != nil { + buf.WriteString(theme.CachedMessageFg()) + } + buf.WriteString(head) + if theme != nil { + buf.WriteString(Reset) + } + buf.WriteString(" (") + countPrefix, countSuffix := theme.Wrap(SlotCount) + buf.WriteString(countPrefix) + buf.WriteString(tail) + buf.WriteString(countSuffix) + buf.WriteByte(')') + } else { + if theme != nil { + buf.WriteString(theme.CachedMessageFg()) + } + buf.WriteString(msg) + if theme != nil { + buf.WriteString(Reset) + } + } + buf.WriteByte('\n') + + // Item lines indented to the message column so they sit flush under the header. + indent := tmpl.CachedMessageIndentStr() + for i, item := range items { + marker := resolvedMarker(item.Marker, i, len(items)) + buf.WriteString(indent) + buf.WriteString(groupItemIndent) + if theme != nil { + buf.WriteString(theme.CachedFieldKeyFg()) + } + buf.WriteString(marker) + buf.WriteByte(' ') + if theme != nil { + buf.WriteString(Reset) + buf.WriteString(theme.CachedMessageFg()) + } + buf.WriteString(item.Text) + if theme != nil { + buf.WriteString(Reset) + } + buf.WriteByte('\n') + } +} + +// WriteContinue renders a ContinuationBlock log entry. The header line is the +// standard log line; continuation lines follow with the │ glyph prefix indented +// to the message column so they land flush under the header text. +func (w *ConsoleWriter) WriteContinue(e *Entry, lines []string) error { + return w.WriteContinueSecure(e, lines, w.isTTY, "[REDACTED]") +} + +// WriteContinueSecure is the trust-aware continuation write path. +func (w *ConsoleWriter) WriteContinueSecure(e *Entry, lines []string, trusted bool, redactionMark string) error { + w.mu.Lock() + if w.closed { + w.mu.Unlock() + return ErrWriterClosed + } + tmpl := w.template + theme := w.theme + tz := w.displayTimezone + isTTY := w.isTTY + w.mu.Unlock() + + tempBuf := GetTemplateBuffer() + defer PutTemplateBuffer(tempBuf) + + switch { + case isTTY && tmpl != nil: + buildContinueLineTTY(tempBuf, e, theme, tz, trusted, redactionMark, lines, tmpl) + case tmpl != nil: + // Non-TTY: standard template for the header, then plain continuation lines. + tmpl.buildWithTimezoneSecure(tempBuf, e, theme, tz, trusted, redactionMark) + writeContinuationLines(tempBuf, lines, tmpl.CachedMessageIndentStr(), false, nil) + default: + fmt.Fprintf(tempBuf, "%s\n", e.Message) + writeContinuationLines(tempBuf, lines, "", false, nil) + } + + w.mu.Lock() + _, err := w.out.Write(tempBuf.Bytes()) + w.mu.Unlock() + return err +} + +// buildContinueLineTTY builds the full TTY continuation block: the standard log +// header (timestamp + level badge + message) then each continuation line indented +// to the message column with a SlotContinuation-coloured │ glyph. +func buildContinueLineTTY(buf *bytes.Buffer, e *Entry, theme *Theme, tz *time.Location, trusted bool, redactionMark string, lines []string, tmpl *Template) { + // Header: identical to the standard TTY log line. + tmpl.buildWithTimezoneSecure(buf, e, theme, tz, trusted, redactionMark) + // buildWithTimezoneSecure appends '\n'; continuation lines follow directly. + writeContinuationLines(buf, lines, tmpl.CachedMessageIndentStr(), true, theme) +} + +// writeContinuationLines appends each line prefixed with the message-column indent +// and the │ glyph. When styled is true and theme is non-nil, the glyph is wrapped +// with SlotContinuation ANSI codes. +func writeContinuationLines(buf *bytes.Buffer, lines []string, indent string, styled bool, theme *Theme) { + var glyphPrefix, glyphSuffix string + if styled && theme != nil { + glyphPrefix, glyphSuffix = theme.Wrap(SlotContinuation) + } + + for _, line := range lines { + buf.WriteString(indent) + if glyphPrefix != "" { + buf.WriteString(glyphPrefix) + } + buf.WriteString(continuationGlyphSep) + if glyphSuffix != "" { + buf.WriteString(glyphSuffix) + } + buf.WriteString(line) + buf.WriteByte('\n') + } +} + func (w *ConsoleWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() @@ -301,17 +738,25 @@ func (w *ConsoleWriter) Close() error { } w.closed = true + // Sync is best-effort: pipes and redirected streams reject it on Windows. if s, ok := w.out.(interface{ Sync() error }); ok { - return s.Sync() + _ = s.Sync() } return nil } +// SetTheme replaces the active theme. When colour was disabled at construction +// (e.g. no-colour theme or non-TTY writer) it stays disabled — switching to a +// coloured theme on a non-TTY writer does not re-enable ANSI output. +// When the writer is a TTY and the new theme has colour, colour is re-enabled. func (w *ConsoleWriter) SetTheme(theme *Theme) { w.mu.Lock() defer w.mu.Unlock() w.theme = theme + // Re-derive useColours from current isTTY and new theme state. + themeHasColour := theme != nil && !theme.noColour + w.template.useColours = w.isTTY && themeHasColour w.cacheLevelColours() } diff --git a/writer_console_rb.go b/writer_console_rb.go index daf607e..3da89f8 100644 --- a/writer_console_rb.go +++ b/writer_console_rb.go @@ -19,6 +19,11 @@ type ConsoleWriterRB struct { ringBuffer *RingBuffer closed atomic.Bool + // isTTY mirrors ConsoleWriter's trust model: TTY = trusted (human terminal), + // non-TTY = untrusted (pipe or file). The template is rendered via the secure + // path so Secure fields are redacted when piping to a file or non-TTY sink. + isTTY bool + mu sync.Mutex // Protects theme and template writes atomic.Uint64 errors atomic.Uint64 @@ -35,15 +40,14 @@ func NewConsoleWriterRB(out io.Writer, theme *Theme, displayTimezone *time.Locat displayTimezone = time.Local } - // Ensure ANSI sequences are populated. ensureCached populates in-place via sync.Once, - // so it is safe to call concurrently and returns the same pointer. - theme = ensureCached(theme) + // Themes are immutable from NewTheme — ANSI codes already populated. w := &ConsoleWriterRB{ out: actualOut, theme: theme, bufPool: NewBufferPool(), displayTimezone: displayTimezone, + isTTY: resolveColourForWriter(actualOut), } w.ringBuffer = NewRingBuffer(actualOut, DefaultRingBufferSize) @@ -88,7 +92,10 @@ func (w *ConsoleWriterRB) Write(e *Entry) error { if hasTemplate { tempBuf := GetTemplateBuffer() defer PutTemplateBuffer(tempBuf) - template.buildWithTimezone(tempBuf, e, theme, w.displayTimezone) + // Use the trust-aware path so Secure fields are redacted on non-TTY output + // (e.g. piped to a file). TTY writers are treated as trusted — same model + // as ConsoleWriter which passes isTTY as the trusted flag. + template.buildWithTimezoneSecure(tempBuf, e, theme, w.displayTimezone, w.isTTY, "[REDACTED]") formattedData = tempBuf.Bytes() } else { buf := NewBytesBuffer(rawBuf) diff --git a/writer_console_rb_test.go b/writer_console_rb_test.go index e43a6ac..153cb1d 100644 --- a/writer_console_rb_test.go +++ b/writer_console_rb_test.go @@ -6,6 +6,46 @@ import ( "time" ) +// TestConsoleWriterRB_SecureFieldRedactedOnNonTTY is a regression test for the bug +// where ConsoleWriterRB always passed trusted=true to the template, meaning Secure +// fields were rendered in plaintext even when the writer was not a terminal. +// The fix uses isTTY (false for bytes.Buffer) so Secure fields are redacted. +func TestConsoleWriterRB_SecureFieldRedactedOnNonTTY(t *testing.T) { + t.Parallel() + + // Use safeBuffer (mutex-protected) because the ring buffer flusher goroutine + // writes to it concurrently with the test's Len() poll in waitFor. + var buf safeBuffer + // Use a theme so the template path is exercised (not the fallback formatEntry path). + w := NewConsoleWriterRB(&buf, ThemeNightOwl, nil, FieldDisplayInline) + // safeBuffer is not a *os.File, so resolveColourForWriter returns false → isTTY=false → untrusted. + + entry := GetEntry() + entry.SetLevel(LevelInfo) + entry.SetMessage("login") + entry.WithFields(Secure("password", "s3cr3t")) + entry.SetTime(time.Now()) + entry.Write() + + if err := w.Write(entry); err != nil { + t.Fatalf("Write returned error: %v", err) + } + + waitFor(t, func() bool { + return buf.Len() > 0 + }, 5*time.Second, 5*time.Millisecond, "data should flush from ConsoleWriterRB") + + _ = w.Close() + + output := buf.String() + if strings.Contains(output, "s3cr3t") { + t.Errorf("Secure field plaintext leaked to non-TTY ConsoleWriterRB: %q", output) + } + if !strings.Contains(output, "[REDACTED]") { + t.Errorf("expected [REDACTED] in non-TTY ConsoleWriterRB output, got: %q", output) + } +} + func TestConsoleWriterRB_Timezone(t *testing.T) { var buf safeBuffer diff --git a/writer_console_test.go b/writer_console_test.go index 677cf06..5e7611a 100644 --- a/writer_console_test.go +++ b/writer_console_test.go @@ -22,14 +22,14 @@ func TestConsoleWriter_InvalidLevel(_ *testing.T) { func TestConsoleWriter_AddCaller(t *testing.T) { var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = &buf cfg.ConsoleLevel = LevelDebug cfg.StructuredOutput = nil cfg.ConsoleTheme = nil cfg.AddCaller = true - log := NewWithConfig(cfg) + log := newFromConfig(cfg) log.Info("caller console test") if !strings.Contains(buf.String(), "_test.go:") { @@ -56,6 +56,147 @@ func TestTemplate_CachedPrefixWidth_BadgeStyle(t *testing.T) { } } +// TestConsoleWriter_ColourEmittedWhenNoThemeSet verifies that a ConsoleWriter constructed +// without an explicit theme (nil → default NightOwl) emits ANSI escape sequences when the +// writer is a TTY. Previously, nil theme silently disabled colour even when DisableColour +// was false. +// +// Technique: construct the writer, then flip isTTY=true and template.useColours=true to +// simulate a real terminal, bypassing the io.Writer TTY probe which never fires on a buffer. +// Both fields must be set together because template.useColours gates ANSI in the template +// path, and isTTY gates the level-colour cache used by the fallback formatEntrySecure path. +func TestConsoleWriter_ColourEmittedWhenNoThemeSet(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + // nil theme → should default to NightOwl with colour enabled. + w := NewConsoleWriter(&buf, nil) + // Simulate a real terminal — both flags must be updated together because + // template.useColours drives ANSI in the template path while isTTY drives + // the level-colour cache used by the fallback formatEntrySecure path. + w.isTTY = true + w.template.useColours = true + w.cacheLevelColours() + + // At least one level colour must be non-empty — NightOwl defines them all. + hasColour := false + for _, code := range w.levelColours { + if code != "" { + hasColour = true + break + } + } + if !hasColour { + t.Error("expected at least one cached level colour after nil-theme construction with isTTY=true, got none") + } + + // Writing a log entry to the writer should produce ANSI escapes. + entry := GetEntry() + defer entry.Release() + entry.SetLevel(LevelInfo) + entry.SetMessage("colour test") + entry.SetTime(entry.Time) // keep zero time — not what we are testing + + if err := w.Write(entry); err != nil { + t.Fatalf("Write: %v", err) + } + + output := buf.String() + if !strings.Contains(output, "\033[") { + t.Errorf("expected ANSI escapes in output with nil theme + isTTY, got: %q", output) + } +} + +// TestConsoleWriter_NoColourWhenDisabled verifies that a ConsoleWriter constructed +// with noColourTheme (the DisableColour path in newFromConfig) emits no ANSI escapes, +// even when isTTY is forced true. +func TestConsoleWriter_NoColourWhenDisabled(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + // Simulate the DisableColour=true path: logger.go passes noColourTheme. + w := NewConsoleWriter(&buf, noColourTheme) + w.isTTY = true + w.cacheLevelColours() + + // No level colour should be cached for a no-colour theme. + for i, code := range w.levelColours { + if code != "" { + t.Errorf("levelColours[%d] = %q, want empty for noColourTheme", i, code) + } + } + + entry := GetEntry() + defer entry.Release() + entry.SetLevel(LevelInfo) + entry.SetMessage("no colour test") + + if err := w.Write(entry); err != nil { + t.Fatalf("Write: %v", err) + } + + output := buf.String() + if strings.Contains(output, "\033[") { + t.Errorf("expected no ANSI escapes in output with noColourTheme, got: %q", output) + } +} + +// TestLogger_DevelopmentPresetHasColour verifies that a logger built with +// WithDevelopment() creates a console writer whose theme has non-empty level +// colours when isTTY is forced true. This guards the DisableColour=false → colour +// path that was broken by the original writer_console.go nil-theme logic. +func TestLogger_DevelopmentPresetHasColour(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + log := New(WithDevelopment(), WithConsoleOutput(&buf)) + if log.consoleWriter == nil { + t.Fatal("expected consoleWriter to be set for WithDevelopment") + } + + // Force TTY mode and rebuild colour cache to exercise the theme path. + log.consoleWriter.isTTY = true + log.consoleWriter.cacheLevelColours() + + hasColour := false + for _, code := range log.consoleWriter.levelColours { + if code != "" { + hasColour = true + break + } + } + if !hasColour { + t.Error("WithDevelopment() logger should have cached level colours when isTTY=true") + } +} + +// TestLogger_ProductionPresetNoColour verifies that a logger built with +// WithProduction() (DisableColour=true) produces a console writer with no cached +// colour codes even when isTTY is forced true. +func TestLogger_ProductionPresetNoColour(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + // WithProduction sets ConsoleOutput to io.Discard, so override it to get a writer. + cfg := defaultConfig() + WithProduction()(cfg) + cfg.ConsoleOutput = &buf + log := newFromConfig(cfg) + + if log.consoleWriter == nil { + t.Fatal("expected consoleWriter to be set after overriding ConsoleOutput") + } + + log.consoleWriter.isTTY = true + log.consoleWriter.cacheLevelColours() + + for i, code := range log.consoleWriter.levelColours { + if code != "" { + t.Errorf("WithProduction() levelColours[%d] = %q, want empty (DisableColour=true)", i, code) + } + } +} + // TestConsoleWriter_CachedWidthsAfterFieldDisplayMutation verifies that constructing a // ConsoleWriter with FieldDisplayTree produces a template whose cached widths match those // of TemplateDefault (field display mode does not affect timestamp or level widths). The @@ -88,3 +229,120 @@ func TestConsoleWriter_CachedWidthsAfterFieldDisplayMutation(t *testing.T) { t.Error("TemplateDefault was mutated — shallow copy semantics broken") } } + +// TestEnvVar_NoColour verifies that NO_COLOR=1 suppresses ANSI output even when +// the writer would otherwise be treated as a TTY (isTTY forced true for the test). +func TestEnvVar_NoColour(t *testing.T) { + t.Setenv("NO_COLOR", "1") + t.Setenv("FORCE_COLOR", "") // ensure FORCE_COLOR does not override + + var buf bytes.Buffer + w := NewConsoleWriter(&buf, ThemeNightOwl) + + // Even forcing isTTY won't emit ANSI when NO_COLOR is set, because + // resolveColourForWriter() checked NO_COLOR at construction time. + // The field stays false regardless. + if w.isTTY { + t.Error("expected isTTY=false when NO_COLOR is set, but got true") + } + if w.template.useColours { + t.Error("expected template.useColours=false when NO_COLOR is set, but got true") + } + + entry := GetEntry() + defer entry.Release() + entry.SetLevel(LevelInfo) + entry.SetMessage("no colour via env") + + if err := w.Write(entry); err != nil { + t.Fatalf("Write: %v", err) + } + + output := buf.String() + if strings.Contains(output, "\033[") { + t.Errorf("NO_COLOR=1: expected no ANSI escapes, got: %q", output) + } +} + +// TestEnvVar_ForceColour verifies that FORCE_COLOR=1 enables ANSI output even +// when the writer is a *bytes.Buffer (which would normally be non-TTY). +func TestEnvVar_ForceColour(t *testing.T) { + t.Setenv("FORCE_COLOR", "1") + t.Setenv("NO_COLOR", "") // ensure NO_COLOR does not suppress + + var buf bytes.Buffer + w := NewConsoleWriter(&buf, ThemeNightOwl) + + if !w.isTTY { + t.Error("expected isTTY=true when FORCE_COLOR is set, but got false") + } + if !w.template.useColours { + t.Error("expected template.useColours=true when FORCE_COLOR is set, but got false") + } + + entry := GetEntry() + defer entry.Release() + entry.SetLevel(LevelInfo) + entry.SetMessage("forced colour via env") + + if err := w.Write(entry); err != nil { + t.Fatalf("Write: %v", err) + } + + output := buf.String() + if !strings.Contains(output, "\033[") { + t.Errorf("FORCE_COLOR=1: expected ANSI escapes, got: %q", output) + } +} + +// TestStatusItem_ColourViaLoggerRender verifies that a StatusItem rendered via +// Logger.Render emits ANSI colour sequences when the console writer is in TTY +// mode. This guards the two-step buffer path in Logger.Render where IsTerminalWriter +// on the intermediate buffer would always return false without TTYRenderable. +func TestStatusItem_ColourViaLoggerRender(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + log := New(WithDevelopment(), WithConsoleOutput(&buf)) + if log.consoleWriter == nil { + t.Fatal("expected consoleWriter to be set") + } + + // Force TTY so colour is enabled. + log.consoleWriter.isTTY = true + log.consoleWriter.template.useColours = true + log.consoleWriter.cacheLevelColours() + + item := NewStatusItem(StatusOK, "service healthy", log.Style()) + log.Render(item) + + output := buf.String() + if !strings.Contains(output, "\033[") { + t.Errorf("StatusItem via Logger.Render with isTTY=true: expected ANSI escapes, got: %q", output) + } +} + +// TestStatusItem_NoColourViaLoggerRenderWhenNotTTY verifies that a StatusItem +// rendered via Logger.Render emits plain text when the console writer is not a TTY. +func TestStatusItem_NoColourViaLoggerRenderWhenNotTTY(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + log := New(WithDevelopment(), WithConsoleOutput(&buf)) + if log.consoleWriter == nil { + t.Fatal("expected consoleWriter to be set") + } + // isTTY stays false — buf is not a terminal. + + item := NewStatusItem(StatusOK, "service healthy", log.Style()) + log.Render(item) + + output := buf.String() + if strings.Contains(output, "\033[") { + t.Errorf("StatusItem via Logger.Render with isTTY=false: expected no ANSI escapes, got: %q", output) + } + // The badge itself must still appear. + if !strings.Contains(output, "[OKAY]") { + t.Errorf("StatusItem via Logger.Render: expected [OKAY] badge in plain output, got: %q", output) + } +} diff --git a/writer_json.go b/writer_json.go index 761ec84..5b9e8d2 100644 --- a/writer_json.go +++ b/writer_json.go @@ -5,6 +5,7 @@ import ( "io" "math" "strconv" + "strings" "sync" "time" ) @@ -24,11 +25,215 @@ func NewJSONWriter(out io.Writer) *JSONWriter { } func (w *JSONWriter) Write(e *Entry) error { + // JSON writer is never trusted — always redact. + return w.WriteSecure(e, false, "[REDACTED]") +} + +// WriteStatus emits a status-aware JSON line. The StatusKind is serialised as +// a "status" field with a lowercase value (e.g. "ok", "fail"). All other fields +// are serialised normally via WriteSecure. The badge text is never embedded in +// the message — JSON consumers must read the "status" field. +func (w *JSONWriter) WriteStatus(e *Entry) error { + return w.WriteStatusSecure(e, false, "[REDACTED]") +} + +// WriteStatusSecure is the trust-aware JSON status write path. +func (w *JSONWriter) WriteStatusSecure(e *Entry, trusted bool, redactionMark string) error { + rawBuf := w.bufPool.Get(HintStructuredLog) + buf := NewBytesBuffer(rawBuf) + + w.formatJSONStatusSecure(buf, e, trusted, redactionMark) + + w.mu.Lock() + if w.closed { + w.mu.Unlock() + w.bufPool.Put(rawBuf) + return ErrWriterClosed + } + _, err := w.out.Write(buf.Bytes()) + if err == nil { + _, err = w.out.Write(newlineByte) + } + w.mu.Unlock() + + w.bufPool.Put(rawBuf) + if err != nil { + return fmt.Errorf("json write failed: %w", err) + } + return nil +} + +func (w *JSONWriter) formatJSONStatusSecure(buf *BytesBuffer, e *Entry, trusted bool, redactionMark string) { + _ = buf.WriteByte('{') + + w.writeJSONString(buf, "timestamp") + _ = buf.WriteByte(':') + w.writeJSONTime(buf, e.Time) + + _ = buf.WriteByte(',') + w.writeJSONString(buf, "level") + _ = buf.WriteByte(':') + w.writeJSONString(buf, e.Level.String()) + + // Emit the status field before message so consumers can filter without parsing. + _ = buf.WriteByte(',') + w.writeJSONString(buf, "status") + _ = buf.WriteByte(':') + w.writeJSONString(buf, e.statusKind.statusJSONValue()) + + _ = buf.WriteByte(',') + w.writeJSONString(buf, "message") + _ = buf.WriteByte(':') + msg := e.Message + if e.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + w.writeJSONString(buf, msg) + + if e.Caller != "" { + _ = buf.WriteByte(',') + w.writeJSONString(buf, "caller") + _ = buf.WriteByte(':') + w.writeJSONString(buf, e.Caller) + _ = buf.WriteByte(',') + w.writeJSONString(buf, "line") + _ = buf.WriteByte(':') + buf.WriteInt(int64(e.Line)) + } + + for _, f := range e.Fields { + _ = buf.WriteByte(',') + w.writeJSONString(buf, f.Key) + _ = buf.WriteByte(':') + w.writeJSONFieldValueSecure(buf, f, trusted, redactionMark) + } + + _ = buf.WriteByte('}') +} + +// WriteGroup emits a JSON entry for a Logger.Group call. +// The entry message is the plain header string "msg (N)"; the structured fields +// "count" (int) and "items" (string array, markers stripped) are added. +func (w *JSONWriter) WriteGroup(e *Entry, items []GroupItem) error { + return w.WriteGroupSecure(e, items, false, "[REDACTED]") +} + +// WriteGroupSecure is the trust-aware group JSON write path. +func (w *JSONWriter) WriteGroupSecure(e *Entry, items []GroupItem, trusted bool, redactionMark string) error { + rawBuf := w.bufPool.Get(HintStructuredLog) + buf := NewBytesBuffer(rawBuf) + + w.formatJSONGroupSecure(buf, e, items, trusted, redactionMark) + + w.mu.Lock() + if w.closed { + w.mu.Unlock() + w.bufPool.Put(rawBuf) + return ErrWriterClosed + } + _, err := w.out.Write(buf.Bytes()) + if err == nil { + _, err = w.out.Write(newlineByte) + } + w.mu.Unlock() + + w.bufPool.Put(rawBuf) + if err != nil { + return fmt.Errorf("json write failed: %w", err) + } + return nil +} + +func (w *JSONWriter) formatJSONGroupSecure(buf *BytesBuffer, e *Entry, items []GroupItem, trusted bool, redactionMark string) { + _ = buf.WriteByte('{') + + w.writeJSONString(buf, "timestamp") + _ = buf.WriteByte(':') + w.writeJSONTime(buf, e.Time) + + _ = buf.WriteByte(',') + w.writeJSONString(buf, "level") + _ = buf.WriteByte(':') + w.writeJSONString(buf, e.Level.String()) + + // Emit message without the " (N)" suffix — the count field carries that. + _ = buf.WriteByte(',') + w.writeJSONString(buf, "message") + _ = buf.WriteByte(':') + // Strip the " (N)" suffix from the composite message so the JSON message is clean. + msg := e.Message + if e.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + // groupMsgWithCount always appends " (N)"; strip it back out for the JSON message. + if idx := strings.LastIndex(msg, " ("); idx >= 0 { + msg = msg[:idx] + } + w.writeJSONString(buf, msg) + + if e.Caller != "" { + _ = buf.WriteByte(',') + w.writeJSONString(buf, "caller") + _ = buf.WriteByte(':') + w.writeJSONString(buf, e.Caller) + _ = buf.WriteByte(',') + w.writeJSONString(buf, "line") + _ = buf.WriteByte(':') + buf.WriteInt(int64(e.Line)) + } + + // Count and items array: markers are visual-only, JSON carries only text. + _ = buf.WriteByte(',') + w.writeJSONString(buf, groupCountKey) + _ = buf.WriteByte(':') + var tmp [20]byte + n := formatInt(tmp[:], int64(len(items))) + _, _ = buf.Write(tmp[:n]) + + _ = buf.WriteByte(',') + w.writeJSONString(buf, groupItemsKey) + _ = buf.WriteByte(':') + _ = buf.WriteByte('[') + for i, item := range items { + if i > 0 { + _ = buf.WriteByte(',') + } + w.writeJSONString(buf, item.Text) + } + _ = buf.WriteByte(']') + + // Any additional Fields on the entry (base fields from With()). + for _, f := range e.Fields { + if f.Type == FieldTypeGroupItems { + continue // already emitted above + } + _ = buf.WriteByte(',') + w.writeJSONString(buf, f.Key) + _ = buf.WriteByte(':') + w.writeJSONFieldValueSecure(buf, f, trusted, redactionMark) + } + + _ = buf.WriteByte('}') +} + +// WriteSecure implements SecureWriter. trusted controls whether Secure field +// values are emitted as plaintext or as redactionMark. JSON writers are typically +// called with trusted=false; a trusted JSON sink (e.g. an internal audit log) +// can be registered via AddWriter with WriterTrusted(). +func (w *JSONWriter) WriteSecure(e *Entry, trusted bool, redactionMark string) error { rawBuf := w.bufPool.Get(HintStructuredLog) buf := NewBytesBuffer(rawBuf) // Format entirely outside the lock; entry is immutable at this point. - w.formatJSON(buf, e) + w.formatJSONSecure(buf, e, trusted, redactionMark) w.mu.Lock() if w.closed { @@ -49,12 +254,12 @@ func (w *JSONWriter) Write(e *Entry) error { return nil } -func (w *JSONWriter) formatJSON(buf *BytesBuffer, e *Entry) { +func (w *JSONWriter) formatJSONSecure(buf *BytesBuffer, e *Entry, trusted bool, redactionMark string) { _ = buf.WriteByte('{') w.writeJSONString(buf, "timestamp") _ = buf.WriteByte(':') - w.writeJSONTime(buf, e.Time, time.RFC3339Nano) + w.writeJSONTime(buf, e.Time) _ = buf.WriteByte(',') w.writeJSONString(buf, "level") @@ -64,7 +269,16 @@ func (w *JSONWriter) formatJSON(buf *BytesBuffer, e *Entry) { _ = buf.WriteByte(',') w.writeJSONString(buf, "message") _ = buf.WriteByte(':') - w.writeJSONString(buf, e.Message) + // Redact tags in message when untrusted; strip markers when trusted. + msg := e.Message + if e.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + w.writeJSONString(buf, msg) if e.Caller != "" { _ = buf.WriteByte(',') @@ -81,17 +295,17 @@ func (w *JSONWriter) formatJSON(buf *BytesBuffer, e *Entry) { _ = buf.WriteByte(',') w.writeJSONString(buf, f.Key) _ = buf.WriteByte(':') - w.writeJSONFieldValue(buf, f) + w.writeJSONFieldValueSecure(buf, f, trusted, redactionMark) } _ = buf.WriteByte('}') } -// writeJSONTime writes a quoted timestamp directly into buf using AppendFormat. -// RFC3339/RFC3339Nano output is ASCII-safe, so JSON escaping is not needed. -func (*JSONWriter) writeJSONTime(buf *BytesBuffer, t time.Time, layout string) { +// writeJSONTime writes a quoted RFC3339Nano timestamp directly into buf. +// RFC3339Nano output is ASCII-safe, so JSON escaping is not needed. +func (*JSONWriter) writeJSONTime(buf *BytesBuffer, t time.Time) { _ = buf.WriteByte('"') - buf.AppendTime(t, layout) + buf.AppendTime(t, time.RFC3339Nano) _ = buf.WriteByte('"') } @@ -140,7 +354,38 @@ func (*JSONWriter) writeJSONString(buf *BytesBuffer, s string) { _ = buf.WriteByte('"') } -func (w *JSONWriter) writeJSONFieldValue(buf *BytesBuffer, f Field) { +func (w *JSONWriter) writeJSONFieldValueSecure(buf *BytesBuffer, f Field, trusted bool, redactionMark string) { + switch f.Type { + case FieldTypeSecure, FieldTypeSecureURL: + if trusted && f.value != nil { + w.writeJSONString(buf, (*secureValue)(f.value).plain) + } else { + // Emit the field-level redacted form (not the writer-level mark) so that + // the URL form still shows e.g. "redis://user:[REDACTED]@host/db". + if f.value != nil { + w.writeJSONString(buf, (*secureValue)(f.value).redacted) + } else { + w.writeJSONString(buf, redactionMark) + } + } + return + case FieldTypeRedacted: + // Unconditionally redacted — trust has no effect. + w.writeJSONString(buf, redactionMark) + return + case FieldTypeTruncated: + if f.value != nil { + w.writeJSONString(buf, *(*string)(f.value)) + } else { + w.writeJSONString(buf, "") + } + return + default: + w.writeJSONFieldValueCore(buf, f) + } +} + +func (w *JSONWriter) writeJSONFieldValueCore(buf *BytesBuffer, f Field) { switch f.Type { case FieldTypeString: v := *(*string)(f.value) @@ -175,7 +420,7 @@ func (w *JSONWriter) writeJSONFieldValue(buf *BytesBuffer, f Field) { case FieldTypeTime: t := *(*time.Time)(f.value) - w.writeJSONTime(buf, t, time.RFC3339Nano) + w.writeJSONTime(buf, t) case FieldTypeDuration: d := time.Duration(f.num) @@ -244,12 +489,154 @@ func (w *JSONWriter) writeJSONFieldValue(buf *BytesBuffer, f Field) { v := *(*any)(f.value) w.writeJSONString(buf, fmt.Sprintf("%v", v)) + case FieldTypeSecure, FieldTypeSecureURL, FieldTypeRedacted, FieldTypeTruncated: + // Handled upstream by writeJSONFieldValueSecure before writeJSONFieldValueCore is called. + + case FieldTypeGroupItems, FieldTypeContinuationLines: + // Typed slice fields are emitted by their dedicated write methods (WriteGroup, + // WriteContinue). In the generic field path emit the element count so the JSON + // remains valid without leaking the raw Go slice pointer. + writeJSONSliceCount(buf, f) + case FieldTypeUnknown: // Null prevents JSON parsing errors when field type cannot be determined buf.WriteString("null") } } +// writeJSONSliceCount emits the element count for typed-slice fields (GroupItems, +// ContinuationLines) in the generic JSON field path. Dedicated write methods +// (WriteGroup, WriteContinue) emit the full structured representation; this is +// the fallback for additional writers that receive the field but don't specialise. +func writeJSONSliceCount(buf *BytesBuffer, f Field) { + if f.value == nil { + buf.WriteString("0") + return + } + var count int + switch f.Type { //nolint:exhaustive // only GroupItems and ContinuationLines are valid callers; default is unreachable + case FieldTypeGroupItems: + count = len(*(*[]GroupItem)(f.value)) + case FieldTypeContinuationLines: + count = len(*(*[]string)(f.value)) + default: + count = 0 + } + var tmp [20]byte + n := formatInt(tmp[:], int64(count)) + _, _ = buf.Write(tmp[:n]) +} + +// WriteContinue emits a JSON entry for a Logger.Continue call. +// The continuation lines are emitted as a "continuation" array. OSC 8 hyperlink +// escape sequences are stripped from each line — JSON consumers are aggregators +// that cannot render terminal control sequences and must not receive raw ESC bytes. +func (w *JSONWriter) WriteContinue(e *Entry, lines []string) error { + return w.WriteContinueSecure(e, lines, false, "[REDACTED]") +} + +// WriteContinueSecure is the trust-aware continuation JSON write path. +func (w *JSONWriter) WriteContinueSecure(e *Entry, lines []string, trusted bool, redactionMark string) error { + rawBuf := w.bufPool.Get(HintStructuredLog) + buf := NewBytesBuffer(rawBuf) + + w.formatJSONContinueSecure(buf, e, lines, trusted, redactionMark) + + w.mu.Lock() + if w.closed { + w.mu.Unlock() + w.bufPool.Put(rawBuf) + return ErrWriterClosed + } + _, err := w.out.Write(buf.Bytes()) + if err == nil { + _, err = w.out.Write(newlineByte) + } + w.mu.Unlock() + + w.bufPool.Put(rawBuf) + if err != nil { + return fmt.Errorf("json write failed: %w", err) + } + return nil +} + +func (w *JSONWriter) formatJSONContinueSecure(buf *BytesBuffer, e *Entry, lines []string, trusted bool, redactionMark string) { + _ = buf.WriteByte('{') + + w.writeJSONString(buf, "timestamp") + _ = buf.WriteByte(':') + w.writeJSONTime(buf, e.Time) + + _ = buf.WriteByte(',') + w.writeJSONString(buf, "level") + _ = buf.WriteByte(':') + w.writeJSONString(buf, e.Level.String()) + + _ = buf.WriteByte(',') + w.writeJSONString(buf, "message") + _ = buf.WriteByte(':') + msg := e.Message + if e.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + w.writeJSONString(buf, msg) + + if e.Caller != "" { + _ = buf.WriteByte(',') + w.writeJSONString(buf, "caller") + _ = buf.WriteByte(':') + w.writeJSONString(buf, e.Caller) + _ = buf.WriteByte(',') + w.writeJSONString(buf, "line") + _ = buf.WriteByte(':') + buf.WriteInt(int64(e.Line)) + } + + // Continuation lines as a JSON array. OSC 8 sequences are stripped because + // log aggregators cannot render terminal control sequences. + _ = buf.WriteByte(',') + w.writeJSONString(buf, continuationKey) + _ = buf.WriteByte(':') + _ = buf.WriteByte('[') + for i, line := range lines { + if i > 0 { + _ = buf.WriteByte(',') + } + w.writeJSONString(buf, stripOSC8(line)) + } + _ = buf.WriteByte(']') + + // Any additional Fields on the entry (base fields from With()). + for _, f := range e.Fields { + if f.Type == FieldTypeContinuationLines { + continue // already emitted above + } + _ = buf.WriteByte(',') + w.writeJSONString(buf, f.Key) + _ = buf.WriteByte(':') + w.writeJSONFieldValueSecure(buf, f, trusted, redactionMark) + } + + _ = buf.WriteByte('}') +} + +// Flush drains any buffered output without closing the writer. +// Only has effect when the underlying io.Writer implements Flush. +func (w *JSONWriter) Flush() error { + w.mu.Lock() + defer w.mu.Unlock() + + if f, ok := w.out.(interface{ Flush() error }); ok { + return f.Flush() + } + return nil +} + func (w *JSONWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() diff --git a/writer_json_test.go b/writer_json_test.go index afe991c..7b77242 100644 --- a/writer_json_test.go +++ b/writer_json_test.go @@ -59,13 +59,13 @@ func TestJSONWriter_NilStringerField(t *testing.T) { func TestJSONWriter_AddCaller(t *testing.T) { var buf bytes.Buffer - cfg := DefaultConfig() + cfg := defaultConfig() cfg.ConsoleOutput = nil cfg.StructuredOutput = &buf cfg.StructuredLevel = LevelDebug cfg.AddCaller = true - log := NewWithConfig(cfg) + log := newFromConfig(cfg) log.Info("caller test") output := buf.String() diff --git a/writer_multi.go b/writer_multi.go index 2a66001..e20f284 100644 --- a/writer_multi.go +++ b/writer_multi.go @@ -5,8 +5,19 @@ import ( "sync" ) +// workerState holds per-writer state cached at AddWriter time. +// isTrusted and redactionMark are stored here rather than re-derived on every +// write, keeping the per-entry cost to a simple bool read in the fan-out loop. +type workerState struct { + w Writer + sw SecureWriter // non-nil when w implements SecureWriter; cached to avoid type assertion per write + redactionMark string + isTrusted bool +} + type MultiWriter struct { - writers map[string]Writer + // workers carries both the writer and its cached trust flag, keyed by name. + workers map[string]workerState // Buffered channels prevent one slow writer from blocking others writeChans map[string]chan *Entry @@ -23,13 +34,18 @@ type MultiWriter struct { func NewMultiWriter() *MultiWriter { return &MultiWriter{ - writers: make(map[string]Writer), + workers: make(map[string]workerState), writeChans: make(map[string]chan *Entry), shutdownChan: make(chan struct{}), } } -func (mw *MultiWriter) AddWriter(name string, w Writer) { +// AddWriter registers a named writer with the given options. +// Replaces any existing writer with the same name. +// Thread-safe; no-op after Close. +func (mw *MultiWriter) AddWriter(name string, w Writer, opts ...WriterOption) { + o := applyWriterOptions(opts) + mw.mu.Lock() defer mw.mu.Unlock() @@ -42,20 +58,38 @@ func (mw *MultiWriter) AddWriter(name string, w Writer) { close(ch) } - mw.writers[name] = w + ws := workerState{ + w: w, + isTrusted: o.isTrusted, + redactionMark: o.effectiveRedactionMark(), + } + // Cache the SecureWriter assertion at registration time so the hot path + // pays only a bool comparison, not a type assertion per entry. + if sw, ok := w.(SecureWriter); ok { + ws.sw = sw + } + mw.workers[name] = ws // Buffer size trades latency vs blocking: smaller = less latency, larger = less blocking ch := make(chan *Entry, 256) mw.writeChans[name] = ch mw.wg.Add(1) - go mw.writerWorker(name, w, ch) + go mw.writerWorker(ws, ch) } -func (mw *MultiWriter) RemoveWriter(name string) { +// RemoveWriter removes the named writer and returns it so the caller can close it +// if needed. Returns nil if the name is not registered. +// Thread-safe; no-op after Close. +func (mw *MultiWriter) RemoveWriter(name string) Writer { mw.mu.Lock() defer mw.mu.Unlock() + state, exists := mw.workers[name] + if !exists { + return nil + } + if ch, ok := mw.writeChans[name]; ok { // If Close() has already set closed=true, it holds a snapshot of this // channel and will close it itself. Closing here too would panic. @@ -66,7 +100,26 @@ func (mw *MultiWriter) RemoveWriter(name string) { delete(mw.writeChans, name) } - delete(mw.writers, name) + delete(mw.workers, name) + return state.w +} + +// WriterByName returns the writer registered under name, or nil. +// Useful for inspecting capabilities without removing the writer. +func (mw *MultiWriter) WriterByName(name string) Writer { + mw.mu.Lock() + defer mw.mu.Unlock() + + return mw.workers[name].w +} + +// IsTrusted reports whether the writer registered under name was added with WriterTrusted(). +// Returns false for unknown names. +func (mw *MultiWriter) IsTrusted(name string) bool { + mw.mu.Lock() + defer mw.mu.Unlock() + + return mw.workers[name].isTrusted } func (mw *MultiWriter) Write(e *Entry) error { @@ -77,16 +130,13 @@ func (mw *MultiWriter) Write(e *Entry) error { return ErrWriterClosed } - // Non-blocking send prevents backpressure from slow writers - // CRITICAL: Call Retain() before send, Release() on failure - // This is more defensive and idiomatic + // Non-blocking send prevents backpressure from slow writers. + // Retain() before send; Release() on channel-full drop. for _, ch := range mw.writeChans { - e.Retain() // Prevent pool reclamation during async processing + e.Retain() select { case ch <- e: - // Successfully sent, Retain() will be balanced by Release() in worker default: - // Channel full - release and skip write to prevent blocking e.Release() } } @@ -94,11 +144,19 @@ func (mw *MultiWriter) Write(e *Entry) error { return nil } -func (mw *MultiWriter) writerWorker(_ string, w Writer, ch chan *Entry) { +func (mw *MultiWriter) writerWorker(ws workerState, ch chan *Entry) { defer mw.wg.Done() // Worker owns the writer lifecycle. Closing here ensures no concurrent // Write() calls happen after the worker exits, regardless of why it stopped. - defer func() { _ = w.Close() }() + defer func() { _ = ws.w.Close() }() + + write := func(e *Entry) { + if ws.sw != nil { + _ = ws.sw.WriteSecure(e, ws.isTrusted, ws.redactionMark) + } else { + _ = ws.w.Write(e) + } + } for { select { @@ -106,18 +164,15 @@ func (mw *MultiWriter) writerWorker(_ string, w Writer, ch chan *Entry) { if !ok { return } - - // Errors silently dropped to prevent panics - _ = w.Write(e) + write(e) // CRITICAL: Balance the Retain() from Write() - // Safe to call Release() here even if Write() failed e.Release() case <-mw.shutdownChan: // Drain all remaining entries. Close() guarantees ch will be closed // after shutdownChan, so range terminates once the channel is empty and closed. for e := range ch { - _ = w.Write(e) + write(e) e.Release() } return diff --git a/writer_ring.go b/writer_ring.go new file mode 100644 index 0000000..effa5fc --- /dev/null +++ b/writer_ring.go @@ -0,0 +1,416 @@ +package velocity + +import ( + "context" + "sync" + "sync/atomic" + "time" +) + +const minRingBufferWriterCapacity = 2 + +// EntrySnapshot is a value-typed deep copy of a log entry. +// Safe to read after the originating *Entry has been released to the pool. +type EntrySnapshot struct { + Time time.Time + Message string + Caller string + Fields []FieldSnapshot + Level Level +} + +// FieldSnapshot holds a pre-formatted field key/value pair. +// Value is the string form produced at write time, not at read time. +type FieldSnapshot struct { + Key string + Value string +} + +// RingStats reports the current state of a RingBufferWriter. +type RingStats struct { + Capacity int + Fill int + Drops int64 + Total int64 +} + +// ringOptions holds configuration applied via RingBufferOption. +type ringOptions struct { + redactionMark string +} + +// RingBufferOption configures a RingBufferWriter at construction time. +type RingBufferOption func(*ringOptions) + +// RingRedactionMark sets the placeholder string used when Phase 4 redacts +// a Secure field for this writer. Default: "[REDACTED]". +func RingRedactionMark(s string) RingBufferOption { + return func(o *ringOptions) { + o.redactionMark = s + } +} + +// subscriber pairs a channel with a once-closer so neither the ctx-cancel +// goroutine nor Close() can panic on a double-close. +type subscriber struct { + ch chan EntrySnapshot + once sync.Once +} + +func (s *subscriber) close() { + s.once.Do(func() { close(s.ch) }) +} + +// fieldSnapshotPool amortises the slice allocation for small field sets. +// Each snapshot borrows a slice, populates it, then keeps it — the pool is +// for the initial Get only; Put is called only when we discard a snapshot +// during ring overflow, not when the caller holds it via Snapshot(). +var fieldSnapshotPool = sync.Pool{ + New: func() any { + s := make([]FieldSnapshot, 0, 8) + return &s + }, +} + +// RingBufferWriter is a fixed-capacity in-process log sink. +// It stores the most recent N log entries as value-typed snapshots, making +// it safe to read after the original *Entry has been returned to its pool. +// +// Designed for the foundryos pattern: attach to a Logger, then serve +// snapshots over an HTTP debug endpoint or fan-out via Subscribe. +// +// Concurrency: a single mutex guards the ring head/tail and subscriber list. +// This is intentional — the ring writer sits off the critical path (attached +// via MultiWriter) and the per-write work (one mutex lock + one slice copy) +// is far cheaper than the CAS machinery in ringbuffer.go, which is optimised +// for byte-stream throughput, not snapshot semantics. +type RingBufferWriter struct { + redactionMark string + + // ring is the fixed-size circular snapshot store. + ring []EntrySnapshot + + // subscribers receive a copy of each new snapshot. + // Slow consumers get dropped entries, not blocked writers. + subscribers []*subscriber + + head int // next write position + fill int // number of valid entries currently held + capacity int + + drops atomic.Int64 + total atomic.Int64 + + mu sync.Mutex + + // closed prevents writes after Close(). + closed bool + + // isTrusted mirrors the WriterTrusted() opt-in so IsTrusted() works + // without the caller needing to inspect writerOptions separately. + // Phase 4 reads this to decide whether to redact Secure fields. + isTrusted bool +} + +// NewRingBufferWriter creates a fixed-capacity snapshot ring. +// Capacity is clamped to minRingBufferWriterCapacity (2) if smaller. +func NewRingBufferWriter(capacity int, opts ...RingBufferOption) *RingBufferWriter { + if capacity < minRingBufferWriterCapacity { + capacity = minRingBufferWriterCapacity + } + + o := ringOptions{ + redactionMark: "[REDACTED]", + } + for _, opt := range opts { + if opt != nil { + opt(&o) + } + } + + return &RingBufferWriter{ + ring: make([]EntrySnapshot, capacity), + capacity: capacity, + redactionMark: o.redactionMark, + } +} + +// IsTrusted implements TrustedWriter. Returns false by default; true when the +// writer is added via AddWriter with WriterTrusted(). The trust flag is stored +// on writerOptions in MultiWriter — this method exists so callers can query +// the writer directly without going through the logger. +func (r *RingBufferWriter) IsTrusted() bool { + return r.isTrusted +} + +// SetTrusted is called by MultiWriter's AddWriter when WriterTrusted() is in +// the option set. Not part of the public API — internal plumbing for Phase 4. +func (r *RingBufferWriter) SetTrusted(v bool) { + r.isTrusted = v +} + +// Write converts the live entry to a value snapshot and appends it to the ring. +// When the ring is full the oldest entry is overwritten (drop-oldest semantics). +// Entries written after Close are silently discarded. +// Secure fields are redacted unless the writer was registered with WriterTrusted(). +func (r *RingBufferWriter) Write(e *Entry) error { + return r.WriteSecure(e, r.isTrusted, r.redactionMark) +} + +// WriteSecure implements SecureWriter. trusted controls whether Secure field +// plaintext is stored in the snapshot. Call via MultiWriter which passes the +// per-worker trust state; direct Write() uses the writer's own isTrusted flag. +func (r *RingBufferWriter) WriteSecure(e *Entry, trusted bool, redactionMark string) error { + if e == nil { + return nil + } + + snap := toSnapshotSecure(e, trusted, redactionMark) + + r.mu.Lock() + + if r.closed { + r.mu.Unlock() + putFieldSnapshot(snap.Fields) + return nil + } + + // Overwrite the oldest slot when full; the displaced snapshot's field + // slice is returned to the pool to keep allocation churn low. + if r.fill == r.capacity { + putFieldSnapshot(r.ring[r.head].Fields) + r.drops.Add(1) + } else { + r.fill++ + } + + r.ring[r.head] = snap + r.head = (r.head + 1) % r.capacity + + // Fan-out to subscribers before releasing the lock so they see a + // consistent snapshot. Non-blocking send: slow consumers drop, not block. + for _, sub := range r.subscribers { + select { + case sub.ch <- snap: + default: + r.drops.Add(1) + } + } + + r.mu.Unlock() + + r.total.Add(1) + return nil +} + +// Snapshot returns the most recent n entries in chronological order (oldest first). +// n is clamped to the current fill count. Each call allocates a new slice — +// this is a diagnostic endpoint, not a hot path. +func (r *RingBufferWriter) Snapshot(n int) []EntrySnapshot { + r.mu.Lock() + defer r.mu.Unlock() + + if n <= 0 || r.fill == 0 { + return nil + } + + if n > r.fill { + n = r.fill + } + + out := make([]EntrySnapshot, n) + + // tail is the index of the oldest valid entry. + // head points to the next write slot, so the oldest is: + // (head - fill + capacity) % capacity + tail := (r.head - r.fill + r.capacity) % r.capacity + + // Start from (fill - n) entries ahead of tail to get the most recent n. + start := (tail + r.fill - n) % r.capacity + + for i := range n { + idx := (start + i) % r.capacity + src := r.ring[idx] + // Deep-copy the field slice so the caller owns its memory. + var fields []FieldSnapshot + if len(src.Fields) > 0 { + fields = make([]FieldSnapshot, len(src.Fields)) + copy(fields, src.Fields) + } + out[i] = EntrySnapshot{ + Time: src.Time, + Level: src.Level, + Message: src.Message, + Fields: fields, + Caller: src.Caller, + } + } + + return out +} + +// Subscribe returns a channel that receives a copy of every new snapshot. +// The channel is buffered to bufSize. Slow consumers get dropped entries +// (the Drops counter is incremented). The channel closes when ctx is cancelled. +// bufSize is clamped to 1 if zero or negative. +func (r *RingBufferWriter) Subscribe(ctx context.Context, bufSize int) <-chan EntrySnapshot { + if bufSize < 1 { + bufSize = 1 + } + + sub := &subscriber{ch: make(chan EntrySnapshot, bufSize)} + + r.mu.Lock() + if r.closed { + r.mu.Unlock() + sub.close() + return sub.ch + } + r.subscribers = append(r.subscribers, sub) + r.mu.Unlock() + + // Unregister and close when the context is cancelled. + // sub.close() is idempotent via sync.Once, so it is safe even if + // Close() fired concurrently and already closed the channel. + go func() { + <-ctx.Done() + r.mu.Lock() + r.removeSubscriber(sub) + r.mu.Unlock() + sub.close() + }() + + return sub.ch +} + +// Stats returns a point-in-time view of ring state. +func (r *RingBufferWriter) Stats() RingStats { + r.mu.Lock() + fill := r.fill + r.mu.Unlock() + + return RingStats{ + Capacity: r.capacity, + Fill: fill, + Drops: r.drops.Load(), + Total: r.total.Load(), + } +} + +// Close prevents further writes and closes all subscriber channels. +// Safe to call more than once. +func (r *RingBufferWriter) Close() error { + r.mu.Lock() + defer r.mu.Unlock() + + if r.closed { + return nil + } + + r.closed = true + + // Close all subscriber channels. sub.close() is guarded by sync.Once, + // so it is safe even if the ctx-cancel goroutine fires concurrently. + for _, sub := range r.subscribers { + sub.close() + } + r.subscribers = nil + + return nil +} + +// toSnapshotSecure deep-copies the entry, applying trust-aware field serialisation. +// When trusted is false, Secure/SecureURL fields emit their redacted form and +// Redacted fields emit redactionMark. +func toSnapshotSecure(e *Entry, trusted bool, redactionMark string) EntrySnapshot { + var fields []FieldSnapshot + + if len(e.Fields) > 0 { + ptr, ok := fieldSnapshotPool.Get().(*[]FieldSnapshot) + if !ok || ptr == nil { + s := make([]FieldSnapshot, 0, len(e.Fields)) + ptr = &s + } + + fs := (*ptr)[:0] + if cap(fs) < len(e.Fields) { + fs = make([]FieldSnapshot, 0, len(e.Fields)) + } + + for _, f := range e.Fields { + val := fieldSnapshotValue(f, trusted, redactionMark) + fs = append(fs, FieldSnapshot{Key: f.Key, Value: val}) + } + fields = fs + } + + msg := e.Message + if e.maybeSecure { + if trusted { + msg = stripSecureTags(msg) + } else { + msg = redactSecureTags(msg, redactionMark) + } + } + + return EntrySnapshot{ + Time: e.Time, + Level: e.Level, + Message: msg, + Fields: fields, + Caller: e.Caller, + } +} + +// fieldSnapshotValue converts a field to its snapshot string form, +// respecting trust state for Secure/SecureURL/Redacted types. +func fieldSnapshotValue(f Field, trusted bool, redactionMark string) string { + switch f.Type { + case FieldTypeSecure, FieldTypeSecureURL: + if trusted && f.value != nil { + return (*secureValue)(f.value).plain + } + if f.value != nil { + return (*secureValue)(f.value).redacted + } + return redactionMark + case FieldTypeRedacted: + return redactionMark + case FieldTypeTruncated: + if f.value != nil { + return *(*string)(f.value) + } + return "" + default: + return FieldValueToString(f) + } +} + +// removeSubscriber removes sub from the subscriber list. +// Must be called with r.mu held. +func (r *RingBufferWriter) removeSubscriber(sub *subscriber) { + for i, s := range r.subscribers { + if s == sub { + // Swap with last to avoid shifting the slice. + last := len(r.subscribers) - 1 + r.subscribers[i] = r.subscribers[last] + r.subscribers[last] = nil + r.subscribers = r.subscribers[:last] + return + } + } +} + +// putFieldSnapshot returns a field slice to the pool when it is no longer +// referenced (e.g. when a ring slot is overwritten). Only called for slices +// that the ring itself owns, never for slices returned by Snapshot(). +func putFieldSnapshot(fs []FieldSnapshot) { + if fs == nil { + return + } + if cap(fs) > 64 { + return + } + fs = fs[:0] + fieldSnapshotPool.Put(&fs) +} diff --git a/writer_ring_test.go b/writer_ring_test.go new file mode 100644 index 0000000..bbe4392 --- /dev/null +++ b/writer_ring_test.go @@ -0,0 +1,471 @@ +package velocity + +import ( + "context" + "sync" + "testing" + "time" +) + +// makeEntry produces a minimal *Entry suitable for ring writer tests. +// The entry is not pool-managed — tests own it directly. +func makeEntry(level Level, msg string, fields ...Field) *Entry { + e := &Entry{ + Time: time.Now(), + Level: level, + Message: msg, + Fields: fields, + } + e.written.Store(1) // mark written so Release is safe if called + e.refCount.Store(1) + return e +} + +// --- Construction --- + +func TestRingBufferWriter_CapacityEnforced(t *testing.T) { + t.Parallel() + + cases := []struct { + in int + want int + }{ + {0, minRingBufferWriterCapacity}, + {-5, minRingBufferWriterCapacity}, + {1, minRingBufferWriterCapacity}, + {2, 2}, + {100, 100}, + } + + for _, tc := range cases { + r := NewRingBufferWriter(tc.in) + if r.capacity != tc.want { + t.Errorf("capacity(%d): got %d, want %d", tc.in, r.capacity, tc.want) + } + } +} + +func TestRingBufferWriter_RedactionMark(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4, RingRedactionMark("***")) + if r.redactionMark != "***" { + t.Errorf("got %q, want %q", r.redactionMark, "***") + } + + // Default + r2 := NewRingBufferWriter(4) + if r2.redactionMark != redactedMark { + t.Errorf("got %q, want %q", r2.redactionMark, redactedMark) + } +} + +// --- Write and wrapping --- + +func TestRingBufferWriter_WritePopulatesRing(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + + e := makeEntry(LevelWarn, "hello", String("k", "v")) + if err := r.Write(e); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + stats := r.Stats() + if stats.Fill != 1 { + t.Errorf("fill: got %d, want 1", stats.Fill) + } + if stats.Total != 1 { + t.Errorf("total: got %d, want 1", stats.Total) + } + // Confirm the level round-trips correctly through the snapshot. + snaps := r.Snapshot(1) + if len(snaps) == 0 || snaps[0].Level != LevelWarn { + t.Errorf("snapshot level: got %v, want %v", snaps[0].Level, LevelWarn) + } +} + +func TestRingBufferWriter_NilWriteIsNoop(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + if err := r.Write(nil); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r.Stats().Total != 0 { + t.Error("nil write should not increment total") + } +} + +func TestRingBufferWriter_WrapsAtCapacity(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(3) + + for i := range 5 { + e := makeEntry(LevelInfo, "msg") + e.Fields = []Field{Int("i", i)} + _ = r.Write(e) + } + + stats := r.Stats() + if stats.Fill != 3 { + t.Errorf("fill: got %d, want 3", stats.Fill) + } + // Two entries were displaced. + if stats.Drops != 2 { + t.Errorf("drops: got %d, want 2", stats.Drops) + } + if stats.Total != 5 { + t.Errorf("total: got %d, want 5", stats.Total) + } + + // Most recent 3 entries should have i=2,3,4 + snaps := r.Snapshot(3) + if len(snaps) != 3 { + t.Fatalf("snapshot len: got %d, want 3", len(snaps)) + } + for idx, want := range []string{"2", "3", "4"} { + if len(snaps[idx].Fields) == 0 || snaps[idx].Fields[0].Value != want { + t.Errorf("snap[%d].Fields[0].Value: got %q, want %q", idx, snaps[idx].Fields[0].Value, want) + } + } +} + +// --- Snapshot --- + +func TestRingBufferWriter_SnapshotReturnsRecentN(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(10) + + for i := range 7 { + _ = r.Write(makeEntry(LevelInfo, "msg", Int("i", i))) + } + + snaps := r.Snapshot(3) + if len(snaps) != 3 { + t.Fatalf("len: got %d, want 3", len(snaps)) + } + // Should be entries i=4, i=5, i=6 + for idx, want := range []string{"4", "5", "6"} { + got := snaps[idx].Fields[0].Value + if got != want { + t.Errorf("snap[%d]: got %q, want %q", idx, got, want) + } + } +} + +func TestRingBufferWriter_SnapshotClampsToFill(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(10) + _ = r.Write(makeEntry(LevelInfo, "only")) + + snaps := r.Snapshot(100) + if len(snaps) != 1 { + t.Errorf("len: got %d, want 1", len(snaps)) + } +} + +func TestRingBufferWriter_SnapshotZeroOrEmptyReturnsNil(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + + if s := r.Snapshot(0); s != nil { + t.Errorf("expected nil for n=0, got %v", s) + } + if s := r.Snapshot(5); s != nil { + t.Errorf("expected nil for empty ring, got %v", s) + } +} + +func TestRingBufferWriter_SnapshotDeepCopy(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + _ = r.Write(makeEntry(LevelInfo, "original", String("key", "value"))) + + snaps := r.Snapshot(1) + + // Write new entries to potentially overwrite the old ring slot. + for range 5 { + _ = r.Write(makeEntry(LevelInfo, "new")) + } + + // The snapshot should still reflect the original. + if snaps[0].Message != "original" { + t.Errorf("snapshot message mutated: %q", snaps[0].Message) + } + if len(snaps[0].Fields) == 0 || snaps[0].Fields[0].Value != "value" { + t.Errorf("snapshot fields mutated: %v", snaps[0].Fields) + } +} + +// --- Subscribe --- + +func TestRingBufferWriter_SubscribeReceivesEntries(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(10) + ctx := t.Context() + + ch := r.Subscribe(ctx, 8) + + messages := []string{"alpha", "beta", "gamma"} + for _, msg := range messages { + _ = r.Write(makeEntry(LevelInfo, msg)) + } + + received := make([]string, 0, len(messages)) + timeout := time.After(time.Second) + for range messages { + select { + case snap := <-ch: + received = append(received, snap.Message) + case <-timeout: + t.Fatalf("timed out waiting for subscriber entry; received %v", received) + } + } + + for i, want := range messages { + if received[i] != want { + t.Errorf("received[%d]: got %q, want %q", i, received[i], want) + } + } +} + +func TestRingBufferWriter_SubscribeClosesOnCtxCancel(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(10) + ctx, cancel := context.WithCancel(context.Background()) + + ch := r.Subscribe(ctx, 4) + cancel() + + // Channel must close; don't block forever. + select { + case _, ok := <-ch: + if ok { + // drain any buffered entries and wait for close + for range ch { + } + } + case <-time.After(time.Second): + t.Fatal("channel did not close after ctx cancel") + } +} + +func TestRingBufferWriter_SubscribeDropsOnSlowConsumer(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(20) + ctx := t.Context() + + // bufSize=1 means the channel fills after one unread entry. + ch := r.Subscribe(ctx, 1) + + // Write enough entries to overflow the channel buffer while the consumer + // is not reading — most will be dropped. + for range 10 { + _ = r.Write(makeEntry(LevelInfo, "flood")) + } + + // At least some drops must have occurred. + if r.drops.Load() == 0 { + t.Error("expected at least one drop for slow consumer") + } + + _ = ch // suppress unused warning; consumer intentionally not reading +} + +// --- Stats --- + +func TestRingBufferWriter_Stats(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(5) + + for range 3 { + _ = r.Write(makeEntry(LevelInfo, "x")) + } + + s := r.Stats() + + if s.Capacity != 5 { + t.Errorf("Capacity: got %d, want 5", s.Capacity) + } + if s.Fill != 3 { + t.Errorf("Fill: got %d, want 3", s.Fill) + } + if s.Total != 3 { + t.Errorf("Total: got %d, want 3", s.Total) + } + if s.Drops != 0 { + t.Errorf("Drops: got %d, want 0", s.Drops) + } +} + +// --- IsTrusted --- + +func TestRingBufferWriter_IsTrustedDefaultFalse(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + if r.IsTrusted() { + t.Error("default IsTrusted should be false") + } +} + +func TestRingBufferWriter_SetTrustedFlipsFlag(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + r.SetTrusted(true) + if !r.IsTrusted() { + t.Error("IsTrusted should be true after SetTrusted(true)") + } +} + +// Verify that WriterTrusted() integration works end-to-end via the logger. +// The trust flag must survive the AddWriter path so both the MultiWriter worker +// and the writer's own IsTrusted() reflect it. +func TestRingBufferWriter_TrustedViaLoggerAddWriter(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + + log := New(WithNop()) + log.AddWriter("ring", r, WriterTrusted()) + + // AddWriter propagates trust to the writer via SetTrusted when available, + // so IsTrusted() on the ring writer itself should return true. + if !r.IsTrusted() { + t.Error("RingBufferWriter.IsTrusted() should be true after AddWriter with WriterTrusted()") + } + + // MultiWriter worker state should also reflect the trust flag. + log.writers.mu.RLock() + multiTrusted := log.writers.mw.IsTrusted("ring") + log.writers.mu.RUnlock() + if !multiTrusted { + t.Error("MultiWriter worker should report trusted for WriterTrusted() registration") + } + + _ = log.Close() +} + +// --- Close --- + +func TestRingBufferWriter_CloseIsIdempotent(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + if err := r.Close(); err != nil { + t.Fatalf("first close: %v", err) + } + if err := r.Close(); err != nil { + t.Fatalf("second close: %v", err) + } +} + +func TestRingBufferWriter_PostCloseWriteDrops(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + _ = r.Write(makeEntry(LevelInfo, "before")) + + _ = r.Close() + + if err := r.Write(makeEntry(LevelInfo, "after")); err != nil { + t.Fatalf("write after close should not error: %v", err) + } + + // Total should not have increased after close. + if r.Stats().Total != 1 { + t.Errorf("total after post-close write: got %d, want 1", r.Stats().Total) + } +} + +func TestRingBufferWriter_CloseClosesSubscribers(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(4) + ctx := t.Context() + + ch := r.Subscribe(ctx, 4) + _ = r.Close() + + // Close() shuts down subscriber channels; the channel should be closed. + select { + case _, ok := <-ch: + if ok { + // Drain remaining. + for range ch { + } + } + case <-time.After(time.Second): + t.Fatal("subscriber channel not closed after writer Close()") + } +} + +// --- Concurrency --- + +func TestRingBufferWriter_ConcurrentWriteSnapshotSubscribe(t *testing.T) { + t.Parallel() + + r := NewRingBufferWriter(64) + ctx, cancel := context.WithCancel(context.Background()) + + ch := r.Subscribe(ctx, 32) + + // Separate WaitGroups so we can cancel ctx after writers finish, + // then wait for the subscriber drainer which exits once ch closes. + var producersWg sync.WaitGroup + var consumerWg sync.WaitGroup + + const numWriters = 8 + const perWriter = 200 + + // Concurrent writers. + for w := range numWriters { + producersWg.Add(1) + go func(id int) { + defer producersWg.Done() + for i := range perWriter { + _ = r.Write(makeEntry(LevelInfo, "concurrent", Int("w", id), Int("i", i))) + } + }(w) + } + + // Concurrent snapshotter — counted with producers since it finishes before cancel. + producersWg.Add(1) + go func() { + defer producersWg.Done() + for range numWriters * perWriter / 10 { + _ = r.Snapshot(10) + } + }() + + // Subscriber drainer: exits when ch closes (after cancel). + consumerWg.Add(1) + go func() { + defer consumerWg.Done() + for range ch { //nolint:revive // intentionally discarding subscriber entries + } + }() + + // Cancel after all writes and snapshots are done so the subscriber + // goroutine inside Subscribe exits and closes ch. + producersWg.Wait() + cancel() + consumerWg.Wait() + + s := r.Stats() + if s.Total != numWriters*perWriter { + t.Errorf("total: got %d, want %d", s.Total, numWriters*perWriter) + } +}