Move approx_log_bytes into a flavour::LogLiteralHelper trait

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 01:24:06 +01:00
co-authored by Claude Sonnet 5
parent 82fa0dfeb4
commit d0ce524e95
5 changed files with 148 additions and 45 deletions
+129 -37
View File
@@ -1,19 +1,18 @@
//! The left-right write/read plumbing around `EngineSnapshot`, the actor loop that owns it, //! The left-right write/read plumbing around `EngineSnapshot`, the actor loop that owns it,
//! and the `EngineHandle` other tasks use to talk to that actor. //! and the `EngineHandle` other tasks use to talk to that actor.
use crate::consts::{ use crate::config::SettingsHandle;
INGEST_LAG_WATERMARK_MAX_ENTRIES, INGEST_LAG_WATERMARK_WINDOW_MS, use crate::consts::{INGEST_LAG_WATERMARK_MAX_ENTRIES, INGEST_LAG_WATERMARK_WINDOW_MS};
};
use crate::engine::alerts::{self, AlertRuntimeState}; use crate::engine::alerts::{self, AlertRuntimeState};
use crate::engine::data_value::DataValue; use crate::engine::data_value::DataValue;
use crate::engine::index_key::IndexKey; use crate::engine::index_key::IndexKey;
use crate::engine::message::EngineMessage; use crate::engine::message::EngineMessage;
use crate::engine::metric_primitive::{MetricPrimitive, Number}; use crate::engine::metric_primitive::{MetricPrimitive, Number};
use crate::engine::snapshot::EngineSnapshot;
use crate::engine::snapshot::params::log_stream_header_transition; use crate::engine::snapshot::params::log_stream_header_transition;
use crate::engine::snapshot::{approx_log_bytes, EngineSnapshot};
use crate::engine::timestamp::Timestamp; use crate::engine::timestamp::Timestamp;
use crate::engine::{ClearParamsResult, IngestedLine}; use crate::engine::{ClearParamsResult, IngestedLine};
use crate::config::SettingsHandle; use crate::flavour::LogLiteralHelper;
use left_right::Absorb; use left_right::Absorb;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
@@ -61,7 +60,9 @@ pub struct IndexReader {
impl Clone for IndexReader { impl Clone for IndexReader {
fn clone(&self) -> Self { fn clone(&self) -> Self {
IndexReader { factory: self.factory.clone() } IndexReader {
factory: self.factory.clone(),
}
} }
} }
@@ -78,14 +79,22 @@ pub struct EngineHandle {
impl Clone for EngineHandle { impl Clone for EngineHandle {
fn clone(&self) -> Self { fn clone(&self) -> Self {
EngineHandle { tx: self.tx.clone(), reader: self.reader.clone() } EngineHandle {
tx: self.tx.clone(),
reader: self.reader.clone(),
}
} }
} }
impl EngineHandle { impl EngineHandle {
pub async fn barrier(&self) { pub async fn barrier(&self) {
let (respond_to, response) = oneshot::channel(); let (respond_to, response) = oneshot::channel();
if self.tx.send(EngineMessage::Barrier(respond_to)).await.is_ok() { if self
.tx
.send(EngineMessage::Barrier(respond_to))
.await
.is_ok()
{
let _ = response.await; let _ = response.await;
} }
} }
@@ -94,19 +103,35 @@ impl EngineHandle {
pub fn spawn(settings: SettingsHandle, channel_capacity: usize) -> EngineHandle { pub fn spawn(settings: SettingsHandle, channel_capacity: usize) -> EngineHandle {
let (tx, rx) = mpsc::channel(channel_capacity); let (tx, rx) = mpsc::channel(channel_capacity);
let (write, read) = left_right::new::<EngineSnapshot, EngineOp>(); let (write, read) = left_right::new::<EngineSnapshot, EngineOp>();
let reader = IndexReader { factory: read.factory() }; let reader = IndexReader {
factory: read.factory(),
};
tokio::spawn(run(rx, settings, IndexWriter { handle: write })); tokio::spawn(run(rx, settings, IndexWriter { handle: write }));
EngineHandle { tx, reader } EngineHandle { tx, reader }
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn test_handle(channel_capacity: usize) -> (EngineHandle, mpsc::Receiver<EngineMessage>) { pub(crate) fn test_handle(
channel_capacity: usize,
) -> (EngineHandle, mpsc::Receiver<EngineMessage>) {
let (tx, rx) = mpsc::channel(channel_capacity); let (tx, rx) = mpsc::channel(channel_capacity);
let (_write, read) = left_right::new::<EngineSnapshot, EngineOp>(); let (_write, read) = left_right::new::<EngineSnapshot, EngineOp>();
(EngineHandle { tx, reader: IndexReader { factory: read.factory() } }, rx) (
EngineHandle {
tx,
reader: IndexReader {
factory: read.factory(),
},
},
rx,
)
} }
async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mut write: IndexWriter) { async fn run(
mut rx: mpsc::Receiver<EngineMessage>,
settings: SettingsHandle,
mut write: IndexWriter,
) {
let mut max_logs: Option<usize> = None; let mut max_logs: Option<usize> = None;
let mut metric_sample_sequence: u64 = 0; let mut metric_sample_sequence: u64 = 0;
let mut alert_states: HashMap<String, AlertRuntimeState> = HashMap::new(); let mut alert_states: HashMap<String, AlertRuntimeState> = HashMap::new();
@@ -116,7 +141,11 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
while let Some(message) = rx.recv().await { while let Some(message) = rx.recv().await {
match message { match message {
EngineMessage::Ingest(line) => { EngineMessage::Ingest(line) => {
let IngestedLine { sequence, timestamp, text } = line; let IngestedLine {
sequence,
timestamp,
text,
} = line;
let processed_at = Timestamp::now(); let processed_at = Timestamp::now();
record_ingest_lag( record_ingest_lag(
&mut ingest_lag_samples, &mut ingest_lag_samples,
@@ -128,19 +157,32 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
let is_new_header = crate::engine::logfmt::LogStreamHeader::detect(&text).is_some(); let is_new_header = crate::engine::logfmt::LogStreamHeader::detect(&text).is_some();
log_stream_header = log_stream_header_transition(&log_stream_header, &text); log_stream_header = log_stream_header_transition(&log_stream_header, &text);
let row_header = if is_new_header { None } else { log_stream_header.clone() }; let row_header = if is_new_header {
None
} else {
log_stream_header.clone()
};
let (rules, ntfy_topic, ntfy_server) = { let (rules, ntfy_topic, ntfy_server) = {
let settings = settings.lock().unwrap(); let settings = settings.lock().unwrap();
(settings.alert_rules.clone(), settings.ntfy_topic.clone(), settings.ntfy_server.clone()) (
settings.alert_rules.clone(),
settings.ntfy_topic.clone(),
settings.ntfy_server.clone(),
)
}; };
let text_for_op = text.clone(); let text_for_op = text.clone();
write.mutate(move |snap: &mut EngineSnapshot| { write.mutate(move |snap: &mut EngineSnapshot| {
snap.approx_bytes_total += approx_log_bytes(&text_for_op); snap.approx_bytes_total += text_for_op.approx_log_bytes();
snap.index.insert( snap.index.insert(
IndexKey::IngestedLog { sequence, timestamp }, IndexKey::IngestedLog {
DataValue::LogLiteral { text: text_for_op.clone() }, sequence,
timestamp,
},
DataValue::LogLiteral {
text: text_for_op.clone(),
},
); );
snap.log_count += 1; snap.log_count += 1;
if let Some(header) = row_header.as_ref() { if let Some(header) = row_header.as_ref() {
@@ -157,11 +199,23 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
let fired = write let fired = write
.peek() .peek()
.map(|guard| { .map(|guard| {
alerts::evaluate_alerts(&rules, &mut alert_states, &guard, sequence, timestamp, &text) alerts::evaluate_alerts(
&rules,
&mut alert_states,
&guard,
sequence,
timestamp,
&text,
)
}) })
.unwrap_or_default(); .unwrap_or_default();
for (rule, message) in fired { for (rule, message) in fired {
tokio::spawn(alerts::deliver_for_rule(rule.name, message, ntfy_topic.clone(), ntfy_server.clone())); tokio::spawn(alerts::deliver_for_rule(
rule.name,
message,
ntfy_topic.clone(),
ntfy_server.clone(),
));
} }
} }
EngineMessage::IngestMetricSample(sample) => { EngineMessage::IngestMetricSample(sample) => {
@@ -175,7 +229,13 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
let timestamp = sample.timestamp; let timestamp = sample.timestamp;
let value = MetricPrimitive::Number(Number::Float(sample.value)); let value = MetricPrimitive::Number(Number::Float(sample.value));
write.mutate(move |snap: &mut EngineSnapshot| { write.mutate(move |snap: &mut EngineSnapshot| {
snap.push_bucket_value(&metric_name, labels.clone(), sequence, timestamp, value.clone()); snap.push_bucket_value(
&metric_name,
labels.clone(),
sequence,
timestamp,
value.clone(),
);
}); });
metric_sample_sequence += 1; metric_sample_sequence += 1;
} }
@@ -185,7 +245,10 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
snap.evict_excess_logs(new_max); snap.evict_excess_logs(new_max);
}); });
} }
EngineMessage::DeclareParam { request, respond_to } => { EngineMessage::DeclareParam {
request,
respond_to,
} => {
let result = write let result = write
.peek() .peek()
.map(|guard| guard.validate_param_declaration(&request)) .map(|guard| guard.validate_param_declaration(&request))
@@ -202,12 +265,16 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
let _ = respond_to.send(result); let _ = respond_to.send(result);
} }
EngineMessage::RemoveParamDeclaration { name, respond_to } => { EngineMessage::RemoveParamDeclaration { name, respond_to } => {
let existed = write let existed = write.peek().is_some_and(|guard| {
.peek() guard
.is_some_and(|guard| guard.index.contains_key(&IndexKey::ParamDeclaration { name: name.clone() })); .index
.contains_key(&IndexKey::ParamDeclaration { name: name.clone() })
});
let name_for_op = name.clone(); let name_for_op = name.clone();
write.mutate(move |snap: &mut EngineSnapshot| { write.mutate(move |snap: &mut EngineSnapshot| {
snap.index.remove(&IndexKey::ParamDeclaration { name: name_for_op.clone() }); snap.index.remove(&IndexKey::ParamDeclaration {
name: name_for_op.clone(),
});
snap.sort_helper_indexes.remove(&name_for_op); snap.sort_helper_indexes.remove(&name_for_op);
snap.param_regex_cache.remove(&name_for_op); snap.param_regex_cache.remove(&name_for_op);
}); });
@@ -215,7 +282,10 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
} }
EngineMessage::ClearParams { respond_to } => { EngineMessage::ClearParams { respond_to } => {
let mut scratch = EngineSnapshot::from_index( let mut scratch = EngineSnapshot::from_index(
write.peek().map(|guard| guard.index.clone()).unwrap_or_default(), write
.peek()
.map(|guard| guard.index.clone())
.unwrap_or_default(),
); );
let removed = scratch.clear_all_param_declarations(); let removed = scratch.clear_all_param_declarations();
let (reextracted, logs_considered) = scratch.reextract_params_from_retained_logs(); let (reextracted, logs_considered) = scratch.reextract_params_from_retained_logs();
@@ -224,9 +294,16 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
snap.reextract_params_from_retained_logs(); snap.reextract_params_from_retained_logs();
snap.sort_helper_indexes.clear(); snap.sort_helper_indexes.clear();
}); });
let _ = respond_to.send(ClearParamsResult { removed, reextracted, logs_considered }); let _ = respond_to.send(ClearParamsResult {
removed,
reextracted,
logs_considered,
});
} }
EngineMessage::DeclareMetric { request, respond_to } => { EngineMessage::DeclareMetric {
request,
respond_to,
} => {
let result = write let result = write
.peek() .peek()
.map(|guard| guard.validate_metric_request(&request).map(|_| ())) .map(|guard| guard.validate_metric_request(&request).map(|_| ()))
@@ -240,7 +317,11 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
}); });
let _ = respond_to.send(result); let _ = respond_to.send(result);
} }
EngineMessage::UpdateMetric { name, request, respond_to } => { EngineMessage::UpdateMetric {
name,
request,
respond_to,
} => {
let result = write let result = write
.peek() .peek()
.map(|guard| guard.validate_update_metric(&name, &request).map(|_| ())) .map(|guard| guard.validate_update_metric(&name, &request).map(|_| ()))
@@ -256,16 +337,21 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
let _ = respond_to.send(result); let _ = respond_to.send(result);
} }
EngineMessage::RemoveMetricDeclaration { name, respond_to } => { EngineMessage::RemoveMetricDeclaration { name, respond_to } => {
let existed = write let existed = write.peek().is_some_and(|guard| {
.peek() guard
.is_some_and(|guard| guard.index.contains_key(&IndexKey::MetricDeclaration { name: name.clone() })); .index
.contains_key(&IndexKey::MetricDeclaration { name: name.clone() })
});
let name_for_op = name.clone(); let name_for_op = name.clone();
write.mutate(move |snap: &mut EngineSnapshot| { write.mutate(move |snap: &mut EngineSnapshot| {
snap.remove_metric_declaration(&name_for_op); snap.remove_metric_declaration(&name_for_op);
}); });
let _ = respond_to.send(existed); let _ = respond_to.send(existed);
} }
EngineMessage::EnsureSortHelperIndex { param_name, respond_to } => { EngineMessage::EnsureSortHelperIndex {
param_name,
respond_to,
} => {
write.mutate(move |snap: &mut EngineSnapshot| { write.mutate(move |snap: &mut EngineSnapshot| {
snap.ensure_sort_helper_index_built(&param_name); snap.ensure_sort_helper_index_built(&param_name);
}); });
@@ -279,12 +365,16 @@ async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle, mu
} }
pub(crate) fn record_ingest_lag(samples: &mut Vec<(Timestamp, i64)>, now: Timestamp, lag_ms: i64) { pub(crate) fn record_ingest_lag(samples: &mut Vec<(Timestamp, i64)>, now: Timestamp, lag_ms: i64) {
samples.retain(|(recorded_at, _)| now.as_millis() - recorded_at.as_millis() < INGEST_LAG_WATERMARK_WINDOW_MS); samples.retain(|(recorded_at, _)| {
now.as_millis() - recorded_at.as_millis() < INGEST_LAG_WATERMARK_WINDOW_MS
});
if samples.len() < INGEST_LAG_WATERMARK_MAX_ENTRIES { if samples.len() < INGEST_LAG_WATERMARK_MAX_ENTRIES {
samples.push((now, lag_ms)); samples.push((now, lag_ms));
return; return;
} }
let Some((min_index, &(_, min_lag))) = samples.iter().enumerate().min_by_key(|(_, (_, lag))| *lag) else { let Some((min_index, &(_, min_lag))) =
samples.iter().enumerate().min_by_key(|(_, (_, lag))| *lag)
else {
return; return;
}; };
if lag_ms > min_lag { if lag_ms > min_lag {
@@ -295,7 +385,9 @@ pub(crate) fn record_ingest_lag(samples: &mut Vec<(Timestamp, i64)>, now: Timest
pub(crate) fn ingest_lag_high_watermark_ms(samples: &[(Timestamp, i64)], now: Timestamp) -> i64 { pub(crate) fn ingest_lag_high_watermark_ms(samples: &[(Timestamp, i64)], now: Timestamp) -> i64 {
samples samples
.iter() .iter()
.filter(|(recorded_at, _)| now.as_millis() - recorded_at.as_millis() < INGEST_LAG_WATERMARK_WINDOW_MS) .filter(|(recorded_at, _)| {
now.as_millis() - recorded_at.as_millis() < INGEST_LAG_WATERMARK_WINDOW_MS
})
.map(|(_, lag)| *lag) .map(|(_, lag)| *lag)
.max() .max()
.unwrap_or(0) .unwrap_or(0)
+2 -5
View File
@@ -18,6 +18,7 @@ use crate::consts::{
MAX_AUTO_DECLARED_PARAMS, MAX_NEW_AUTO_PARAMS_PER_LINE, OTTYL_RESERVED_PARAM_NAMES, MAX_AUTO_DECLARED_PARAMS, MAX_NEW_AUTO_PARAMS_PER_LINE, OTTYL_RESERVED_PARAM_NAMES,
SORT_HELPER_INDEX_PURGE_AFTER, SORT_HELPER_INDEX_PURGE_AFTER,
}; };
use crate::flavour::LogLiteralHelper;
use crate::engine::data_value::{DataValue, MetricPrimitiveKind, ParamDeclaration}; use crate::engine::data_value::{DataValue, MetricPrimitiveKind, ParamDeclaration};
use crate::engine::filter_parser::{self, CompareOp, FilterExpr, FilterValue}; use crate::engine::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
use crate::engine::index_key::IndexKey; use crate::engine::index_key::IndexKey;
@@ -123,10 +124,6 @@ impl EngineSnapshot {
} }
} }
pub(crate) fn approx_log_bytes(text: &str) -> u64 {
text.len() as u64
}
impl EngineSnapshot { impl EngineSnapshot {
pub(crate) fn from_index(index: BTreeMap<IndexKey, DataValue>) -> Self { pub(crate) fn from_index(index: BTreeMap<IndexKey, DataValue>) -> Self {
let mut snapshot = EngineSnapshot { index, ..Default::default() }; let mut snapshot = EngineSnapshot { index, ..Default::default() };
@@ -165,7 +162,7 @@ impl EngineSnapshot {
break; break;
}; };
if let Some(DataValue::LogLiteral { text }) = self.index.get(&oldest_key) { if let Some(DataValue::LogLiteral { text }) = self.index.get(&oldest_key) {
self.approx_bytes_total = self.approx_bytes_total.saturating_sub(approx_log_bytes(text)); self.approx_bytes_total = self.approx_bytes_total.saturating_sub(text.approx_log_bytes());
} }
self.index.remove(&oldest_key); self.index.remove(&oldest_key);
self.log_count -= 1; self.log_count -= 1;
+4 -3
View File
@@ -19,6 +19,7 @@ use crate::engine::snapshot::query::*;
use crate::engine::snapshot::sort::*; use crate::engine::snapshot::sort::*;
use crate::engine::snapshot::*; use crate::engine::snapshot::*;
use crate::engine::timestamp::*; use crate::engine::timestamp::*;
use crate::flavour::LogLiteralHelper;
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use super::*; use super::*;
@@ -363,7 +364,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
let mut index = EngineSnapshot::default(); let mut index = EngineSnapshot::default();
for sequence in 0..5u64 { for sequence in 0..5u64 {
let text = format!("line {sequence}"); let text = format!("line {sequence}");
index.approx_bytes_total += approx_log_bytes(&text); index.approx_bytes_total += text.approx_log_bytes();
index.index.insert( index.index.insert(
IndexKey::IngestedLog { sequence, timestamp: Timestamp::from_millis(sequence as i64) }, IndexKey::IngestedLog { sequence, timestamp: Timestamp::from_millis(sequence as i64) },
DataValue::LogLiteral { text }, DataValue::LogLiteral { text },
@@ -377,7 +378,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
let remaining: Vec<_> = index.logs_range().map(|(seq, _, _)| seq).collect(); let remaining: Vec<_> = index.logs_range().map(|(seq, _, _)| seq).collect();
assert_eq!(remaining, vec![2, 3, 4]); assert_eq!(remaining, vec![2, 3, 4]);
let expected_bytes: u64 = let expected_bytes: u64 =
remaining.iter().map(|seq| approx_log_bytes(&format!("line {seq}"))).sum(); remaining.iter().map(|seq| format!("line {seq}").approx_log_bytes()).sum();
assert_eq!(index.approx_bytes_total, expected_bytes); assert_eq!(index.approx_bytes_total, expected_bytes);
} }
@@ -413,7 +414,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
fn evict_excess_logs_is_noop_when_unlimited_or_under_cap() { fn evict_excess_logs_is_noop_when_unlimited_or_under_cap() {
let mut index = EngineSnapshot::default(); let mut index = EngineSnapshot::default();
for sequence in 0..3u64 { for sequence in 0..3u64 {
index.approx_bytes_total += approx_log_bytes("x"); index.approx_bytes_total += "x".approx_log_bytes();
index.index.insert( index.index.insert(
IndexKey::IngestedLog { sequence, timestamp: Timestamp::from_millis(sequence as i64) }, IndexKey::IngestedLog { sequence, timestamp: Timestamp::from_millis(sequence as i64) },
DataValue::LogLiteral { text: "x".to_string() }, DataValue::LogLiteral { text: "x".to_string() },
+12
View File
@@ -0,0 +1,12 @@
pub trait LogLiteralHelper {
fn approx_log_bytes(&self) -> u64;
}
impl<T> LogLiteralHelper for T
where
T: AsRef<str>,
{
fn approx_log_bytes(&self) -> u64 {
self.as_ref().len() as u64
}
}
+1
View File
@@ -3,6 +3,7 @@ mod cli;
mod config; mod config;
mod consts; mod consts;
mod engine; mod engine;
pub(crate) mod flavour;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {