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
+60 -13
View File
@@ -394,18 +394,52 @@ impl EngineSnapshot {
Ok(re)
}
/// Self-contained declare, backfilling from `self.logs_range()` — only used by tests that
/// build a standalone `EngineSnapshot` without the actor split; production code always goes
/// through the profile actor, which calls `declare_metric_with_lines` directly (its own
/// snapshot holds no raw log text of its own to backfill from).
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn declare_metric(&mut self, request: DeclareMetricRequest) -> Result<(), String> {
let lines = self.owned_logs_range();
self.declare_metric_with_lines(request, &lines)
}
/// Like `declare_metric`, but backfills from `lines` instead of `self.logs_range()` — the
/// profile actor (`engine::profile`) calls this, since its own snapshot holds no raw log
/// text (only mirrored param declarations plus its own metric declarations/buckets); it
/// sources backfill lines by reading through its cloned primary `IndexReader` instead.
pub(crate) fn declare_metric_with_lines(
&mut self,
request: DeclareMetricRequest,
lines: &[(u64, Timestamp, String)],
) -> Result<(), String> {
let re = self.validate_metric_request(&request)?;
self.metric_regex_cache
.insert(request.name.clone(), re.clone());
self.resolve_backfill_and_store_metric(request, &re);
self.resolve_backfill_and_store_metric(request, &re, lines);
Ok(())
}
/// Shared by `declare_metric` and `update_metric`: resolves param declarations, backfills
/// the metric's buckets from existing log history, then writes the `MetricDeclaration` entry.
/// Callers differ only in what happens before this (rename bookkeeping for updates).
fn resolve_backfill_and_store_metric(&mut self, request: DeclareMetricRequest, re: &Regex) {
/// `logs_range` collected into owned `(sequence, timestamp, text)` triples, for callers
/// (like `declare_metric`) that need to hand backfill lines to a method taking them by
/// value rather than borrowing `self` for the duration.
#[cfg_attr(not(test), allow(dead_code))]
fn owned_logs_range(&self) -> Vec<(u64, Timestamp, String)> {
self.logs_range()
.map(|(sequence, timestamp, text)| (sequence, timestamp, text.to_string()))
.collect()
}
/// Shared by `declare_metric`/`update_metric` (and their `_with_lines` variants): resolves
/// param declarations, backfills the metric's buckets from `lines`, then writes the
/// `MetricDeclaration` entry. Callers differ only in what happens before this (rename
/// bookkeeping for updates) and where `lines` comes from.
fn resolve_backfill_and_store_metric(
&mut self,
request: DeclareMetricRequest,
re: &Regex,
lines: &[(u64, Timestamp, String)],
) {
let resolved_params = self.resolved_param_declarations();
self.backfill_metric(
&request.name,
@@ -415,6 +449,7 @@ impl EngineSnapshot {
&resolved_params,
request.matched_labels.as_ref(),
request.unmatched_labels.as_ref(),
lines,
);
self.index.insert(
IndexKey::MetricDeclaration {
@@ -451,10 +486,25 @@ impl EngineSnapshot {
self.validate_metric_request(request)
}
/// Self-contained update, backfilling from `self.logs_range()` — see `declare_metric`'s doc
/// comment; only used by tests that build a standalone `EngineSnapshot`.
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn update_metric(
&mut self,
existing_name: &str,
request: DeclareMetricRequest,
) -> Result<(), String> {
let lines = self.owned_logs_range();
self.update_metric_with_lines(existing_name, request, &lines)
}
/// Like `update_metric`, but backfills from `lines` instead of `self.logs_range()` — see
/// `declare_metric_with_lines`.
pub(crate) fn update_metric_with_lines(
&mut self,
existing_name: &str,
request: DeclareMetricRequest,
lines: &[(u64, Timestamp, String)],
) -> Result<(), String> {
let re = self.validate_update_metric(existing_name, &request)?;
@@ -468,7 +518,7 @@ impl EngineSnapshot {
self.metric_regex_cache
.insert(request.name.clone(), re.clone());
self.resolve_backfill_and_store_metric(request, &re);
self.resolve_backfill_and_store_metric(request, &re, lines);
Ok(())
}
@@ -593,11 +643,8 @@ impl EngineSnapshot {
resolved_params: &HashMap<String, ResolvedParam>,
matched_labels: Option<&BTreeMap<String, String>>,
unmatched_labels: Option<&BTreeMap<String, String>>,
lines: &[(u64, Timestamp, String)],
) {
let lines: Vec<(u64, Timestamp, String)> = self
.logs_range()
.map(|(sequence, timestamp, text)| (sequence, timestamp, text.to_string()))
.collect();
for (sequence, timestamp, text) in lines {
self.apply_metric_to_log(
metric_name,
@@ -607,9 +654,9 @@ impl EngineSnapshot {
resolved_params,
matched_labels,
unmatched_labels,
sequence,
timestamp,
&text,
*sequence,
*timestamp,
text,
);
}
}