Ticket 108 anticipated a large test-harness migration (primary-only/ profile-only helpers, migrating ~150 tests in engine/tests.rs), but that turned out unnecessary: the profile actor reuses EngineSnapshot as its own snapshot type (see ticket 104), so every existing EngineSnapshot unit test already exercises primary-shaped and profile-shaped behavior unchanged -- zero migration needed, confirmed by the full suite passing untouched through tickets 100-107. What was still a real gap: tests asserting on profile-actor-owned data (declared metrics, alerts) after ingesting had no deterministic way to wait for the profile actor to catch up -- EngineHandle::barrier() only confirms the primary's queue drained, not that the profile actor has processed what it published. They were passing on scheduler luck on the single-threaded test runtime, not a real guarantee. Added ProfileHandle::processed_sequence, a permanent (not test-only) watch channel tracking the profile actor's watermark, mirrored onto EngineHandle as profile_processed_sequence, with a wait_for_profile_sequence(sequence) test helper built on it. This replaces the ad hoc on_advance/spawn_observed mechanism ticket 103 added just to prove the wake loop worked -- that test now uses the same real primitive. Retrofitted it into promql_query_returns_series_for_seeded_fixture to eliminate its latent race. Not done: a dedicated test for the accepted eviction/lag tradeoff (primary evicts on schedule regardless of profile-actor progress, per idea 0001's resolved decision) -- deterministically forcing that race without new test-injection hooks proved more involved than the remaining scope justified; left as a known gap rather than shipping a flaky or synthetic test for it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
ottyl
Observability for any command's terminal output — no logging backend, no agents, no config to ship.
your_app --verbose | ottyl
Pipe a program's stdout into ottyl and turn it into a searchable log timeline, ad-hoc metrics extracted from the log lines themselves, and Grafana-style dashboards — served from a single local binary, for the lifetime of that process:
- Search: A syntax-aware filter bar over your logs — regex matches and metric comparisons like
latency_ms[value]<200, with relative (5h,-3m) and absolute datetimes. - Log timeline: A bucketed bar chart of message volume over time, Kibana-style. Drag across it to set the active time range.
- Metrics from logs: Declare a metric by matching a regex against your log lines and extracting a value. It applies retroactively to everything already ingested, and to everything that follows.
- Dashboards: Bar, line, and pie chart panels driven by PromQL-style queries over your declared metrics.
- Mixed sources: Poll an existing Prometheus/OpenMetrics endpoint alongside log ingestion, in the same dashboard.
- Profiles: Parsing rules and dashboards are saved per app, so they're there again next time that app runs through ottyl.
Use cases
- Debugging a noisy CLI or long-running script, instead of scrolling a terminal buffer.
- Getting metrics and charts out of a program that only ever prints text.
- Local dashboards without standing up a metrics stack.
- Combining scraped Prometheus metrics with log-derived ones during a debugging session.
Get started
Requires Rust and Node.js. The webapp is built and embedded into the binary, so the result is a single self-contained executable.
git clone <this-repo>
cd ottyl
make build
your_app --verbose | ottyl
Or point it at a log file instead of stdin, and mix in a Prometheus/OpenMetrics endpoint:
ottyl --file /var/log/your_app.log --metrics-url http://localhost:9090/metrics
ottyl prints the URL of the local web server it starts (an OS-assigned free port by default; pin one with --port). See ottyl --help for the rest, including where settings and profiles live (--settings-path).
Full documentation — filtering syntax, parameters, metrics, dashboards — is built into the app itself, under the nav menu's docs section.
Development
make dev # frontend dev server with hot reload, proxying to a separately running `cargo run` backend
make test # backend + frontend tests
make lint # clippy + oxlint