Docs, ticks, working app
This commit is contained in:
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user