From 0a21582282c7d7159e1df475883e6202f8188699 Mon Sep 17 00:00:00 2001
From: Hugh Kaznowski
- ottyl turns a CLI program's stdout into a Grafana-like dashboard. Pipe a program's output into it, and it
- indexes each line in memory as it arrives while serving a dashboard from the same process.
- ottyl turns a CLI program's stdout into a dashboard. Pipe a program's output into it:
- Everything — log lines, params, metrics, and derived buckets — lives in one in-memory index for the
- process's lifetime. Nothing touches disk unless you save a profile or change a setting.
+ It indexes each line in memory as it arrives and serves the dashboard from the same process. Everything —
+ log lines, params, metrics, and derived buckets — lives in that in-memory index for the process's
+ lifetime. Nothing touches disk unless you save a profile or change a setting.
CLI arguments Settings
- There is one settings file per machine, holding the colour scheme, any imported/linked CSS, and
+ One settings file per machine holds the colour scheme, imported/linked CSS, the ntfy sink, and
which profile is active. Settings only tracks profile pointers — the profiles themselves (params, metrics,
dashboards) are stored separately, next to the settings file by default.
Profiles
A profile bundles the parsing/indexing rules for one CLI app — its params and metrics, plus any saved
- dashboards — so restarting ottyl against the same program doesn't lose your setup. A
- The filter bar takes a query combining regex search over raw log text with comparisons against declared
- metrics, joined by
-
- Date/time formats Timestamp comparisons accept several forms:
- The same filter string goes to both Date/time formats Timestamp comparisons accept three forms:
+ The filter string is evaluated on the server. A parse error shows as inline text under the filter bar.
- A parameter declaration tells ottyl how to derive a named value from a log line: a regex the message must
- match, a capture group, and a type (
- Params are the building blocks metrics are made of (a metric is really a message-match regex plus a set of
- param matchers), and they drive filtering and the log preview's column picker — declaring a{' '}
-
- The Params panel declares a bare param directly ({'cli_command --verbose | ottyl'}
--port — port to serve the API and webapp on (default 8420).
+ --port — port to serve on. Example: ottyl --port 9000. Default: an OS-assigned
+ free port.
--static-dir — directory containing the built webapp (default webapp/dist).
+ --settings-path — path to the settings file. Example:{' '}
+ ottyl --settings-path ./ottyl-settings.json. Default: a per-OS config location, shown by{' '}
+ ottyl --help.
--settings-path — path to the settings file; defaults to a per-OS config location (e.g.{' '}
- ~/.config/ottyl/settings.json on Linux).
+ --file — tail a file instead of reading stdin, like tail -f. Example:{' '}
+ ottyl --file /var/log/app.log.
+ --metrics-url — poll a Prometheus/OpenMetrics endpoint for metrics, alongside log ingestion.
+ Example: ottyl --metrics-url http://localhost:9100/metrics. Scraped samples are queryable by
+ metric name in PromQL and dashboards immediately, with no declaration step.
Default{' '}
- profile always exists and can't be deleted. Creating a new profile copies the active one (or starts empty
- if none is active), so variations build on what you already have. Switching profiles hot-swaps the running
- engine's declared params/metrics immediately, with no restart needed.
+ dashboards — so restarting ottyl against the same program keeps your setup. The Default{' '}
+ profile always exists and can't be deleted. The profile dropdown's "+" button creates a new profile as a
+ copy of the active one, and its "×" button deletes the active profile. Switching profiles hot-swaps the
+ running engine's declared params/metrics immediately, with no restart.
and/or:
+ The filter bar combines regex search over raw log text with comparisons against declared metrics, joined
+ by and/or. Examples:
{'regex("error") and sequence<5 or timestamp>12:00'}
- {'metric_name[param]<5'} compares a metric's captured param against a number, string, or
- timestamp. sequence and timestamp are reserved metric names present on every log
- line, so they're always filterable without declaring anything.
-
5h, -3m
+ {'regex("error")'} — lines containing "error".
+ {'regex("error") and sequence<5'} — lines containing "error" with sequence under 5.
+ {'requests[duration]>100'} — the requests metric's captured{' '}
+ duration param over 100.
+ {'timestamp>-5m'} — lines from the last 5 minutes.
GET /api/logs and GET /api/timeline as the{' '}
- filter query param and is evaluated on the server; a parse error surfaces as inline text under
- the filter bar rather than crashing the page.
+ sequence and timestamp are reserved metric names present on every log line, so
+ they're always filterable without declaring anything.
+
+
+ 5h (5 hours from now), -3m (3 minutes ago)
+ 12:00, 09:30:00
+ 2026-01-15, 2026-01-15T09:30:00Z
+ string, number, or timestamp).
- Declaring one retroactively indexes every already-ingested line and checks every new line as it arrives.
+ A parameter declaration derives a named value from a log line: a regex the message must match, a capture
+ group, and a type (string, number, or timestamp). Example:
+
duration
+ {'duration=(?P
+ duration
+ number
+ duration param, for instance, makes it available as both a log-table column and a comparable
- value in the filter bar.
- POST /api/params) — the same request the
- Metrics panel's declaration form makes per param matcher. Declaring one there updates the log preview's
- column picker immediately, with no page reload needed.
+ Declaring a param indexes every already-ingested line immediately, then checks every new line as it
+ arrives. It shows up right away as a column in the log table. To filter on it (e.g.{' '}
+ requests[duration]>100), declare it as a param on a metric instead — see the Metrics
+ section below.
- The preset buttons (All, 5m, 15m, 1h,{' '}
- 24h) are Grafana-style shortcuts: All clears the range entirely, and a relative
- preset is rolling — its window keeps advancing to stay "last N minutes from now" instead of
- freezing at selection time. Dragging while a preset is active switches to a fixed (non-rolling) range, same
- as a normal drag.
+ The preset buttons — All, 1m, 2m, 5m, 15m,{' '}
+ 1h, 24h — are quick-range shortcuts. All clears the range. Every
+ other preset is rolling: its window keeps advancing to stay "last N minutes from now" instead of
+ freezing at selection time. Dragging while a preset is active switches to a fixed range.
The log table lists ingested lines newest-last, each with its sequence number, timestamp, and raw text. The column picker (top-right) adds a column per declared param, showing that line's derived value (blank if it - didn't match) — a quick way to eyeball params without switching to the Metrics panel. + didn't match). +
++ Scrolling stays anchored to the row you're looking at as new lines arrive or old ones age out of a rolling + window — the view doesn't jump when content is pruned from the top.
), @@ -155,16 +185,29 @@ const SECTIONS: DocSection[] = [ body: (- Every metric is fundamentally a counter: a message-match regex paired with one or more named param - matchers (each with its own capture group and type). Every log line matching the regex is one counter - increment — a timestamped occurrence, tagged with the labels captured from that line. + Every metric is a counter: a message-match regex paired with one or more named param matchers + (each with its own capture group and type). Every log line matching the regex is one counter increment — a + timestamped occurrence, tagged with the labels captured from that line. Example:
+requests
+ {'request path=(?P\\S+) duration=(?P\\d+)ms'}
+ path (capture group path, type string, role{' '}
+ label), duration (capture group duration, type number
+ , role value)
+
Each param is either a label (the default) or the metric's single value{' '}
- param. Labels identify which bucket an occurrence belongs to; a value param's captured number becomes the
- occurrence's stored value instead of a label — e.g. a duration param marked value{' '}
- lets you query the distribution or latest reading, not just how often it occurred. A metric with only
- label params defaults every occurrence's value to a fixed 1 (a pure occurrence count).
+ param. Labels identify which bucket an occurrence belongs to. A value param's captured number becomes the
+ occurrence's stored value instead of a label — in the example above, duration lets you query
+ the distribution or latest reading, not just how often the line occurred. A metric with only label params
+ defaults every occurrence's value to a fixed 1 (a pure occurrence count).
Declaring a metric backfills it against everything already ingested, then keeps indexing new lines as they @@ -232,39 +275,44 @@ const SECTIONS: DocSection[] = [
An alert rule fires a notification when its condition matches a newly-ingested log line, evaluated per line - as it arrives rather than backfilled against history. There are four condition types: + as it arrives. There are four condition types:
{'regex("panic")'}.
{'regex("error")'}, op{' '}
+ >=, threshold 10, window 1m.
{'user_id=(?P\\d+)'} , capture group id. Seen values live only in memory, so
+ a restart forgets them.
{'regex("error")'}, query B {'regex("request")'}, op >=, ratio{' '}
+ 0.1, window 5m.
- Query-based conditions (Immediate/Threshold/Ratio) reuse the filter bar's syntax, but only the subset that
- evaluates a single line's own fields — regex(...), sequence,{' '}
- timestamp, and and/or. A metric_name[param] comparison
- isn't supported in an alert query.
+ Query-based conditions (Immediate/Threshold/Ratio) reuse the filter bar's syntax, restricted to{' '}
+ regex(...), sequence, timestamp, and and/or
+ . A metric_name[param] comparison isn't supported in an alert query.
- A window accepts a plain number of milliseconds or a humantime-style duration like 30s,{' '}
+ A window accepts a plain number of milliseconds or a humantime-style duration: 30s,{' '}
5m, 1h.
- Every rule delivers to an ntfy sink: a topic (required) and an optional server override (default{' '}
- https://ntfy.sh). "Send test notification" delivers to the form's current topic/server without
- needing to save the rule first.
+ Every rule delivers to the single ntfy sink configured in Settings: a topic and an optional server override
+ (default https://ntfy.sh). Configure the sink and send a test notification from the Settings
+ panel — the Alerts panel only defines rule conditions.
- The Settings panel controls process-wide state: colour scheme, the max number of logs kept in memory - (blank means unlimited), and imported/linked CSS. Changes apply immediately and persist to the settings - file on disk — no save button needed. + The Settings panel controls process-wide state: colour scheme, the ntfy sink alert rules deliver to, the + max number of logs kept in memory (blank means unlimited), and imported/linked CSS. Changes apply + immediately and persist to the settings file on disk. There is no save button.
- The Presets panel manages profiles: create, switch, and delete them. Switching activates a profile's
- params/metrics in the running engine immediately, no restart needed — see General idea above for
- what a profile stores.
+ The profile dropdown in the sidebar manages profiles. Selecting one switches to it and activates its
+ params/metrics in the running engine immediately, with no restart. The + button prompts for a
+ name and creates a new profile as a copy of the active one, then switches to it. The × button
+ deletes the active profile; it's disabled for Default, which can't be deleted. See{' '}
+ General idea above for what a profile stores.