# AGENTS.md Guidance for AI coding agents working in this repository. ## What this is otel-viewer is a small OTLP/gRPC collector that stores traces, metrics and logs in DuckDB and serves a web UI plus a REST query API. It ships in two forms: - **CLI/server crate** (`src/`): `otel-viewer` — OTLP/gRPC receiver + HTTP (REST API + embedded web UI). Run with `cargo run`, point OTLP exporters at it. - **Desktop shell** (`src-tauri/`): `otel-viewer-tauri` — wraps the library crate in a Tauri 2 window. The webview is served through a custom `otelview://` URI scheme that proxies to the in-process REST router (no loopback HTTP from the webview — see "White window" below). The same router also binds 127.0.0.1:6666 for external access (browser, curl). - **Web UI** (`web/`): React + Vite + Tailwind + recharts SPA. Built into `web/dist`, embedded into the binary via rust-embed (`src/api.rs`). ## Layout | Path | Purpose | |---|---| | `src/` | collector library crate: gRPC ingestion (`grpc.rs`), DuckDB layer (`db.rs`), REST API + static UI (`api.rs`), query builders (`queries.rs`), read-only SQL guard (`sqlguard.rs`), demo data (`demo.rs`), dashboards (`dashboards.rs` + `dashboards/*.yml`) | | `src-tauri/` | Tauri 2 desktop shell (`src/lib.rs`), bundler config (`tauri.conf.json`) | | `web/src/` | React UI: panels (`components/panels/`), API client (`lib/api.ts`), chart-shape detection (`lib/chart.ts`), dashboard types/SQL rendering (`lib/dash.ts`) | | `locust-test/` | aiolocust load-test harness (uv venv) used to generate realistic telemetry | ## Commands ```bash cargo check # workspace check (root crate) cargo test # unit + integration tests pnpm -C web build # tsc + vite build of the web UI (writes web/dist) pnpm -C web dev # vite dev server at :5173, proxies /api -> :6666 cargo run -- --seed-demo # run collector + API + UI at http://127.0.0.1:6666 pnpm tauri dev # desktop shell (debug), embedded web/dist build pnpm tauri build # desktop shell (release) -> app/dmg (mac), nsis (win) pnpm tauri icon src-tauri/app-icon.png # regenerate bundle icons ``` Run `cargo check`, `cargo test` and `pnpm -C web build` before declaring work done. There is no rustfmt/clippy config beyond the defaults. ## Important facts - **`web/dist` must exist before release builds.** rust-embed bakes it in at compile time (`#[folder = "web/dist"]` in `src/api.rs`). In debug builds rust-embed reads files live from disk; in release it uses the embedded copies. `tauri.conf.json` runs `pnpm -C web build` as `beforeBuildCommand`/`beforeDevCommand` — those commands execute **from the repo root**, not from `web/` or `src-tauri/`, hence the `pnpm -C web` form. - **Ports**: HTTP 6666 (fallbacks 6667–6670, then OS-assigned), OTLP/gRPC 4317 (fallbacks 4318–4321). Actual addresses are in `/api/health`. The desktop app binds 127.0.0.1 only. - **CORS is already permissive** (`CorsLayer::very_permissive()` in `src/api.rs`), and the UI is normally same-origin with the API, so no CORS work is needed for any access pattern (vite dev proxies `/api`, browser/CLI hits :6666 directly, the Tauri webview uses the scheme). - **sqlguard** (`src/sqlguard.rs`): user SQL in the SQL panel must stay a single read-only statement; keywords are blocklisted and `--` comments are stripped before scanning. Queries without a LIMIT get one appended. - **Maintenance endpoints**: `POST /api/reset` deletes all telemetry rows (serialized through the db writer task); `GET /api/dbstats` returns DuckDB storage stats (file size, blocks, per-table sizes, memory) — used by the header (i) button. - **No native JS dialogs in the Tauri webview**: `window.confirm/alert` are silent no-ops — use inline two-step confirmation / error states instead (see the reset button in `web/src/components/AppHeader.tsx`). - **aiolocust telemetry quirks** (matters for the SQL panel examples in `web/src/components/panels/SqlPanel.tsx`): - `locust.current_users` is a gauge stored in `value_int` — use `coalesce(value_double, value_int)`. - `locust.client.duration` histograms are cumulative (`aggregation_temporality = 2`); bounds and bucket_counts are JSON strings, cast with `::DOUBLE[]`. Rates/percentiles must diff consecutive snapshots. - **Timestamps** from `/api/query` come back as ISO strings; HUGEINT values come back as strings (DuckDB JSON limitation, `src/queries.rs`). The web chart layer (`lib/chart.ts` `toNum`/`toTimeMs`) parses numeric strings, so HUGEINT columns still plot. - **Dashboards** (`src/dashboards.rs`, `web/src/lib/dash.ts`, `components/panels/DashboardsPanel.tsx`): YAML docs from two sources — `src/dashboards/*.yml` (embedded via rust-embed, ids `locust`/`dotnet`/ `python`) and `~/.otel-viewer/dashboards/*.yml` (rescanned per request on dir mtime change; builtin docs are parsed once at startup — editing a builtin yml in debug needs a restart, user files don't). Endpoints: `GET /api/dashboards`, `GET /api/dashboards/{id}`. Panel SQL is templated client-side in `renderSql` (lib/dash.ts): `$from_ns`/`$to_ns` numbers, `$` string literals, and an **empty input replaces the quoted token `'$x'` including its quotes with bare NULL** — the `('$x' IS NULL OR … = '$x')` idiom depends on this. Histogram panels must diff cumulative counts, filter `+Inf` bounds with `isfinite()` and coalesce open-bucket percentiles to the last finite bound (see the built-in locust dashboard). Dials/stats read the first numeric column of the last row — give their SQL `ORDER BY ts_ns DESC LIMIT 1`. - **Trace waterfall** (`TraceDetail.tsx`) has a sticky quarter-tick ruler + background gridlines; the overlay shares the rows' exact `grid-cols-[minmax(180px,32%)_1fr] gap-3 px-1` template so lines align with the bar track — keep the classes in sync when changing row layout. - **Desktop app data** lives at `~/Library/Application Support/com.otelviewer.desktop/otel-viewer.duckdb` (macOS; equivalent app-data dir on Windows). Deleting it resets to demo data. ## macOS 26 "white window" — solved, do not reintroduce On macOS 26 (Tahoe), WKWebView silently refuses navigations to plain `http://127.0.0.1:` (Local Network privacy hardening): no error, no TCP connection, permanent `about:blank`. App Transport Security exceptions in Info.plist do **not** help. Therefore the desktop shell must **not** point its window at the local HTTP server. It uses the custom `otelview://` scheme (`register_asynchronous_uri_scheme_protocol` in `src-tauri/src/lib.rs`) which calls the axum router in-process (`otel_viewer::run_with_db` shares the `Db`). On Windows the same scheme is served as `https://otelview.localhost/` — WebView2 intercepts custom protocols via WebResourceRequested as `{http|https}://.localhost` with `http` as the **default**; the `https` form used here requires `.use_https_scheme(true)` on the `WebviewWindowBuilder` (without it the window navigates to `https://otelview.localhost/`, matches nothing and shows "not reachable"). External browser/curl access to `http://127.0.0.1:6666` keeps working.