Files
ottyl/ideas/0002-nested-and-jq-param-extraction.md
2026-09-07 12:16:36 +01:00

159 lines
8.1 KiB
Markdown

# Idea: dependent/nested param extraction and jq-style JSON extraction
## Original note
1. A param currently auto-detects or is declared via a regex against
the raw log line — that's the baseline, already true today.
2. Semantics for "has a value": if the message has a non-empty or
missing capture for the target group, treat that as the param
having a value (as opposed to today's behavior — needs
confirmation, see Open questions).
3. Extract a param from *another param's already-captured string*,
applying a second regex on top of it, rather than always
re-matching the raw line.
4. When a string param (or the whole message) is JSON, extract fields
via `jq` syntax rather than the current plain dot-path.
## Current state (confirmed in code)
- `ParamDeclaration` (`src/engine/data_value.rs:6-20`) supports exactly
one regex + one named capture group per param, or one JSON
dot-path, or the computed `elapsed_since_last` — these three modes
are mutually exclusive (validated in
`src/engine/snapshot/mod.rs:618-633`).
- `extract_param_raw` (`src/engine/snapshot/params.rs:85-99`) *always*
operates on the raw ingested line's text — there is no "source
param" concept anywhere in `ParamDeclaration`/
`ParamDeclarationRecord`/`ParamDeclarationDto`. Every param
independently re-parses the same raw line; none can chain off
another param's captured value.
- JSON extraction (`extract_json_field`, `src/engine/data_value.rs:22-39`)
requires the whole line to parse as a JSON object, and walks a plain
dot-separated path through nested objects only — no array indexing,
no jq filters/pipes/functions. Frontend surface:
`webapp/src/components/Params.tsx:24-33` (gates the "JSON field"
option on `looksLikeJsonObject`), free-text path input, no
path-builder/autocomplete.
- No `jq` crate or jq-like dependency exists in the codebase today.
## Why this is an idea, not a ticket yet
This is a real feature addition with several open design questions
that change its shape and scope significantly — it's not a
well-bounded bug fix or mechanical change.
## Open questions
- **Chaining model**: should a param declaration gain an optional
"source param" field (extract from another param's value instead of
the raw line)? If so, declarations form a small dependency graph —
what happens on a cycle, or when the source param has no value for a
given line (does the dependent param also have no value, silently)?
Does extraction order need to become topological instead of the
current independent-per-declaration model?
- **Missing/empty capture semantics**: what should currently happen
when a regex matches but the named group didn't participate (e.g. an
optional group in an alternation)? Worth checking
`extract_param_raw`'s exact behavior before assuming this needs a
change — it may already do the right thing.
- **jq scope**: is full jq syntax the actual goal, or would extending
the existing dot-path extractor with array indexing (`items[0].id`)
cover most real cases far more cheaply than pulling in a jq
implementation (e.g. the `jaq` crate) as a new dependency? jq's
filter language (pipes, `select()`, `map()`) is a much bigger surface
to expose safely in a declaration UI than a path string.
- **UI shape**: `Params.tsx`'s declaration form is built around one
regex/one capture group or one JSON path; a chained/jq model needs a
materially different UI (e.g. "extract from: [raw message ▾] or
[param: session_id ▾]" plus a jq expression box with live preview
against a sample line).
## Suggested next step
Worth a short design pass (e.g. `ce-brainstorm` or a quick
conversation) to settle the chaining model and jq-vs-extended-dot-path
question before this gets broken into tickets — the two features (2/3
and 4) could also ship independently rather than as one big change.
## Implications and things to consider
### Performance
- Chained extraction means a line can now do N regex/JSON passes
instead of 1 per param, and a dependent param's cost is paid *after*
its source param already ran — worst case, a profile with a long
dependency chain multiplies per-line ingest cost by chain depth.
This lands directly in the single actor's per-line ingest path (see
[[0001-actor-split]] — if that split happens first, param extraction
is exactly the workload it isolates, so it's worth sequencing this
after that decision rather than before).
- A `jq`-style engine (even a Rust one like `jaq`) is meaningfully
more expensive per invocation than a compiled `regex::Regex` capture
— evaluating a jq filter against a parsed `serde_json::Value` for
*every* ingested line for *every* JSON-extraction param is a
different cost profile than today's simple dot-path walk. Worth
benchmarking before committing to full jq rather than assuming it's
free.
- Topological ordering of dependent params (if declarations form a
DAG) adds a per-declaration-change cost (recompute order) but not a
per-line cost, as long as the order is cached after each
add/remove/edit rather than recomputed per line.
### Code structure
- `extract_param_raw` (`src/engine/snapshot/params.rs:85-99`) currently
has a flat signature `(text: &str, declaration) -> Option<String>`.
Chaining requires either passing in the full set of already-extracted
param values for the current line (so a declaration can look up its
source param's value) or restructuring extraction as a fold over
declarations in dependency order — a real shape change to this
function and its caller, not a param-by-param tweak.
- The "three mutually exclusive modes" validation in
`src/engine/snapshot/mod.rs:618-633` needs a fourth mode (or an
orthogonal "source" field crossed with the existing regex/JSON
modes) — worth deciding whether source-param and JSON-field are
independent axes (e.g. "JSON-extract from param X" as well as from
the raw line) since that combination is implied by note #3.
- This is exactly the shape of change flagged in this repo's
`CLAUDE.md` as needing three coordinated updates (engine message +
handler, settings/profile persistence, api.rs route) plus a frontend
affordance — touches more surface area than the "Current state"
section alone suggests.
### Functionality
- Cycle detection isn't optional once chaining exists — a user could
declare param B from param A and (accidentally, via UI) param A from
param B. Needs either a hard validation error at declare-time or a
documented "first N declarations in insertion order" fallback.
- Auto-derived params (JSON auto-flattening,
`flatten_json_object`/`flatten_json_object_into`,
`src/engine/data_value.rs:41-60+`) aren't user declarations — decide
whether a chained/jq param can source from an *auto-derived* param,
which would tie this feature to auto-detection internals that
weren't designed to be a stable "source" API.
- Backfill: declaring a new dependent param triggers the same
`backfill_metric`-style history replay as any param declaration
(see [[0004-profile-switch-reindexing-cost]] for the general
backfill-cost concern) — a chain of N dependent params means N
passes over retained history on declare, not just N passes per new
line going forward.
### Not yet considered
- Error/partial-failure UX: what does the UI show for a param whose
jq expression is syntactically valid but fails on a particular
line's JSON shape (e.g. missing key, type mismatch) — silent
no-value, or a visible per-line extraction error surfaced somewhere?
- Whether jq's more powerful operations (`map`, `select`, arithmetic)
should be allowed at all in a param *value* context, where the
result is expected to be a single scalar — most of jq's power is
about reshaping/filtering collections, which doesn't map cleanly
onto "this param has one value per log line."
- No investigation yet into how this interacts with `sort_by`
(`SortHelperKey`) or PromQL — do chained/jq-derived params get the
same downstream treatment (sortable, usable in metric declarations,
usable in dashboard queries) as directly-extracted params, or are
there gaps once the extraction pipeline is no longer "one regex
against the raw line"?