Seek log/metric-bucket queries by key range instead of scanning

Log reads (logs_since, the unsorted Logs query path) now seek the
IngestedLog range directly from since_sequence/before_sequence instead
of scanning from IndexKey::Start and filtering every prior entry out
in a closure. Metric-comparison filters and bucket_values now seek to
a single metric's Bucket key range instead of scanning every declared
metric's buckets. evaluate_promql sits on top of bucket_values, so its
per-poll dashboard evaluation is narrowed by the same fix without
further changes; caching prior evaluations across polls is left for a
follow-up if profiling ever shows it's needed (per ticket 081's own
scope, not built speculatively).

Resolves tickets 077, 078, 081.

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-06 20:51:24 +01:00
co-authored by Claude Sonnet 5
parent 65330d37e0
commit 946fd50d53
8 changed files with 128 additions and 195 deletions
+36 -10
View File
@@ -157,13 +157,41 @@ impl EngineSnapshot {
}
pub(crate) fn logs_range(&self) -> impl Iterator<Item = (u64, Timestamp, &str)> {
self.index.range(IndexKey::Start..IndexKey::LogParamSplit).filter_map(|(key, value)| {
match (key, value) {
(IndexKey::IngestedLog { sequence, timestamp }, DataValue::LogLiteral { text }) => {
Some((*sequence, *timestamp, text.as_str()))
}
_ => None,
self.logs_in_sequence_range(..)
}
/// Seeks directly to `sequence_range` within the `IngestedLog` key range instead of
/// scanning from `IndexKey::Start` and filtering after the fact. `IngestedLog` sorts by
/// `(sequence, timestamp)`, and `sequence` is assigned monotonically at ingest
/// (`handle.rs`), so a bound on `sequence` alone is an exact seek — `Timestamp::MIN` is
/// used as the synthetic sub-key since it sorts below any real timestamp for that sequence.
pub(crate) fn logs_in_sequence_range(
&self,
sequence_range: impl std::ops::RangeBounds<u64>,
) -> impl Iterator<Item = (u64, Timestamp, &str)> {
let lower = match sequence_range.start_bound() {
std::ops::Bound::Included(&sequence) => {
IndexKey::IngestedLog { sequence, timestamp: Timestamp::MIN }
}
std::ops::Bound::Excluded(&sequence) => {
IndexKey::IngestedLog { sequence: sequence.saturating_add(1), timestamp: Timestamp::MIN }
}
std::ops::Bound::Unbounded => IndexKey::Start,
};
let upper = match sequence_range.end_bound() {
std::ops::Bound::Included(&sequence) => {
IndexKey::IngestedLog { sequence: sequence.saturating_add(1), timestamp: Timestamp::MIN }
}
std::ops::Bound::Excluded(&sequence) => {
IndexKey::IngestedLog { sequence, timestamp: Timestamp::MIN }
}
std::ops::Bound::Unbounded => IndexKey::LogParamSplit,
};
self.index.range(lower..upper).filter_map(|(key, value)| match (key, value) {
(IndexKey::IngestedLog { sequence, timestamp }, DataValue::LogLiteral { text }) => {
Some((*sequence, *timestamp, text.as_str()))
}
_ => None,
})
}
@@ -173,10 +201,8 @@ impl EngineSnapshot {
since_ms: i64,
now_ms: i64,
) -> impl Iterator<Item = (u64, Timestamp, &str)> {
self.logs_range().filter(move |(sequence, timestamp, _)| {
*sequence < before_sequence
&& timestamp.as_millis() > since_ms
&& timestamp.as_millis() <= now_ms
self.logs_in_sequence_range(..before_sequence).filter(move |(_, timestamp, _)| {
timestamp.as_millis() > since_ms && timestamp.as_millis() <= now_ms
})
}
}