Move metric declarations and bucket computation to the profile actor (ticket 104)

Metric declare/update/remove, backfill, and per-line apply_all_metrics
now run on the profile actor (engine::profile) instead of inline in the
primary's ingest closure, per idea 0001's resolved decision that the
primary handles params only. The profile actor reuses EngineSnapshot as
its own snapshot type -- declare_metric/update_metric/apply_all_metrics
work unchanged -- and mirrors the primary's param declarations into its
own snapshot on every wake (and synchronously before handling a
DeclareMetric/UpdateMetric message, since there's no ordering guarantee
between the watch-wake and message branches in tokio::select!) so
metric label/value resolution keeps working without storing a second
copy of raw log text; backfill reads lines through its cloned primary
IndexReader instead.

EngineHandle gained a profile_reader field so MetricDeclarations/
BucketValues/Promql queries route to the profile actor's snapshot
instead of the primary's -- without it, metrics could be declared but
never queried, since the primary no longer computes or stores them.
DeclareMetric/UpdateMetric/RemoveMetricDeclaration messages are still
sent to the primary's mpsc channel and proxied to the profile actor by
handle.rs's run() loop; EngineHandle's write-side API is otherwise
unchanged (ticket 106 gives API handlers a direct route). Alert
evaluation still reads the primary's (now metric-less) snapshot
directly -- an accepted gap until ticket 105 moves it, per idea 0001's
resolved decision that alerts move with metric computation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
This commit is contained in:
Hugh Kaznowski
2026-09-07 13:53:06 +01:00
co-authored by Claude Sonnet 5
parent 033e8bbab5
commit ce3e741cb0
6 changed files with 434 additions and 191 deletions
@@ -1,73 +0,0 @@
# 104 — Move metric declarations and bucket computation into the profile actor
## Summary
Second slice of the split: metric declarations, `apply_all_metrics`,
and the declare/update/remove/backfill machinery move off the primary
`EngineSnapshot` and onto the profile actor's `ProfileSnapshot`
(stood up in [[103-profile-actor-skeleton-and-primary-signaling]]).
The primary keeps raw log storage and param declarations only, per
[[0001-actor-split]]'s resolved decision that the primary "extracts
declared + auto params only... no metrics, no profile logic." The
profile actor consumes newly-visible primary lines (via the
watch-wake loop from ticket 103) and runs metric regexes against them
itself, building its own bucket data independent of the primary.
## Where
- `src/engine/snapshot/mod.rs:364-434` (`validate_metric_request`,
`declare_metric`, `resolve_backfill_and_store_metric`) and
`:434-487` (`validate_update_metric`, `update_metric`,
`remove_metric_declaration`) — the declare/update/remove machinery
to move onto `ProfileSnapshot`.
- `src/engine/snapshot/mod.rs:587-617` (`backfill_metric`,
`apply_all_metrics`) — backfill replays `self.logs_range()` (a
primary-side accessor) through `apply_metric_to_log`; once metrics
live on the profile actor, backfill needs to read logs through the
profile actor's cloned primary `IndexReader` instead of `self`.
- `src/engine/handle.rs` — `EngineMessage::DeclareMetric`/
`UpdateMetric`/`RemoveMetricDeclaration` arms (see the message list
at `src/engine/message.rs:30-63`) currently route through the
single actor's `write.mutate` against `EngineSnapshot`; these need
to route to the profile actor's `ProfileSnapshot` instead.
- `src/engine/handle.rs`'s `Ingest` arm — `snap.apply_all_metrics(...)`
is called directly after `ingest_params` today
(see ticket 101's change); this call moves out of the primary's
ingest closure entirely once the profile actor computes metrics
itself from the lines it observes via the watch/read loop, rather
than the primary computing them inline.
## Scope
- Move `MetricDeclaration`/bucket-related `IndexKey`/`DataValue`
variants' *usage* (not necessarily their type definitions, which are
shared) onto `ProfileSnapshot`'s own index/map, separate from the
primary's.
- Move `declare_metric`/`update_metric`/`remove_metric_declaration`/
`backfill_metric`/`apply_all_metrics` (and their validation
counterparts) to operate on `ProfileSnapshot`, reading raw log text
via the profile actor's cloned primary `IndexReader` wherever they
need `logs_range`-style access, instead of `self.index` on the
primary.
- Remove the `apply_all_metrics` call from the primary's `Ingest` arm
in `handle.rs` — the primary no longer computes metrics inline; the
profile actor's watch-wake loop (from ticket 103) now calls
`apply_all_metrics`-equivalent logic itself per newly-visible line.
- `DeclareMetric`/`UpdateMetric`/`RemoveMetricDeclaration` messages
need a new route to the profile actor (a second `mpsc` channel, or
folding into the profile actor's own message enum) — decide the
concrete mechanism during implementation; `EngineHandle`'s public
API doesn't need to change yet if this ticket keeps routing internal
to `handle.rs` (full API surface changes are
[[106-split-enginehandle-api-for-primary-and-profile]]'s scope).
- Out of scope: alert evaluation (still reads the primary's snapshot
directly today — [[105-move-alert-evaluation-to-profile-actor]]
moves it), and the profile-switch (`activate_profile`) reload path
(still targets whatever this ticket lands as the metric-owning
actor, but retargeting `src/api/mod.rs:812-852` explicitly is
[[106-split-enginehandle-api-for-primary-and-profile]]'s scope).
- Backfill cost and blocking behavior: per [[0001-actor-split]]'s
performance rationale, this move is what stops a slow backfill from
blocking the primary's ingest/query-serving — confirm with a test
that ingesting new lines on the primary keeps working while the
profile actor is mid-backfill on a newly declared metric.