Files
ottyl/tickets/101-extract-metric-computation-into-separate-stage.md
T
Hugh KaznowskiandClaude Sonnet 5 f43445b280 Arc-ify stored log text to remove per-line double clones (ticket 100)
DataValue::LogLiteral and IngestedLine now carry an Arc<str> instead
of an owned String, so the two clones the ingest path paid per line
(once to move text into the write closure, once to store it) become
cheap refcount bumps instead of full string copies. Also cuts the
per-string-content cost of left_right's full-EngineSnapshot deep clone
on each reconciliation sync.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 13:26:03 +01:00

3.3 KiB

101 — Metric computation is inlined into the single ingest actor's write closure, not a separately-callable stage

Summary

Per-line ingest currently does ingest-lag bookkeeping, log-stream header detection, raw-text storage, auto-param detection, and every declared metric's regex evaluation, all synchronously inside one write.mutate(...) closure in the actor's run() loop, followed by alert evaluation against the result in the same message handler. This is migration step 2 of 0001-actor-split's resolved plan: pull metric computation out into a stage that can be called independently of the ingest closure — still within the single actor for now — so that step 3 (splitting into reader/primary/profile actors) is a wiring change rather than also being the first time metric computation is separated from raw ingest. Depends on 100-arc-ify-stored-log-and-metric-strings landing first (Arc'd text is what makes passing a line into a separate stage cheap).

Where

  • src/engine/handle.rs:132-224 (run()'s EngineMessage::Ingest arm) — the closure passed to write.mutate at :180-198 does header detection, storage, auto_declare_params, and snap.apply_all_metrics(sequence, timestamp, &text_for_op) (:196) all in one synchronous block; alert evaluation follows immediately after at :199-219, reading the snapshot metric computation just produced.
  • src/engine/snapshot/mod.rs:616 (apply_all_metrics) — the entry point that iterates every declared metric and calls apply_metric_to_log (:499-586) per match; currently only reachable as a method on EngineSnapshot invoked from inside the ingest closure above.
  • src/engine/alerts.rs:211 (evaluate_alerts) — currently called directly from the ingest arm (handle.rs:199-219) against the snapshot mutated by the same message; this is the alert-evaluation call site that migration step 3 will later move to a profile actor (per 0001-actor-split's "Resolved decisions" — not in scope for this ticket).

Scope

  • Extract param-extraction (header detection, auto_declare_params, raw storage) and metric-computation (apply_all_metrics) into separate, independently-callable functions/methods with a clear boundary between them — e.g. a function that takes a line and returns/records derived params only, and a second that takes the declared-metrics set plus a param-resolved line and returns bucket updates — rather than one closure that does both inline.
  • Keep both stages running inside the same actor and the same write.mutate transaction for this ticket — the goal is a clean internal seam, not concurrency yet. Step 3 (a separate ticket, not this one) is what actually moves metric computation to a different task.
  • Do not change alert evaluation's behavior or its current call site relative to metric computation (still immediately after, same actor) — only prepare the metric-computation code so it's callable as its own unit; moving alerts to a profile actor is explicitly step 3's concern per 0001-actor-split's resolved decisions.
  • Out of scope: the reader/primary/profile actor split itself, any new tokio::sync::watch signaling, and any change to EngineHandle's public API — this ticket is an internal refactor within the existing single-actor structure.