6.3 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/*.rsfile (#[cfg(test)] mod tests), not in a separatetests/dir. - The webapp has no unit tests —
npm run build(tsc + vite) is the only frontend check besides lint. webapp/distmust exist beforecargo buildwill produce a working binary (it's embedded viafs_embed!), so always build the frontend first —make buildhandles ordering; don't runcargo buildalone after frontend changes without rebuildingwebapp/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/— owns all live state, split across two actor tasks underengine/actor/:actor/primary.rs(ingested log lines, param declarations) andactor/profile.rs(metric declarations, computed metric buckets, alert evaluation — moved off the primary perideas/0001-actor-split.md). Both are plaintokio::spawned loops;actor/mod.rsholds theleft_right-basedIndexReader/IndexReadHandleread plumbing they share. All access is throughEngineHandle(defined inactor/primary.rs, re-exported fromengine/mod.rs) sendingEngineMessageover an mpsc channel and awaiting a response via oneshot — there is no shared mutable engine state accessed directly from request handlers.engine/snapshot/is the state (EngineSnapshot) both actors mutate; this is the place to start reading to understand how declarations, filtering, and bucketing actually work.api.rs— axum routes and HTTP-facingApiState(holdsEngineHandle+SettingsHandle). Translates HTTP requests intoEngineMessages 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.rshandlers 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 (seeApiState::persist_param_declaration/persist_metric_declarationinapi.rs), and adding a new declaration-mutating endpoint means updating both sides plus the load-time replay path.filter_parser.rs(grammar infilter_parser.pest, viapest) +filter_datetime.rs— parse the search/filter bar syntax (regex matches, metric comparisons likelatency_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 betweenapi.rsand the frontend'swebapp/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 withfile.rs:linereferences, 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
EngineMessagevariant + handler inengine.rs, a persistence method insettings.rs/profile.rs, and anapi.rsroute 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.