Docs, ticks, working app

This commit is contained in:
Hugh Kaznowski
2026-09-05 15:05:39 +01:00
parent 41362aa3d8
commit 51e577f01e
41 changed files with 1039 additions and 683 deletions
-35
View File
@@ -1,35 +0,0 @@
# 001 — Param declarations
## Summary
Implement the `ParamDeclaration` slice of the primary index: users can declare how a
named parameter is derived from a log line (e.g. deriving a timestamp different from
the ingestion timestamp, or a log level), and the engine stores/serves those
declarations.
## Scope
- `IndexKey::ParamDeclaration { name }` and a matching `DataValue` variant for the
declaration body (a regex against the raw log text, plus how the capture maps to a
`MetricPrimitive` — reuse `RegexToMetricPrimitiveMatcher` / `MetricPrimitiveKind`
from `data_value.rs`, which already exist but are unused).
- Engine message(s) to add/list/remove a param declaration.
- Validate declared regexes at declaration time (reject invalid regex / unknown
primitive kind up front rather than failing per-log later).
- No derivation/backfill against existing logs yet — that's
[002](002-metric-declarations.md) / [003](003-metric-bucket-derivation.md), since
metrics are what actually get indexed into buckets. This ticket is just the
declaration storage + validation.
## Acceptance criteria
- Declaring a param with a valid regex + capture group succeeds and is retrievable.
- Declaring a param with an invalid regex or referencing a non-existent capture group
is rejected with a clear error, not silently stored.
- Declarations sit in their own contiguous range of the BTreeMap
(`ParamDeclaration` between `LogParamSplit` and `ParamMetricSplit`), per the
`IndexKey` ordering in the README.
## Depends on
None — this is foundational.
-35
View File
@@ -1,35 +0,0 @@
# 002 — Metric declarations
## Summary
Implement the `MetricDeclaration` slice of the index: a language/API for declaring a
metric — a regex that matches a log message plus params extracted from it — so that
matching logs can later be bucketed ([003](003-metric-bucket-derivation.md)).
## Scope
- `IndexKey::MetricDeclaration { name }` paired with `DataValue::MetricDeclaration`
(already sketched in `data_value.rs`: `message_match_regex` +
`Vec<RegexToMetricPrimitiveMatcher>`).
- Engine message(s) to add/list/remove a metric declaration.
- A metric declaration's params may reference params already declared via
[001](001-param-declarations.md) (e.g. label a bucket by an existing param) in
addition to inline capture groups on the metric's own regex — decide and document
which is supported for v1 (README implies inline capture-group derivation is the
primary mechanism: "buckets and parameters can be derived" from the metric
declaration itself).
- Validate the regex and capture-group → primitive-kind mapping at declaration time,
same as param declarations.
## Acceptance criteria
- Declaring a metric with a valid regex + param matchers succeeds and is retrievable.
- Invalid regex, or a matcher referencing a capture group the regex doesn't have, is
rejected at declaration time.
- Declarations sit in their own contiguous range of the BTreeMap
(`MetricDeclaration` between `ParamMetricSplit` and `MetricBucketSplit`).
## Depends on
- [001 — Param declarations](001-param-declarations.md) (shares matcher/primitive
validation code and the "derive a value from a regex capture" concept).
-37
View File
@@ -1,37 +0,0 @@
# 003 — Metric bucket derivation
## Summary
Apply metric declarations to log lines and populate `IndexKey::Bucket` /
`DataValue::BucketValue` entries, both retroactively (backfill already-ingested logs
when a metric is declared) and going forward (index each new log against all current
metric declarations as it's ingested).
## Scope
- On `MetricDeclaration` add: scan the existing `IngestedLog` range, run the metric's
regex against each log's text, and for each match compute the labeled `Bucket` key
(`metric_name` + `labels: BTreeMap<String, MetricPrimitive>`) and append
`(sequence, timestamp, value)` to that bucket's `BucketValue`.
- On new log ingest: run all currently-declared metrics' regexes against the line and
update buckets incrementally, same as backfill.
- Decide bucket cardinality behavior: a single log line can match multiple metrics: it
should update all matching buckets. Document how differing label sets from the same
metric become distinct `Bucket` keys (labels are part of the key, so this falls out
naturally from `BTreeMap` — just needs the insert-or-append logic).
- This is the piece that makes `MetricPrimitive`, `DataValue::BucketValue`, and the
`Bucket` index range (currently defined but never populated) actually load-bearing.
## Acceptance criteria
- Declaring a metric against a log stream that already has matching lines populates
buckets for those lines without needing new input.
- Ingesting a new log line after metrics are declared updates the relevant buckets
live.
- A log line matching zero metrics doesn't touch the bucket range.
- Bucket values preserve insertion order / are queryable by sequence and timestamp
range (needed by [004](004-metrics-api.md) and dashboards).
## Depends on
- [002 — Metric declarations](002-metric-declarations.md)
-31
View File
@@ -1,31 +0,0 @@
# 004 — Metrics API
## Summary
Expose param/metric declarations and bucket data over HTTP so the frontend (and the
filter/dashboard query engines) can declare metrics and read their values.
## Scope
- `POST /api/params` — declare a param ([001](001-param-declarations.md)).
- `POST /api/metrics` — declare a metric ([002](002-metric-declarations.md)).
- `GET /api/metrics` — list declared metrics (+ their declared params/labels).
- `GET /api/metrics/:name` — query bucket values for a metric, filtered by label
selector and time/sequence range, mirroring the existing `LogsParams`/
`TimelineParams` pattern in `dto.rs`.
- Follow the existing engine architecture: new `EngineQuery`/`EngineMessage` variants,
handled in `engine.rs`'s single-owner loop, DTOs added to `dto.rs`, routes added to
`api.rs`'s router — no new locking or shared state.
## Acceptance criteria
- Declaring a param/metric via the API round-trips through to engine state and shows
up in `GET /api/metrics`.
- Querying a metric's bucket values supports the same start/end and limit-style
params as the existing logs/timeline endpoints.
- Bad declarations (invalid regex etc.) return a 4xx with a useful error body, not a
500 or silent no-op.
## Depends on
- [003 — Metric bucket derivation](003-metric-bucket-derivation.md)
-37
View File
@@ -1,37 +0,0 @@
# 005 — FilterBar query language (pest grammar)
## Summary
Build the syntax-aware search language described in the README's FilterBar section,
using `pest`, supporting regex searches and metric comparisons combined with
boolean operators.
## Scope
- Add `pest` + `pest_derive` dependencies.
- Grammar covering:
- `regex("...")` — raw-text regex search over log lines.
- `metric_name[param] <op> value` comparisons (`<`, `>`, `<=`, `>=`, `==`, `!=`) —
`metric_name[param]<5` per the README example.
- `sequence` and `timestamp` as reserved metric names usable the same way
(`sequence<5`, `timestamp>12:00`).
- Boolean combinators `and` / `or` with standard precedence, e.g.
`regex("error") and sequence<5 or timestamp>12:00`.
- Parse into an AST (not directly into engine queries — that's
[007](007-filterbar-integration.md)).
- Surface parse errors with position info suitable for showing under the FilterBar
input.
## Acceptance criteria
- Grammar test cases cover: bare regex, single metric comparison, reserved-metric
comparison, `and`/`or` combinations with correct precedence, and at least one
malformed-input case producing a located error rather than a panic.
- Parser is a standalone module with no dependency on the engine/index types, so it
can be unit tested in isolation.
## Depends on
None directly, but the metric-comparison side of the grammar is only meaningful once
[002](002-metric-declarations.md) exists — build/test this ticket against metric
names as opaque strings.
-30
View File
@@ -1,30 +0,0 @@
# 006 — FilterBar datetime parsing
## Summary
Support the several datetime formats the README requires for timestamp comparisons
in the filter language: relative humantime, absolute time, and full dates.
## Scope
- Relative humantime: `5h`, `-3m`, etc. — resolved against "now" (or against the
query's reference time, if one is introduced later) at parse/eval time.
- Absolute time: e.g. `12:00` (time-of-day, as in the README's
`timestamp>12:00` example) — needs a defined rule for which day it resolves to.
- Full date: complete date(+time) literals.
- Produce a single normalized value (`Timestamp`/millis) regardless of input form, for
the filter evaluator ([007](007-filterbar-integration.md)) to compare against.
- Pick and document a crate (e.g. `humantime` for the relative case) rather than
hand-rolling a datetime parser.
## Acceptance criteria
- Each of the three formats parses to the correct absolute millis value in unit
tests, including edge cases (negative relative offsets, time-of-day near midnight).
- Ambiguous/invalid input produces a clear error rather than a silently wrong
timestamp.
## Depends on
None — standalone parsing module, consumed by
[007 — FilterBar integration](007-filterbar-integration.md).
-36
View File
@@ -1,36 +0,0 @@
# 007 — FilterBar query integration
## Summary
Wire the parsed filter AST into actual log/timeline queries against the engine, so
FilterBar's syntax (not just a naive substring match) drives what's shown in
LogPreview and LogTimeline.
## Scope
- Evaluator that walks the AST from [005](005-filterbar-pest-parser.md), resolving
`regex(...)` against log text and `metric_name[param] <op> value` against bucket
data ([004](004-metrics-api.md)), using datetime literals resolved via
[006](006-filterbar-datetime-parsing.md).
- New/extended `EngineQuery::Logs` (and `Timeline`) filter parameter that accepts the
compiled filter, evaluated inside the engine's single-owner loop (per the README's
"engine is sole owner of the btreemap" architecture — don't evaluate against a
copy).
- `sequence`/`timestamp` reserved-metric comparisons map directly to the
`IngestedLog` key fields rather than a declared metric lookup.
- API layer: accept the raw filter string, parse it (return 4xx on parse error),
compile to whatever the engine query needs, send to engine.
## Acceptance criteria
- `GET /api/logs?filter=...` and the timeline equivalent apply the full filter
language, not just substring matching.
- A filter combining a regex clause and a metric comparison with `and`/`or` returns
correct results against a seeded log+metric fixture.
- Malformed filter strings return a 4xx with the parser's error message.
## Depends on
- [004 — Metrics API](004-metrics-api.md)
- [005 — FilterBar pest parser](005-filterbar-pest-parser.md)
- [006 — FilterBar datetime parsing](006-filterbar-datetime-parsing.md)
-33
View File
@@ -1,33 +0,0 @@
# 008 — Settings model
## Summary
Implement the single, process-wide settings model described in the README's NavMenu
section: colour scheme, CSS path config, and the settings file location — settable
via CLI args, persisted at a sensible default OS location.
## Scope
- A `Settings` struct: colour scheme choice, list of imported/linked CSS entries
(see [010](010-css-management.md)), and whatever else profiles need to hang off of.
- Default settings file location using an OS-conventions crate (e.g. `directories`)
rather than a hardcoded path.
- `clap` arg(s) to override the settings file path at runtime (extend the existing
`Args` in `main.rs`, which today only has `--port`/`--static-dir`).
- Load-on-start / save-on-change semantics; "at runtime, there is only 1 settings" —
i.e. a single in-memory settings instance, not per-request state, analogous to how
the engine owns the index.
- Settings "owns" the profiles per the README — this ticket only needs to define
where profiles live under settings, not implement profile CRUD itself (that's
[009](009-presets-profiles.md)).
## Acceptance criteria
- Running with no args creates/reads a settings file at the documented default
location.
- `--settings-path` (or similar) overrides it.
- Settings persist across restarts (edit, restart, re-read matches).
## Depends on
None.
-34
View File
@@ -1,34 +0,0 @@
# 009 — Presets / profiles
## Summary
Implement presets ("profiles") that track parsing/indexing rules (param + metric
declarations) and stored dashboards, so restarting ottyl against a given CLI app
doesn't lose that configuration.
## Scope
- Profile = a named bundle of: param declarations ([001](001-param-declarations.md)),
metric declarations ([002](002-metric-declarations.md)), and saved dashboards
([014](014-dashboards-ui.md), once it exists — can stub the dashboard field until
then).
- Persisted under the settings storage location from
[008](008-settings-model.md); settings reference which profile(s) exist / which is
active.
- On startup, load the active profile's param/metric declarations into the engine
before ingestion starts (or apply-and-backfill if logs arrive before load
completes — define the ordering explicitly).
- CRUD for profiles, exposed later via NavMenu UI ([011](011-navmenu-ui.md)).
## Acceptance criteria
- Declaring params/metrics through a profile, restarting the process, and re-running
the same CLI command reproduces the same bucket derivation without re-declaring
anything.
- Multiple profiles can exist; switching the active profile changes which
declarations are loaded on next start.
## Depends on
- [008 — Settings model](008-settings-model.md)
- [002 — Metric declarations](002-metric-declarations.md)
-30
View File
@@ -1,30 +0,0 @@
# 010 — CSS import/management
## Summary
Let users import custom CSS (stored alongside settings/profiles, deletable) or link
to an external CSS path/URL (not deletable, just referenced).
## Scope
- `POST /api/settings/css` (import): accept raw CSS content, store it in the same
directory as settings/profiles ([008](008-settings-model.md)), return an id/handle.
- `DELETE /api/settings/css/:id`: remove an imported CSS file; reject deletion for
linked (non-imported) entries.
- `POST /api/settings/css/link`: register a path/URL reference without copying
content; these are never deletable via the delete endpoint (only "unlink"-able,
if that distinction matters — clarify in implementation).
- Settings model tracks the list of CSS entries (imported + linked) with enough
metadata to distinguish the two kinds.
- Serve imported CSS content back so the frontend can `<link>`/inline it.
## Acceptance criteria
- Importing CSS persists content under the settings directory and survives restart.
- Deleting an imported CSS entry removes the file and the settings reference.
- Attempting to delete a linked (non-imported) entry is rejected.
- Linked entries resolve to their original path/URL, not a copy.
## Depends on
- [008 — Settings model](008-settings-model.md)
-30
View File
@@ -1,30 +0,0 @@
# 011 — NavMenu UI
## Summary
Build out the NavMenu frontend component for managing settings, presets, and CSS,
per the README's block diagram (NavMenu occupies its own column alongside the rest
of the dashboard).
## Scope
- Settings panel: colour scheme picker, CSS import (file/text input) + list of
imported/linked CSS with delete where applicable.
- Presets panel: list/create/switch/delete profiles ([009](009-presets-profiles.md)),
showing each profile's param/metric declaration counts and saved dashboards.
- Wire to the settings/profile/CSS APIs from
[008](008-settings-model.md)/[009](009-presets-profiles.md)/[010](010-css-management.md).
- Apply the active colour scheme / imported CSS to the app shell at runtime.
## Acceptance criteria
- Switching active profile in the UI changes engine state (declared params/metrics)
without a manual restart, or clearly communicates that a restart is needed if
that's the chosen design from [009](009-presets-profiles.md).
- Importing/deleting CSS in the UI is reflected immediately in the rendered app.
## Depends on
- [008 — Settings model](008-settings-model.md)
- [009 — Presets / profiles](009-presets-profiles.md)
- [010 — CSS import/management](010-css-management.md)
-27
View File
@@ -1,27 +0,0 @@
# 012 — Metrics UI
## Summary
Replace the current "Coming Soon" Metrics placeholder with a real component: declare
metrics via the metric language, and view their derived values.
## Scope
- A form/editor for declaring a metric (regex + param matchers) that calls
`POST /api/metrics` ([004](004-metrics-api.md)), with inline validation errors
surfaced from the API's 4xx responses.
- List of currently-declared metrics with their labels.
- A view of a selected metric's bucket values over the active time range (reuse the
time-range plumbing already in `App.tsx`/`LogTimeline`).
## Acceptance criteria
- Declaring a metric in the UI immediately reflects newly-matching logs (existing
and new) in the metric's value view, per the backfill behavior from
[003](003-metric-bucket-derivation.md).
- Invalid metric definitions show the backend's validation error instead of failing
silently.
## Depends on
- [004 — Metrics API](004-metrics-api.md)
-30
View File
@@ -1,30 +0,0 @@
# 013 — Dashboards: PromQL query support
## Summary
Support PromQL-style queries over declared metrics to derive dashboard
visualizations, per the README's Dashboards section ("There should be support for
promql queries to derive the visualisation").
## Scope
- Evaluate a useful PromQL subset (instant/range vector selection by metric name +
label matchers, at minimum; consider `rate()`/`sum()`/`avg()` aggregations as
stretch) against `Bucket` data ([003](003-metric-bucket-derivation.md)).
- Decide whether to hand-roll the subset or bring in an existing PromQL parser
crate — document the choice.
- New `EngineQuery` variant for PromQL evaluation, following the existing
single-owner-evaluates-in-place pattern.
- API endpoint (e.g. `GET /api/query?promql=...`) returning a chart-ready series
format (labels + time-value pairs), shaped for bar/line/pie consumption by
[014](014-dashboards-ui.md).
## Acceptance criteria
- A basic instant/range query against a declared metric with a label matcher returns
correct series data against a seeded fixture.
- Malformed PromQL returns a 4xx with a useful error.
## Depends on
- [004 — Metrics API](004-metrics-api.md)
-30
View File
@@ -1,30 +0,0 @@
# 014 — Dashboards UI
## Summary
Replace the current "Coming Soon" Dashboards placeholder with a Grafana-style
dashboard builder: bar, line, and pie charts backed by PromQL queries, saved as part
of a profile.
## Scope
- Chart components for bar/line/pie (pick a charting lib — none is currently in
`webapp/package.json`).
- Dashboard = a named set of panels, each with a PromQL query
([013](013-dashboards-promql.md)) and a chart type.
- Save/load dashboards as part of the active profile
([009](009-presets-profiles.md)), so they persist across restarts.
- Dashboard panels respect the app's active time range, same as LogTimeline/
LogPreview.
## Acceptance criteria
- Creating a panel with a PromQL query against a real declared metric renders the
correct chart type with live data.
- Dashboards persist across restart via the active profile.
- Panels re-query and update when the global time range changes.
## Depends on
- [013 — Dashboards: PromQL query support](013-dashboards-promql.md)
- [009 — Presets / profiles](009-presets-profiles.md)
-28
View File
@@ -1,28 +0,0 @@
# 015 — LogPreview: selectable parameter columns
## Summary
The README's LogPreview spec says it should be possible to pick which parameters are
visible as columns, alongside the always-visible sequence/timestamp. Currently
LogPreview only shows sequence, timestamp, and raw text.
## Scope
- Extend the logs API/DTO ([004](004-metrics-api.md) territory, but specifically the
existing `GET /api/logs`) to include derived param values per log line, sourced
from [001](001-param-declarations.md)'s declarations applied to that line.
- Frontend: column picker UI in/near LogPreview; selected columns persist (localStorage
is fine for a per-viewer preference, or fold into settings if it should be shared —
pick localStorage unless product direction says otherwise).
## Acceptance criteria
- Declared params with a match on a given log line appear as selectable, populated
columns in LogPreview.
- Column selection persists across a page reload.
## Depends on
- [001 — Param declarations](001-param-declarations.md)
- [004 — Metrics API](004-metrics-api.md) (for the pattern of extending log query
responses)
-26
View File
@@ -1,26 +0,0 @@
# 016 — FilterBar UI integration
## Summary
Wire the FilterBar frontend component to the real filter query language instead of
the current naive client-side substring match in `App.tsx`
(`logs.filter((l) => l.text.includes(filter))`).
## Scope
- Send the raw filter string to `GET /api/logs`/`GET /api/timeline` via the new
`filter` param from [007](007-filterbar-integration.md), instead of filtering
client-side.
- Surface parse errors from the API (4xx + message) inline under the FilterBar input.
- Remove the now-dead client-side `visibleLogs` substring filter in `App.tsx`.
## Acceptance criteria
- Typing a valid filter expression (regex + metric comparisons combined with
and/or) narrows both LogPreview and LogTimeline via server-side evaluation.
- An invalid expression shows an inline error and does not clear/break the
currently-displayed logs.
## Depends on
- [007 — FilterBar query integration](007-filterbar-integration.md)
+29
View File
@@ -0,0 +1,29 @@
# 019 — Default port should be 0
## Summary
`--port` currently defaults to `8420` (`Args` in `main.rs`). The wishlist asks for a
default of `0`, i.e. let the OS assign an ephemeral free port rather than colliding
with whatever else is running on a fixed port.
## Scope
- Change `Args::port`'s default to `0`.
- After `TcpListener::bind` succeeds, read back the actual bound port (via
`listener.local_addr()`) since binding to `0` means the requested port and the
real one differ.
- Print the real bound port in the startup log line (it already prints
`args.port`, which would otherwise always read `0`).
- Consider whether the actual port needs to be surfaced anywhere else (e.g. a
`GET /api/stats`-style endpoint) — not required unless something already depends
on knowing it programmatically today; check before adding.
## Acceptance criteria
- Running `ottyl` with no `--port` binds an OS-assigned free port instead of a fixed
one, and the startup message prints that actual port.
- `--port <n>` still binds the requested fixed port as before.
## Depends on
None.
+39
View File
@@ -0,0 +1,39 @@
# 020 — File input for ingestion, with restart-safe tailing
## Summary
`ingest.rs` only reads stdin today. The wishlist asks for an optional file path as
an alternative input source, plus resilience to the log file being recreated (e.g.
the host process being tailed restarts and truncates/recreates its log file) —
ottyl should keep tailing rather than treating that as EOF-and-done.
## Scope
- New `clap` arg (e.g. `--file <path>`), mutually exclusive-in-spirit with stdin:
when given, ingest reads from the file instead of stdin; when absent, behavior is
unchanged (stdin, as today).
- File-mode ingestion must behave like `tail -f`: keep reading as new lines are
appended, don't exit on reaching current EOF.
- Detect file recreation (the common "log rotated / process restarted and reopened
the path" case) — e.g. by tracking the inode/file id and re-opening when it
changes, or noticing truncation (current read offset > file size) — and resume
tailing the new file from its start rather than exiting or getting stuck.
- Sequence numbers continue incrementing across a recreation event (per
`IndexKey::IngestedLog`, sequence is ottyl's own line counter, not tied to the
source file).
- Reuse the existing `IngestedLine` / `EngineMessage::Ingest` plumbing — this only
changes where lines come from, not how they're indexed.
## Acceptance criteria
- `ottyl --file some.log` ingests existing content and then picks up appended
lines live.
- Deleting and recreating the file at the same path (simulating a host restart)
does not kill ottyl or stop ingestion — new content from the recreated file
continues to be ingested.
- Running with no `--file` is unchanged: stdin ingestion as today.
## Depends on
None — parallels the existing `ingest::run`, doesn't require any of the metrics/
filter machinery.
+35
View File
@@ -0,0 +1,35 @@
# 021 — Contextual "?" help icons linking into Docs
## Summary
Every major UI section should have a "?" icon that opens the Docs panel scrolled
to (and highlighting) the relevant section, instead of the user having to find it
themselves in the Docs accordion.
## Scope
- A small reusable `HelpIcon`/`HelpLink` component: renders a "?" affordance,
accepts a target docs section id, and on click opens the Docs panel
(`NavMenu`'s `docs` panel / `Docs.tsx`) navigated to that section.
- Add one to each major component per the README's block diagram: `FilterBar`,
`LogTimeline`, `LogPreview`, `Metrics`, `Dashboards`, and NavMenu's own
Settings/Presets panels.
- Docs panel: support being opened with a target section id (not just opened to
the top/last-open accordion item) — expand the right accordion section and
scroll it into view.
- "Flash" behavior: when navigated to via a "?" icon, the target paragraph/section
visually flashes (e.g. a brief background-color pulse) so the user's eye finds it
immediately, then settles back to normal styling.
## Acceptance criteria
- Clicking a "?" icon on any of the listed components opens Docs, expands the
correct accordion section, scrolls it into view, and it visibly flashes.
- Clicking the same icon twice in a row re-triggers the flash (not just a no-op
because the panel is already open to that section).
- Docs opened normally (via the NavMenu Docs button, not a "?" icon) behaves as
today — no flash, no forced section.
## Depends on
None — builds on the existing `Docs.tsx` / NavMenu docs panel.
+53
View File
@@ -0,0 +1,53 @@
# 022 — Metrics rethink: counters as the primitive, histograms/gauges derived
## Summary
The wishlist calls for rethinking the metrics model: everything a metric declares
should fundamentally be a **counter** (an occurrence at a point in time, optionally
labeled). Histograms and gauges are not separate declaration kinds — they're
*derived views* computed from counter data (bucketing counter events into
histogram buckets; reducing counter series into a gauge-style "current value").
This changes how `MetricDeclaration`/`BucketValue` are modeled and how the
already-shipped PromQL-style query support (`promql.rs`) evaluates
`rate()`/`sum()`/histogram/gauge-shaped queries.
## Scope
- Re-express metric declarations as counter declarations: a match increments a
counter for the resolved label set at the matched timestamp. This is close to
today's `BucketValue{values: Vec<(Sequence, Timestamp, MetricPrimitive)>}` — audit
whether the existing bucket representation already is this, or whether captured
non-counter values (e.g. a captured `Number` used as a gauge reading today) need
to be reinterpreted as counter increments plus a separately-derived gauge.
Document the resulting semantics clearly since this is a model change to code
that already exists and has callers (`engine.rs`, `promql.rs`, `Metrics.tsx`,
`Dashboards.tsx`).
- Histogram derivation: given a counter's events over a time range, bucket them
(fixed-width or user-specified boundaries) and expose bucket counts — likely as
a new PromQL-evaluable form (e.g. `histogram(metric_name[param])`) or a query
option, consumed the same way existing series are.
- Gauge derivation: given a counter's events, derive a "last value" / "value at
time T" series from the most recent matching counter event — likely as another
derived query form (e.g. `gauge(metric_name[param])`), rather than a distinct
declaration/storage kind.
- Update `promql.rs`'s evaluator to support these derived forms without breaking
existing raw counter queries.
- Update `Metrics.tsx`'s declaration form/help text and `Dashboards.tsx`'s panel
config if the query surface changes.
## Acceptance criteria
- A declared metric behaves as a counter: each match is an increment/event, not an
arbitrary overwritten value.
- A histogram-derived query against a counter with several distinct captured
values buckets them correctly across a seeded fixture.
- A gauge-derived query against a counter returns the most-recent value at a given
point in the selected time range.
- Existing raw counter queries (sum/rate over a plain counter, as today) continue
to work unchanged.
## Depends on
None — this is a rework of the already-shipped metric/bucket/PromQL code, not new
foundational work. [023](023-duration-param.md), [025](025-default-dashboard-seed.md)
build on the resulting model.
+37
View File
@@ -0,0 +1,37 @@
# 023 — Duration-type parameter
## Summary
Support a `Duration` parameter kind: its value is the time elapsed since the
previous trigger of the *same* metric with the *same* labels/params, measured
using whichever timestamp field the user has configured (ingestion time by
default, or a param-derived timestamp).
## Scope
- New `MetricPrimitive`/param-kind variant for durations (e.g. milliseconds as an
`i64`, consistent with how `Timestamp` is already stored).
- Derivation rule: for a metric match with a given label set, look up that same
label set's previous counter event ([022](022-metrics-counters-rethink.md)) and
compute `this_event_time - previous_event_time` using the configured time
source (ingestion timestamp, or a declared timestamp param per the README's
"deriving a timestamp different from the ingested timestamp" note).
- First occurrence of a given label set has no previous event — define and
document the behavior (e.g. omit the duration value, or emit `null`/zero;
omitting is likely correct so it doesn't pollute histograms with a bogus zero).
- Expose duration values the same way other derived/counter data is queried
(bucket lookup, PromQL, Metrics UI) so it composes with existing dashboards.
## Acceptance criteria
- Two log lines matching the same metric+labels, seconds apart, produce a
duration value equal to that elapsed time on the second occurrence.
- The first occurrence of a given label set produces no duration value (not a
zero or garbage value).
- Duration values are queryable via the same bucket/PromQL surface as other
metric data.
## Depends on
- [022 — Metrics rethink](022-metrics-counters-rethink.md) (durations are computed
between successive counter events of the same metric).
+41
View File
@@ -0,0 +1,41 @@
# 024 — Time range controls (Grafana-style presets)
## Summary
Today the only way to set the active time range is dragging over `LogTimeline`
(`App.tsx`'s `range` state, passed to `LogPreview`/`Metrics`/`Dashboards` as
`startMs`/`endMs`). The wishlist wants explicit controls like Grafana's: an "All"
option and relative presets ("last 5 minutes", etc.), not just drag-to-select.
## Scope
- A time-range control component (dropdown or button row) offering at least:
"All" (no range — today's `range === null` default) and a handful of relative
presets (e.g. last 5m / 15m / 1h / 24h), placed near `LogTimeline` alongside the
existing drag-select interaction.
- Selecting a preset computes `{startMs, endMs}` relative to now and feeds the
same `range` state `App.tsx` already threads through to `LogTimeline`,
`LogPreview`, `Metrics`, and `Dashboards` — no new plumbing needed downstream,
since all four already accept/consume a range.
- Relative presets need to "move with time" while active (e.g. "last 5 minutes"
should mean *rolling* last 5 minutes, not a frozen window from selection time) —
decide and implement a refresh cadence (e.g. re-resolve the relative range on
the same interval the app already polls for new data) versus a one-shot
resolved range; document the choice.
- Manually dragging on `LogTimeline` after a preset is active should override it
back to a fixed (non-rolling) range, same as today.
## Acceptance criteria
- Selecting "All" clears the range the same way today's implicit default does.
- Selecting a relative preset narrows `LogPreview`, `LogTimeline`, `Metrics`, and
`Dashboards` to that window.
- A rolling preset's window visibly advances over time without user interaction,
if that's the chosen behavior (see Scope).
- Dragging on `LogTimeline` while a preset is active switches back to a fixed
range bounded by the drag.
## Depends on
None — the underlying range plumbing (`App.tsx` → `LogTimeline`/`LogPreview`/
`Metrics`/`Dashboards`) already exists; this only adds the control surface.
+39
View File
@@ -0,0 +1,39 @@
# 025 — Seed a default dashboard with 2 default panels
## Summary
`ProfileStore::ensure_default` creates the `Default` profile with no dashboards.
The wishlist wants every fresh Default profile to come with a starter dashboard:
a line chart of "error" vs. non-"error" log volume, and a pie chart of the same
ratio.
## Scope
- On `ensure_default` creating a brand-new `Default` profile (not on every
startup — only when the profile itself is newly created), seed one
`Dashboard` with two panels:
- Line chart: log message count over time, split into "contains `error`" vs.
"does not contain `error`" series.
- Pie chart: same split, as a ratio/total-count breakdown.
- Express both panels as PromQL-style queries against a counter
([022](022-metrics-counters-rethink.md)) — this likely requires an implicit
built-in "log matched regex X" counter concept (e.g. auto-declaring a metric
for `regex("error")` vs. its complement), or a raw-text regex count primitive
in the PromQL surface if one doesn't already exist. Decide and document which.
- Seeded dashboard/panels must persist through the normal profile save path (same
as any user-created dashboard) so it's editable/deletable like any other.
## Acceptance criteria
- A fresh install (no existing settings/profile store) starts up with a `Default`
profile that already has one dashboard containing the line and pie panels
described above, without any user action.
- Restarting with an already-existing `Default` profile does not re-seed or
duplicate the dashboard.
- The seeded panels render real data against a log stream containing a mix of
"error" and non-"error" lines.
## Depends on
- [022 — Metrics rethink](022-metrics-counters-rethink.md) (defines what a
"count of matching logs" counter/query looks like).
+34
View File
@@ -0,0 +1,34 @@
# 026 — Settings: maximum logs kept in memory
## Summary
ottyl currently keeps every ingested log forever in the in-memory `BTreeMap`. The
wishlist wants a settings-configurable cap on how many logs are retained, with no
cap (unlimited) as the default.
## Scope
- New `Settings` field (e.g. `max_logs: Option<usize>`), `None`/unset = unlimited,
matching the existing "by default it should be unlimited" requirement.
- Enforce the cap in the engine's ingest path: once the `IngestedLog` range
exceeds the configured max, evict the oldest entries (lowest `sequence`) to
bring it back under the cap.
- Decide and document what happens to data that references evicted logs:
- Params/metrics already derived into `Bucket` values from an evicted log are
**not** retroactively removed (buckets are aggregate data, not raw logs) —
state this explicitly so it's not treated as a bug later.
- `sequence` numbering does not reset/renumber on eviction.
- Expose the setting in the Settings API/NavMenu settings panel (a numeric input,
clearable back to "unlimited").
## Acceptance criteria
- With no cap configured, behavior is unchanged (unbounded retention, as today).
- Setting a cap and ingesting past it keeps memory usage bounded — oldest logs
are evicted, newest `max_logs` remain queryable via `GET /api/logs`.
- Changing the cap at runtime (via settings) takes effect on the next ingest
without requiring a restart.
## Depends on
None — extends the existing `Settings` model.
+33
View File
@@ -0,0 +1,33 @@
# 027 — Dashboards: remove manual save, autosave everything
## Summary
`Dashboards.tsx` currently has an explicit "Save dashboard" button
(`saveDashboard` called on click). The wishlist wants dashboards to always
autosave — no explicit save step, matching how settings/profile changes already
persist immediately elsewhere in the app.
## Scope
- Remove the "Save dashboard" button and the `saving`/dirty-draft gating around
it in `Dashboards.tsx`.
- Persist panel/name edits automatically: debounce edits (e.g. text field
keystrokes) so autosave doesn't fire on every keystroke, but don't require any
user action to commit a change.
- Cover all mutations that currently route through the save button: panel
add/remove/edit (title, PromQL, chart type), dashboard rename.
- Surface save failures unobtrusively (e.g. a small inline status/error near the
dashboard, not a blocking dialog) since there's no longer a discrete "Save"
click to attach an error to.
## Acceptance criteria
- Editing a panel's title, query, or chart type persists without clicking
anything, verified by reloading the page and seeing the change retained.
- Rapid typing in a text field doesn't spam the save endpoint on every keystroke.
- A failed autosave (e.g. simulated API error) surfaces to the user without
losing the in-progress edit.
## Depends on
None — modifies the existing `Dashboards.tsx` save path.