Files
ottyl/CLAUDE.md
T

6.0 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

ottyl turns a piped command's stdout (or a tailed file) into a local observability dashboard: searchable log timeline, ad-hoc metrics extracted from log lines via regex, and Grafana-style dashboards — all served from a single self-contained binary for the lifetime of that process. It can also poll a Prometheus/OpenMetrics endpoint alongside log ingestion. See README.md for the user-facing feature list and CLI usage.

Commands

make build      # builds webapp (npm install + vite build) then the release Rust binary; webapp/dist is embedded into the binary
make dev        # frontend dev server with hot reload, proxying API calls to a separately running `cargo run` backend (see webapp/vite.config.ts)
make test       # cargo test + frontend build (frontend has no test suite, just `tsc -b && vite build`)
make lint       # cargo clippy --all-targets + oxlint (webapp)
make run        # build then run ./target/release/ottyl
  • Single backend test: cargo test <test_name>
  • Backend tests live inline in each src/*.rs file (#[cfg(test)] mod tests), not in a separate tests/ dir.
  • The webapp has no unit tests — npm run build (tsc + vite) is the only frontend check besides lint.
  • webapp/dist must exist before cargo build will produce a working binary (it's embedded via fs_embed!), so always build the frontend first — make build handles ordering; don't run cargo build alone after frontend changes without rebuilding webapp/dist.

Architecture

Backend: Rust (axum + tokio), single binary, in src/. Frontend: React 19 + TypeScript + Tailwind + Recharts, in webapp/, built with Vite and embedded into the binary at build time (src/api.rs's WEBAPP_DIST via fs_embed!("webapp/dist")).

Data flow: main.rs wires everything together at startup — loads Settings, spawns the engine actor task, replays the active profile's param/metric declarations into it (before ingest starts, since there's no way to reprocess earlier lines later), then starts ingest (stdin/file tailing and/or Prometheus polling) and the axum HTTP server.

  • engine.rs (largest file, ~3400 lines) — the actor owning all live state: ingested log lines, declared params/metrics, computed metric buckets, alert evaluation. Runs as a single tokio task; all access is through EngineHandle sending EngineMessage over an mpsc channel and awaiting a response via oneshot — there is no shared mutable engine state accessed directly from request handlers. This is the file to read first to understand how declarations, filtering, and bucketing actually work.
  • api.rs — axum routes and HTTP-facing ApiState (holds EngineHandle + SettingsHandle). Translates HTTP requests into EngineMessages and DTOs back into JSON. Also serves the embedded webapp.
  • settings.rs / profile.rs — persisted configuration. A profile is per-app: parsing rules (param/metric declarations) and dashboards, saved so they're there again next time that app runs through ottyl (see README's "Profiles"). Declarations are persisted here so the engine can replay them on restart; api.rs handlers that mutate engine state (e.g. declare_metric) call a corresponding persist method here so the two stay in sync — this dual-write is a recurring pattern (see ApiState::persist_param_declaration / persist_metric_declaration in api.rs), and adding a new declaration-mutating endpoint means updating both sides plus the load-time replay path.
  • filter_parser.rs (grammar in filter_parser.pest, via pest) + filter_datetime.rs — parse the search/filter bar syntax (regex matches, metric comparisons like latency_ms[value]<200, relative/absolute datetimes).
  • promql.rs — parses and evaluates the PromQL-style queries backing dashboard panels.
  • logfmt.rs / ingest.rs — line ingestion: tailing stdin/a file, structured logfmt-style extraction, and polling a Prometheus/OpenMetrics --metrics-url.
  • alerts.rs — alert rule evaluation and ntfy delivery.
  • dto.rs — shared request/response types between api.rs and the frontend's webapp/src/lib/api.ts; data_value.rs / metric_primitive.rs — the value/type model for params and metrics; index_key.rs / rate.rs — bucket indexing and rate computation helpers.

Frontend structure: webapp/src/App.tsx is the shell; webapp/src/components/ has one component per feature area (FilterBar, LogTimeline, Metrics, Params, Dashboards, Alerts, Docs, NavMenu, ...) that map closely to the backend concepts above. webapp/src/lib/api.ts is the typed client for the Rust API; webapp/src/lib/polling.ts handles refresh polling; webapp/src/lib/themes.ts covers the style-preset/CSS system.

Conventions

  • tickets/ holds scoped, written-up work items (problem statement, pointers to exact call sites with file.rs:line references, and an explicit scope section) — check here for planned/in-flight work before starting something that might overlap. Tickets are removed from this directory once implemented (see recent commit history — e.g. "Remove ticket 047 (implemented: ...)").
  • Declaration-related features (params, metrics) consistently need three coordinated changes: an EngineMessage variant + handler in engine.rs, a persistence method in settings.rs/profile.rs, and an api.rs route wiring them together (plus a webapp UI affordance). Grep for an existing similar declaration (e.g. how metric declaration is wired) as the template before adding a new one.
  • Doc comments in this codebase tend to explain why a non-obvious choice was made (e.g. ordering constraints, a workaround, a past ticket's rationale) rather than restating what the code does — match that style rather than adding descriptive comments.
  • All markdown files (*.md) in this repo should follow one-sentence-per-line: each sentence starts on its own line, even within the same paragraph.