Resolve tickets 071-074, 076: log-stream contamination, sort, ottyl_fmt column, ingest lag

- 076: gate auto-declared log-stream column/message params
  (`ParamDeclaration::log_stream_gated`) behind `is_log_stream_line` at
  extraction time, since their fixed-offset regex has no content anchor
  and previously extracted garbage from continuation lines and the
  header row itself (the regex crate doesn't support lookaround, so
  this couldn't be baked into the regex text).
- 074: end-to-end regression test confirms sorting a real `log stream`
  capture by a log-stream column (e.g. PID) now actually reorders
  results, since 076's fix removes the garbage values that were
  corrupting the sort helper index; also add a "Building sort
  index…" indicator in the timeline UI for a slow first-time sort.
- 072: attach the reserved `ottyl_fmt` pseudo-param to each
  `LogEntryDto`'s params so it can be picked as a Log Preview column
  (previously listed as pickable but always blank).
- 073: track a rolling 5s high-watermark of ingestion-to-engine-processing
  lag and surface it in the nav stats popover next to approx_bytes_total.
- 071: group the regex(...) help button back with the filter bar's other
  controls, and restructure the Filtering docs section into scannable
  per-feature subsections (also fixes stale log_type/process/pid mention).

Tickets 070 (CLI scoping) and 075 (API unresponsiveness investigation)
are left in place per their own scope — both are explicitly
conversation/investigation tasks, not implementation tickets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
This commit is contained in:
Hugh Kaznowski
2026-09-06 15:12:07 +01:00
co-authored by Claude Sonnet 5
parent f34dbba6c7
commit 9fc7540a5b
16 changed files with 511 additions and 460 deletions
@@ -1,49 +0,0 @@
# 071 — Filter bar's syntax button/popup is ugly and incoherent
## Summary
Wishlist feedback, verbatim: "UI filter ugly syntax. The button moved
to the right, away from the existing button cluster. The popup is a
incoherent mess and should be clearer."
Two distinct complaints bundled together:
1. Layout regression — some button (the filter-help/syntax button,
presumably introduced or moved by ticket 065's filter-bar help
rewrite) now sits away from the rest of the filter bar's button
cluster in `FilterBar.tsx` (`webapp/src/components/FilterBar.tsx`,
currently 67 lines), instead of grouped with the others.
2. Content/clarity — whatever popup that button opens (the filter
syntax help introduced/reworked in ticket 065,
"filter-bar-help-should-describe-filter-syntax-not-regex") reads as
an incoherent mess rather than a clear reference.
## Scope
- Reproduce in the running app: open the filter bar, locate the
syntax/help button, and confirm which button has drifted from the
cluster and what "incoherent" means concretely (wall of text?
disorganized sections? unclear examples?) — the wishlist entry
doesn't specify, so this needs eyes-on before a fix.
- Fix the button's position so it's grouped with the filter bar's other
controls again.
- Rework the popup's content/layout for clarity — likely restructuring
into clearly separated sections (e.g. by filter feature: regex match,
metric comparison, datetime, sort) with short examples per section,
rather than a single undifferentiated block.
- Out of scope: changing the underlying filter grammar itself
(`filter_parser.pest`/`filter_parser.rs`) — this is presentation only.
## Acceptance criteria
- The filter syntax button sits back with the filter bar's other
buttons, not isolated to one side.
- The help popup reads as a structured, scannable reference rather than
a single dense block — verified visually in the running app (`make
dev`), not just by inspecting JSX.
## Depends on
None. Touches the same area as ticket 065 and ticket 066
("filter-bar-value-doesnt-reflect-active-preset-or-sort") — check those
haven't already reshuffled this layout before starting.
-44
View File
@@ -1,44 +0,0 @@
# 072 — Log Preview needs a format column (`ottyl_fmt`)
## Summary
Wishlist feedback: "Log preview needs format column." `ottyl_fmt`
(json/logfmt/none, `crate::logfmt::detect_format`) already exists as a
reserved, filterable pseudo-param (`OTTYL_RESERVED_PARAM_NAMES`,
`src/engine.rs:1960`; filter-bar support via
`compile_ottyl_fmt_comparison`, `src/engine.rs:1067`) — you can already
filter `ottyl_fmt==json` in the search bar. But it isn't surfaced as a
visible column in the Log Preview table
(`webapp/src/components/LogPreview.tsx`), and it can't be, because
`LogEntryDto` (`src/dto.rs:10-16`) doesn't include it: `params` only
holds declared `ParamDeclaration` matches, and format detection is
computed line-locally elsewhere, never attached to the returned log
entry.
## Scope
- Add the per-line detected format to `LogEntryDto` (`src/dto.rs:10`),
populated wherever log entries are assembled for the `Logs` query
response in `engine.rs` (reuse `crate::logfmt::detect_format`, the
same detection `ottyl_fmt` filtering already calls).
- Add `ottyl_fmt` as a column option in Log Preview's `ColumnPicker`
(`webapp/src/components/LogPreview.tsx:28-56`) alongside declared
params — it's a reserved pseudo-param like `ottyl_seq`/`ottyl_ts`, so
treat it the way the params list already flags `reserved`
(`dto.rs:38`, `ParamDeclarationDto.reserved`) rather than requiring
it be "declared" to show up as a pickable column.
- Render the column value as the short format label (`json`/`logfmt`/
`none`) matching what `ottyl_fmt` filter comparisons already produce.
## Acceptance criteria
- A "Format" (or `ottyl_fmt`) column is available in the Log Preview's
column picker and, once toggled on, shows the correct detected format
per row — json lines show `json`, logfmt lines show `logfmt`, neither
shows `none`.
- No change to filter-bar `ottyl_fmt` comparison behavior (still works
as before).
## Depends on
None.
@@ -1,64 +0,0 @@
# 073 — Track and surface ingestion-to-engine-processing lag (high watermark)
## Summary
Wishlist feedback, verbatim:
> There needs to be a ingstion to parsing latency on frontend.
> Basically when a log is read, it gets what would be the `ottyl_ts`
> value. However, the engine can lag on ingestion. That lag needs to
> be tracked as a high watermark. We can have a collection that stores
> say 5 highest values within the last 5 seconds. If a value in that
> collection is older than the 5s window, it gets replaced. If a value
> is higher than the minimum current, it gets replaced. The frontend
> will display the highest single value (humantime duration lag, or ms
> lag) alongside where it displays the logs and allocation size.
Confirmed the mechanism this is about: `ingest.rs` stamps
`timestamp: Timestamp::now()` at read time (this becomes `ottyl_ts`,
e.g. `ingest.rs:25`/`:107`) *before* sending `EngineMessage::Ingest`
over the mpsc channel to the engine actor task. The engine only
processes it whenever the actor's message loop gets scheduled to it —
under load (large backlog, slow query in flight, etc.) that gap can
grow. Today nothing measures `(engine processing time) - (ottyl_ts)`,
so there's no visibility into ingest backpressure.
The "allocation size" reference point is `approx_bytes_total`
(`src/dto.rs:80`, ticket 011), currently shown in
`webapp/src/components/NavMenu.tsx:104` in the stats popover — that's
the existing display location the lag figure should sit alongside.
## Scope
- In the engine actor's ingest handling (where `EngineMessage::Ingest`
is received, `engine.rs`), compute `lag = Timestamp::now() -
message.timestamp` at the moment of processing.
- Maintain a small rolling high-watermark collection per the spec
above: keep up to 5 entries, each valid only within the last 5
seconds; a new lag value replaces the current minimum-held entry if
it's larger, and any entry older than the 5s window is evicted
(replaced) regardless of magnitude. Expose the current max of that
collection.
- Add this figure to whatever stats DTO backs the popover
`approx_bytes_total` comes from, and to the corresponding stats
query/response path in `api.rs`.
- Display it in `NavMenu.tsx` next to `approx_bytes_total` (`:104`),
formatted as a humantime-style duration for larger lag and plain ms
for small values — match whatever precedent this codebase already
uses for duration formatting (check `webapp/src/lib` for an existing
humanize helper before writing a new one).
## Acceptance criteria
- Under normal (non-lagged) ingestion, the displayed value stays near
zero.
- Artificially delaying engine processing (e.g. a slow query
saturating the actor's message loop, or a synthetic test) causes the
displayed high-watermark to rise and correctly decays out of the
rolling 5s window afterward.
- The value is visible in the same UI location as
`approx_bytes_total`.
## Depends on
None.
-68
View File
@@ -1,68 +0,0 @@
# 074 — Timeline sort-by-property still doesn't work after tickets 059-061
## Summary
Wishlist feedback, verbatim: "The sorting on the timeline still does
not work. I have tried sorting things that I know are sorted (logs
param timestamp) and things that arent (thread name or pid). It does
not work (change view, index etc). If it's taking time to index, that
needs to be visible."
This is a regression/bug report against functionality that should
already exist: sort-by-param was added in ticket 045, and tickets
060/061 (resolved in commit `de9ba6f`, "Resolve tickets 059-061:
generic log-stream columns + sort helper indexes") replaced the naive
full-materialize-then-sort with an incremental helper-index scheme
(`SortHelperIndexEntry`, `build_sort_helper_index`,
`update_sort_helper_indexes`, `purge_stale_sort_helper_indexes` in
`engine.rs:1332-1431`), wired into the `EngineQuery::Logs` sorted
branch around `engine.rs:1598-1634`. On the frontend, `sortBy`/
`sortMissingFallback` are threaded from `LogTimeline.tsx`'s settings
through `App.tsx` (`:349-350`, `:505-506`) into `fetchLogs`
(`webapp/src/lib/api.ts:256-266`). Nothing found in a static read of
this wiring looks obviously broken, so this needs live reproduction,
not just a code read — treat the below as an investigation, not an
assumed root cause.
## Scope
- Reproduce with `make dev` against real piped input: declare a param
covering thread name or PID, set the timeline's sort dropdown to it,
and confirm whether results actually reorder. Also test sorting by an
already-monotonic value (`ottyl_ts` or a declared timestamp param) to
isolate whether the bug is "sort does nothing" vs. "sort scrambles an
already-correct order."
- Check whether the helper-index rebuild triggers correctly on first
use per sort key (`engine.rs:1604`, `.entry(sort_by...).or_insert_with`)
— a stale/empty helper index reused across a param rename or
re-declaration (see the ticket-061-era comments at `engine.rs:225-233`
about helper indexes needing invalidation when a declaration changes)
is one plausible cause worth ruling in or out.
- Check whether the frontend is actually sending `sort_by` on every
poll/refresh, not just the initial query — a poll path that omits it
(`webapp/src/lib/polling.ts`) would look exactly like "sorting
doesn't work" despite correct backend logic.
- Add visibility for the "still indexing" case the wishlist explicitly
calls out: if a helper index is being built (or rebuilt) for a
newly-selected sort key and that's slow enough to be user-visible,
surface a loading/building state in the timeline UI rather than
silently returning unsorted or partial results while it builds.
- Write a regression test once the actual defect is identified —
ticket 060's existing tests apparently didn't catch this, so identify
what scenario they miss.
## Acceptance criteria
- Sorting the timeline by a declared param (including thread
name/PID-style non-monotonic params) actually reorders displayed log
entries, verified live in the browser.
- Sorting by a param that's already effectively time-ordered doesn't
regress (still displays in the expected order).
- If a helper index build takes long enough to matter, the UI shows
that it's building rather than looking broken or stalled.
## Depends on
Builds on tickets 045, 060, 061 — read those and their resolving
commits (`de9ba6f` and its predecessor implementing 045) before
forming a hypothesis.
@@ -1,104 +0,0 @@
# 076 — `log stream` declared columns get contaminated with garbage on non-columnar lines
## Summary
Wishlist feedback, verbatim: "The `log stream` type whitespace-separated
formatting is not working at all. I have a session `suited-gyrfalcon`
running, please investigate."
Investigated directly against the live `suited-gyrfalcon` instance
(`ottyl sessions` → port 62059 → `/api/params`, `/api/logs`,
`/api/stats`; 236,069 ingested log lines, real `log stream` output).
Root cause confirmed — this is not a gap in header/column detection
(ticket 059's detector itself works correctly on genuine `log stream`
rows), it's that the *declared params* it produces get applied far too
broadly once they exist.
**What's actually happening:** `auto_declare_log_stream_params`
(`engine.rs:472-497`) correctly gates *declaration* on
`is_log_stream_line(text)` (`:484`) — so it only fires column
declarations the first time it sees a genuine columnar data row. But
the resulting `ParamDeclaration.message_match_regex` for each column is
a pure **fixed character-offset** pattern with no content anchor at all
— e.g. observed live: `Activity` → `^.{55}(?P<Activity>\S(?:.{0,19}\S)?)`,
`PID` → `^.{76}(?P<PID>\S(?:.{0,5}\S)?)`, `Thread` →
`^.{32}(?P<Thread>\S(?:.{0,9}\S)?)` (from `log_stream_column_pattern`,
`logfmt.rs:287-303`). Once declared, these are ordinary
`ParamDeclaration`s: the *general* per-line param-derivation path
(`auto_declare_params`/`extract_param_raw`) applies every declared
param's regex to **every** ingested line, with no re-check that the
line is actually log-stream-shaped — there's no gate equivalent to
`is_log_stream_line` at derivation time, only at first-declaration time.
Confirmed impact from the live instance:
- Sampling ~1000-line windows at several points across the 236k-line
stream, roughly **1-2% of lines get a garbage `Timestamp` value**
that isn't a real timestamp — e.g. line 306:
`"so_gencnt: 4042707 so_state: 0x0"` (a continuation line from a
wrapped multi-line network-stats dump, no relation to the columnar
format at all) got `Timestamp: "so_gencnt: 4042707 so_state: 0x0"`,
`Activity`/`PID`/etc sliced from arbitrary character offsets of that
unrelated text. Same for bracket/brace-only continuation lines
(`"}"`, `"]"`) inside pretty-printed dumps.
- Even the header line itself (sequence 0,
`"Timestamp Thread Type Activity PID TTL"`) ends up with
`Activity: "Activity"`, `Thread: "Thread"`, `Timestamp: "Timestamp"`,
`Type: "Type"` as its own param values — because ticket 052's
retroactive re-derivation (`engine.rs:540`, re-running
`auto_declare_params` over every retained line whenever a new param
is declared) re-applies the newly-declared fixed-offset regexes back
over already-ingested lines, including the header line, with no
exemption for it.
- This directly undermines ticket 074's "sorting is broken" complaint
too: sorting by `Timestamp` mixes ~1-2% garbage string values in among
otherwise lexically-sortable ISO timestamps, which would visibly
scramble ordering — worth re-checking ticket 074 once this is fixed,
the two may partially be the same underlying issue.
This explains "not working at all" better than a full parse failure
would: the common case (genuine columnar rows) parses fine — it's that
the contamination on the ~1-2% of non-columnar lines interleaved
throughout every real `log stream` capture (multi-line dumps are
common) makes every declared log-stream column look untrustworthy.
## Scope
- Make the per-column `message_match_regex` self-verifying, not purely
positional: either (a) prefix each generated column/message pattern
(`log_stream_column_pattern`/`log_stream_message_pattern`,
`logfmt.rs:287-320`) with the same shape check `is_log_stream_line`
uses so the regex only matches on a genuinely columnar line, or (b)
gate the general param-derivation path itself so a param declared via
`auto_declare_log_stream_params` is only evaluated against lines
where `is_log_stream_line` holds — (a) is likely simpler since it
keeps the gate local to the declaration rather than adding a
special case to the generic derivation path.
- Fix ticket 052's retroactive re-derivation (`engine.rs:540`) to
respect that same gate, so the header line (and any other
non-columnar retained line) doesn't retroactively pick up columnar
param values.
- Add a regression test using the real contaminated lines captured
above (continuation lines like `"so_gencnt: ... so_state: ..."` and
the header line itself) alongside the existing
`log_stream_sample.txt` fixture — the current fixture and its tests
don't include a multi-line-dump continuation line, which is why
ticket 059's tests passed despite this.
## Acceptance criteria
- Ingesting a `log stream` capture that includes multi-line
continuation content (network stat dumps, bracketed/braced
pretty-printed blocks) does not populate `Timestamp`/`Thread`/`Type`/
`Activity`/`PID`/`TTL` params on those continuation lines.
- The header line itself never ends up with its own column names as
values.
- Existing correctly-parsed columnar rows are unaffected.
- A regression test using real captured contamination examples (not
just the original fixture) passes.
## Depends on
Builds on ticket 059 (commit `de9ba6f`) and ticket 052's retroactive
re-derivation. Related to ticket 074 (timeline sort) — re-verify that
complaint after this fix lands, since garbage `Timestamp` values are a
plausible contributor to it.