# 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.