From 6afca09dd1bb8a420bdaad9e252198301a4b9d72 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 16:08:56 +0200 Subject: [PATCH 01/22] Add CEL query --- ljcel/Cargo.toml | 11 ++ ljcel/src/context.rs | 122 +++++++++++++++++++++ ljcel/src/error.rs | 20 ++++ ljcel/src/expression.rs | 97 +++++++++++++++++ ljcel/src/lib.rs | 232 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 482 insertions(+) create mode 100644 ljcel/Cargo.toml create mode 100644 ljcel/src/context.rs create mode 100644 ljcel/src/error.rs create mode 100644 ljcel/src/expression.rs create mode 100644 ljcel/src/lib.rs diff --git a/ljcel/Cargo.toml b/ljcel/Cargo.toml new file mode 100644 index 0000000..eddbab7 --- /dev/null +++ b/ljcel/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ljcel" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +cel = { version = "0.14", features = ["json"] } +opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "logs"] } +prost = "0.14" +serde_json = "1" diff --git a/ljcel/src/context.rs b/ljcel/src/context.rs new file mode 100644 index 0000000..fe5d650 --- /dev/null +++ b/ljcel/src/context.rs @@ -0,0 +1,122 @@ +use opentelemetry_proto::tonic::common::v1::any_value::Value; +use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue}; +use serde_json::{Map as JsonMap, Value as JsonValue}; + +use crate::error::CelError; + +pub(crate) struct LogRecordContext { + pub body: String, + pub severity_text: String, + pub severity_number: i64, + pub event_name: String, + pub service_name: String, + pub scope_name: String, + pub time_unix_nano: i64, + pub observed_time_unix_nano: i64, + pub trace_id: String, + pub span_id: String, + pub flags: i32, + pub resource: JsonMap, + pub scope: JsonMap, + pub attributes: JsonMap, +} + +impl LogRecordContext { + pub(crate) fn to_cel_context(&self) -> Result, CelError> { + let mut ctx = cel::Context::default(); + ctx.add_variable("body", self.body.clone()) + .map_err(|e| CelError::Compile(format!("body: {e}")))?; + ctx.add_variable("severity_text", self.severity_text.clone()) + .map_err(|e| CelError::Compile(format!("severity_text: {e}")))?; + ctx.add_variable("severity_number", self.severity_number) + .map_err(|e| CelError::Compile(format!("severity_number: {e}")))?; + if !self.event_name.is_empty() { + ctx.add_variable("event_name", self.event_name.clone()) + .map_err(|e| CelError::Compile(format!("event_name: {e}")))?; + } + if !self.service_name.is_empty() { + ctx.add_variable("service_name", self.service_name.clone()) + .map_err(|e| CelError::Compile(format!("service_name: {e}")))?; + } + if !self.scope_name.is_empty() { + ctx.add_variable("scope_name", self.scope_name.clone()) + .map_err(|e| CelError::Compile(format!("scope_name: {e}")))?; + } + ctx.add_variable("time_unix_nano", self.time_unix_nano) + .map_err(|e| CelError::Compile(format!("time_unix_nano: {e}")))?; + if self.observed_time_unix_nano > 0 { + ctx.add_variable("observed_time_unix_nano", self.observed_time_unix_nano) + .map_err(|e| CelError::Compile(format!("observed_time_unix_nano: {e}")))?; + } + if !self.trace_id.is_empty() { + ctx.add_variable("trace_id", self.trace_id.clone()) + .map_err(|e| CelError::Compile(format!("trace_id: {e}")))?; + } + if !self.span_id.is_empty() { + ctx.add_variable("span_id", self.span_id.clone()) + .map_err(|e| CelError::Compile(format!("span_id: {e}")))?; + } + if self.flags != 0 { + ctx.add_variable("flags", self.flags) + .map_err(|e| CelError::Compile(format!("flags: {e}")))?; + } + ctx.add_variable("resource", self.resource.clone()) + .map_err(|e| CelError::Compile(format!("resource: {e}")))?; + ctx.add_variable("scope", self.scope.clone()) + .map_err(|e| CelError::Compile(format!("scope: {e}")))?; + ctx.add_variable("attributes", self.attributes.clone()) + .map_err(|e| CelError::Compile(format!("attributes: {e}")))?; + Ok(ctx) + } +} + +pub(crate) fn key_values_to_map(attrs: &[KeyValue]) -> JsonMap { + let mut map = JsonMap::new(); + for attr in attrs { + if map.contains_key(&attr.key) { + continue; + } + if let Some(value) = attr.value.as_ref() + && let Some(json) = any_value_to_json(value) + { + map.insert(attr.key.clone(), json); + } + } + map +} + +pub(crate) fn any_value_to_json(value: &AnyValue) -> Option { + match &value.value { + Some(Value::StringValue(s)) => Some(JsonValue::String(s.clone())), + Some(Value::BoolValue(b)) => Some(JsonValue::Bool(*b)), + Some(Value::IntValue(n)) => Some(JsonValue::Number((*n).into())), + Some(Value::DoubleValue(d)) => serde_json::Number::from_f64(*d).map(JsonValue::Number), + Some(Value::BytesValue(b)) => Some(JsonValue::String(format!("<{} bytes>", b.len()))), + Some(Value::ArrayValue(a)) => { + Some(JsonValue::Array(a.values.iter().filter_map(any_value_to_json).collect())) + } + Some(Value::KvlistValue(k)) => { + let mut obj = JsonMap::new(); + for item in &k.values { + if let Some(inner) = item.value.as_ref().and_then(any_value_to_json) { + obj.insert(item.key.clone(), inner); + } + } + Some(JsonValue::Object(obj)) + } + None => None, + } +} + +pub(crate) fn hex_encode(bytes: &[u8]) -> String { + if bytes.is_empty() { + return String::new(); + } + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for &byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} diff --git a/ljcel/src/error.rs b/ljcel/src/error.rs new file mode 100644 index 0000000..8d8998d --- /dev/null +++ b/ljcel/src/error.rs @@ -0,0 +1,20 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum CelError { + Compile(String), + Decode(String), + NotBoolean(String), +} + +impl Display for CelError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Compile(msg) => write!(f, "CEL compilation failed: {msg}"), + Self::Decode(msg) => write!(f, "OTLP decode failed: {msg}"), + Self::NotBoolean(val) => write!(f, "CEL result was not boolean: {val}"), + } + } +} + +impl std::error::Error for CelError {} diff --git a/ljcel/src/expression.rs b/ljcel/src/expression.rs new file mode 100644 index 0000000..b637f94 --- /dev/null +++ b/ljcel/src/expression.rs @@ -0,0 +1,97 @@ +use std::sync::Arc; + +use cel::Program; +use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; +use prost::Message; + +use crate::context::{LogRecordContext, hex_encode, key_values_to_map}; +use crate::error::CelError; + +#[derive(Debug, Clone)] +pub struct CelExpression { + program: Arc, + source: String, +} + +impl CelExpression { + pub fn compile(expr: &str) -> Result { + let program = Program::compile(expr).map_err(|e| CelError::Compile(e.to_string()))?; + Ok(Self { program: Arc::new(program), source: expr.to_string() }) + } + + pub fn source(&self) -> &str { + &self.source + } + + pub fn matches_logs_payload(&self, payload: &[u8]) -> Result { + let batch = ExportLogsServiceRequest::decode(payload).map_err(|e| CelError::Decode(e.to_string()))?; + + for rl in &batch.resource_logs { + let resource_attrs = + key_values_to_map(rl.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); + + let service_name = rl + .resource + .as_ref() + .and_then(|r| r.attributes.iter().find(|a| a.key == "service.name")) + .and_then(|a| a.value.as_ref()) + .and_then(|v| match &v.value { + Some(opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(s)) => Some(s.clone()), + _ => None, + }) + .unwrap_or_default(); + + for sl in &rl.scope_logs { + let scope_name = sl.scope.as_ref().map(|s| s.name.clone()).unwrap_or_default(); + let scope_attrs = key_values_to_map( + sl.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[]), + ); + + for lr in &sl.log_records { + let body = lr + .body + .as_ref() + .and_then(|v| match &v.value { + Some(opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(s)) => { + Some(s.clone()) + } + _ => None, + }) + .unwrap_or_default(); + + let ctx = LogRecordContext { + body, + severity_text: lr.severity_text.clone(), + severity_number: lr.severity_number as i64, + event_name: lr.event_name.clone(), + service_name: service_name.clone(), + scope_name: scope_name.clone(), + time_unix_nano: lr.time_unix_nano as i64, + observed_time_unix_nano: lr.observed_time_unix_nano as i64, + trace_id: hex_encode(&lr.trace_id), + span_id: hex_encode(&lr.span_id), + flags: lr.flags as i32, + resource: resource_attrs.clone(), + scope: scope_attrs.clone(), + attributes: key_values_to_map(&lr.attributes), + }; + + if self.eval(&ctx)? { + return Ok(true); + } + } + } + } + Ok(false) + } + + fn eval(&self, ctx: &LogRecordContext) -> Result { + let cel_ctx = ctx.to_cel_context()?; + match self.program.execute(&cel_ctx) { + Ok(cel::Value::Bool(true)) => Ok(true), + Ok(cel::Value::Bool(false)) => Ok(false), + Ok(other) => Err(CelError::NotBoolean(format!("{other:?}"))), + Err(e) => Err(CelError::Compile(e.to_string())), + } + } +} diff --git a/ljcel/src/lib.rs b/ljcel/src/lib.rs new file mode 100644 index 0000000..0dd9f7a --- /dev/null +++ b/ljcel/src/lib.rs @@ -0,0 +1,232 @@ +mod context; +mod error; +mod expression; + +pub use expression::CelExpression; +pub use error::CelError; + +#[cfg(test)] +mod tests { + use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; + use opentelemetry_proto::tonic::common::v1::any_value::Value; + use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue}; + use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs}; + use opentelemetry_proto::tonic::resource::v1::Resource; + use prost::Message; + + use crate::CelExpression; + + fn make_log_payload(records: Vec) -> Vec { + make_log_payload_with_context(records, &[], &[], "test-svc") + } + + fn make_log_payload_with_context( + records: Vec, + resource_attrs: &[KeyValue], + scope_attrs: &[KeyValue], + service_name: &str, + ) -> Vec { + let mut res_attrs = vec![KeyValue { + key: "service.name".to_string(), + value: Some(AnyValue { value: Some(Value::StringValue(service_name.to_string())) }), + }]; + res_attrs.extend_from_slice(resource_attrs); + + let batch = ExportLogsServiceRequest { + resource_logs: vec![ResourceLogs { + resource: Some(Resource { attributes: res_attrs, dropped_attributes_count: 0, entity_refs: Vec::new() }), + scope_logs: vec![ScopeLogs { + scope: Some(InstrumentationScope { + name: "test-scope".to_string(), + version: "1.0.0".to_string(), + attributes: scope_attrs.to_vec(), + dropped_attributes_count: 0, + }), + log_records: records, + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + batch.encode_to_vec() + } + + fn string_attr(key: &str, value: &str) -> KeyValue { + KeyValue { + key: key.to_string(), + value: Some(AnyValue { value: Some(Value::StringValue(value.to_string())) }), + } + } + + fn int_attr(key: &str, value: i64) -> KeyValue { + KeyValue { + key: key.to_string(), + value: Some(AnyValue { value: Some(Value::IntValue(value)) }), + } + } + + fn log_record(body: &str, severity_number: i32, severity_text: &str) -> LogRecord { + LogRecord { + body: Some(AnyValue { value: Some(Value::StringValue(body.to_string())) }), + severity_number, + severity_text: severity_text.to_string(), + ..Default::default() + } + } + + fn log_record_with_attrs(body: &str, severity_number: i32, severity_text: &str, attrs: &[KeyValue]) -> LogRecord { + LogRecord { + body: Some(AnyValue { value: Some(Value::StringValue(body.to_string())) }), + severity_number, + severity_text: severity_text.to_string(), + attributes: attrs.to_vec(), + ..Default::default() + } + } + + #[test] + fn compile_simple_comparison() { + let expr = CelExpression::compile("severity_number >= 13").unwrap(); + assert_eq!(expr.source(), "severity_number >= 13"); + } + + #[test] + fn compile_with_body_contains() { + let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); + assert_eq!(expr.source(), "body.contains(\"timeout\")"); + } + + #[test] + fn compile_invalid_expression_returns_error() { + let result = CelExpression::compile("this is not valid CEL @#$%"); + assert!(result.is_err()); + } + + #[test] + fn severity_number_match() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 17, "ERROR")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn severity_number_no_match() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn severity_text_match() { + let expr = CelExpression::compile("severity_text == \"ERROR\"").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 17, "ERROR")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn service_name_match() { + let expr = CelExpression::compile("service_name == \"test-svc\"").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn service_name_no_match() { + let expr = CelExpression::compile("service_name == \"other-service\"").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn body_contains_match() { + let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); + let payload = make_log_payload(vec![log_record("connection timeout after 30s", 17, "ERROR")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn body_contains_no_match() { + let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); + let payload = make_log_payload(vec![log_record("connection established", 9, "INFO")]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn record_attribute_string_match() { + let expr = CelExpression::compile("attributes[\"custom.thread\"] == \"worker-1\"").unwrap(); + let payload = make_log_payload(vec![log_record_with_attrs( + "msg", 13, "WARN", + &[string_attr("custom.thread", "worker-1")], + )]); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn record_attribute_int_match() { + let expr = CelExpression::compile("attributes[\"custom.count\"] == 42").unwrap(); + let payload = make_log_payload(vec![log_record_with_attrs( + "msg", 13, "WARN", + &[int_attr("custom.count", 42)], + )]); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn resource_attribute_match() { + let expr = CelExpression::compile("resource[\"custom.label\"].contains(\"processor-a\")").unwrap(); + let payload = make_log_payload_with_context( + vec![log_record("msg", 9, "INFO")], + &[string_attr("custom.label", "processor-a")], + &[], + "test-svc", + ); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn scope_attribute_match() { + let expr = CelExpression::compile("scope[\"custom.channel\"] >= 3").unwrap(); + let payload = make_log_payload_with_context( + vec![log_record("msg", 9, "INFO")], + &[], + &[int_attr("custom.channel", 4)], + "test-svc", + ); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn combined_conditions_and() { + let expr = CelExpression::compile("severity_number >= 17 && service_name == \"test-svc\"").unwrap(); + let payload = make_log_payload(vec![log_record("fatal error", 17, "FATAL")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn combined_conditions_and_fails_on_one() { + let expr = CelExpression::compile("severity_number >= 17 && service_name == \"other-service\"").unwrap(); + let payload = make_log_payload(vec![log_record("fatal error", 17, "FATAL")]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn any_record_in_batch_matches() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![ + log_record("info msg", 9, "INFO"), + log_record("warn msg", 13, "WARN"), + log_record("error msg", 17, "ERROR"), + ]); + assert!(expr.matches_logs_payload(&payload).unwrap()); + } + + #[test] + fn no_record_in_batch_matches() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![ + log_record("info msg", 9, "INFO"), + log_record("warn msg", 13, "WARN"), + ]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); + } +} From 6c8859a74fcd4221d4dd2d594c5ccb62edd3638d Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 16:15:35 +0200 Subject: [PATCH 02/22] Move UT away from the source --- Cargo.lock | 93 +++++++++++++- Cargo.toml | 2 + ljcel/src/lib.rs | 226 +---------------------------------- ljcel/tests/unit/ljcel_ut.rs | 210 ++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 226 deletions(-) create mode 100644 ljcel/tests/unit/ljcel_ut.rs diff --git a/Cargo.lock b/Cargo.lock index e5d701a..986f52c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "antlr4rust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093d520274bfff7278d776f7ea12981a0a0a6f96db90964658e0f38fc6e9a6a6" +dependencies = [ + "better_any", + "bit-set 0.8.0", + "byteorder", + "lazy_static", + "murmur3", + "once_cell", + "parking_lot", + "typed-arena", + "uuid", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -288,13 +305,28 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "better_any" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4372b9543397a4b86050cc5e7ee36953edf4bac9518e8a774c2da694977fb6e4" + [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -303,6 +335,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -369,6 +407,24 @@ dependencies = [ "shlex", ] +[[package]] +name = "cel" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed39583e427bf41d93c28c7f27c943a93bcb8697220ff3575fd53a9e13f3814" +dependencies = [ + "antlr4rust", + "base64", + "chrono", + "lazy_static", + "nom", + "pastey", + "regex", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -389,6 +445,7 @@ checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", + "serde", "windows-link", ] @@ -765,7 +822,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" dependencies = [ - "bit-set", + "bit-set 0.5.3", "regex", ] @@ -1369,6 +1426,16 @@ version = "0.9.0" name = "lj-syslog-ingest" version = "0.9.0" +[[package]] +name = "ljcel" +version = "0.9.0" +dependencies = [ + "cel", + "opentelemetry-proto", + "prost", + "serde_json", +] + [[package]] name = "ljd" version = "0.9.0" @@ -1408,6 +1475,7 @@ dependencies = [ "crossterm 0.28.1", "libloading", "liblogjet", + "ljcel", "logjet", "opentelemetry-proto", "prost", @@ -1547,6 +1615,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "murmur3" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a198f9589efc03f544388dfc4a19fe8af4323662b62f598b8dcfdac62c14771c" +dependencies = [ + "byteorder", +] + [[package]] name = "nix" version = "0.29.0" @@ -1784,6 +1861,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pem" version = "3.0.6" @@ -2899,6 +2982,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.20.0" diff --git a/Cargo.toml b/Cargo.toml index 3fc3183..08eb182 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "demo", "ljx", "liblogjet", + "ljcel", "plugins/syslog-ingest", "plugins/logcat-ingest", "plugins/stress-ingest", @@ -28,6 +29,7 @@ default-members = [ "demo", "ljx", "liblogjet", + "ljcel", "plugins/syslog-ingest", "plugins/logcat-ingest", "plugins/stress-ingest", diff --git a/ljcel/src/lib.rs b/ljcel/src/lib.rs index 0dd9f7a..1dc05a3 100644 --- a/ljcel/src/lib.rs +++ b/ljcel/src/lib.rs @@ -6,227 +6,5 @@ pub use expression::CelExpression; pub use error::CelError; #[cfg(test)] -mod tests { - use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; - use opentelemetry_proto::tonic::common::v1::any_value::Value; - use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue}; - use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs}; - use opentelemetry_proto::tonic::resource::v1::Resource; - use prost::Message; - - use crate::CelExpression; - - fn make_log_payload(records: Vec) -> Vec { - make_log_payload_with_context(records, &[], &[], "test-svc") - } - - fn make_log_payload_with_context( - records: Vec, - resource_attrs: &[KeyValue], - scope_attrs: &[KeyValue], - service_name: &str, - ) -> Vec { - let mut res_attrs = vec![KeyValue { - key: "service.name".to_string(), - value: Some(AnyValue { value: Some(Value::StringValue(service_name.to_string())) }), - }]; - res_attrs.extend_from_slice(resource_attrs); - - let batch = ExportLogsServiceRequest { - resource_logs: vec![ResourceLogs { - resource: Some(Resource { attributes: res_attrs, dropped_attributes_count: 0, entity_refs: Vec::new() }), - scope_logs: vec![ScopeLogs { - scope: Some(InstrumentationScope { - name: "test-scope".to_string(), - version: "1.0.0".to_string(), - attributes: scope_attrs.to_vec(), - dropped_attributes_count: 0, - }), - log_records: records, - schema_url: String::new(), - }], - schema_url: String::new(), - }], - }; - batch.encode_to_vec() - } - - fn string_attr(key: &str, value: &str) -> KeyValue { - KeyValue { - key: key.to_string(), - value: Some(AnyValue { value: Some(Value::StringValue(value.to_string())) }), - } - } - - fn int_attr(key: &str, value: i64) -> KeyValue { - KeyValue { - key: key.to_string(), - value: Some(AnyValue { value: Some(Value::IntValue(value)) }), - } - } - - fn log_record(body: &str, severity_number: i32, severity_text: &str) -> LogRecord { - LogRecord { - body: Some(AnyValue { value: Some(Value::StringValue(body.to_string())) }), - severity_number, - severity_text: severity_text.to_string(), - ..Default::default() - } - } - - fn log_record_with_attrs(body: &str, severity_number: i32, severity_text: &str, attrs: &[KeyValue]) -> LogRecord { - LogRecord { - body: Some(AnyValue { value: Some(Value::StringValue(body.to_string())) }), - severity_number, - severity_text: severity_text.to_string(), - attributes: attrs.to_vec(), - ..Default::default() - } - } - - #[test] - fn compile_simple_comparison() { - let expr = CelExpression::compile("severity_number >= 13").unwrap(); - assert_eq!(expr.source(), "severity_number >= 13"); - } - - #[test] - fn compile_with_body_contains() { - let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); - assert_eq!(expr.source(), "body.contains(\"timeout\")"); - } - - #[test] - fn compile_invalid_expression_returns_error() { - let result = CelExpression::compile("this is not valid CEL @#$%"); - assert!(result.is_err()); - } - - #[test] - fn severity_number_match() { - let expr = CelExpression::compile("severity_number == 17").unwrap(); - let payload = make_log_payload(vec![log_record("msg", 17, "ERROR")]); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn severity_number_no_match() { - let expr = CelExpression::compile("severity_number == 17").unwrap(); - let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); - assert!(!expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn severity_text_match() { - let expr = CelExpression::compile("severity_text == \"ERROR\"").unwrap(); - let payload = make_log_payload(vec![log_record("msg", 17, "ERROR")]); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn service_name_match() { - let expr = CelExpression::compile("service_name == \"test-svc\"").unwrap(); - let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn service_name_no_match() { - let expr = CelExpression::compile("service_name == \"other-service\"").unwrap(); - let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); - assert!(!expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn body_contains_match() { - let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); - let payload = make_log_payload(vec![log_record("connection timeout after 30s", 17, "ERROR")]); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn body_contains_no_match() { - let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); - let payload = make_log_payload(vec![log_record("connection established", 9, "INFO")]); - assert!(!expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn record_attribute_string_match() { - let expr = CelExpression::compile("attributes[\"custom.thread\"] == \"worker-1\"").unwrap(); - let payload = make_log_payload(vec![log_record_with_attrs( - "msg", 13, "WARN", - &[string_attr("custom.thread", "worker-1")], - )]); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn record_attribute_int_match() { - let expr = CelExpression::compile("attributes[\"custom.count\"] == 42").unwrap(); - let payload = make_log_payload(vec![log_record_with_attrs( - "msg", 13, "WARN", - &[int_attr("custom.count", 42)], - )]); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn resource_attribute_match() { - let expr = CelExpression::compile("resource[\"custom.label\"].contains(\"processor-a\")").unwrap(); - let payload = make_log_payload_with_context( - vec![log_record("msg", 9, "INFO")], - &[string_attr("custom.label", "processor-a")], - &[], - "test-svc", - ); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn scope_attribute_match() { - let expr = CelExpression::compile("scope[\"custom.channel\"] >= 3").unwrap(); - let payload = make_log_payload_with_context( - vec![log_record("msg", 9, "INFO")], - &[], - &[int_attr("custom.channel", 4)], - "test-svc", - ); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn combined_conditions_and() { - let expr = CelExpression::compile("severity_number >= 17 && service_name == \"test-svc\"").unwrap(); - let payload = make_log_payload(vec![log_record("fatal error", 17, "FATAL")]); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn combined_conditions_and_fails_on_one() { - let expr = CelExpression::compile("severity_number >= 17 && service_name == \"other-service\"").unwrap(); - let payload = make_log_payload(vec![log_record("fatal error", 17, "FATAL")]); - assert!(!expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn any_record_in_batch_matches() { - let expr = CelExpression::compile("severity_number == 17").unwrap(); - let payload = make_log_payload(vec![ - log_record("info msg", 9, "INFO"), - log_record("warn msg", 13, "WARN"), - log_record("error msg", 17, "ERROR"), - ]); - assert!(expr.matches_logs_payload(&payload).unwrap()); - } - - #[test] - fn no_record_in_batch_matches() { - let expr = CelExpression::compile("severity_number == 17").unwrap(); - let payload = make_log_payload(vec![ - log_record("info msg", 9, "INFO"), - log_record("warn msg", 13, "WARN"), - ]); - assert!(!expr.matches_logs_payload(&payload).unwrap()); - } -} +#[path = "../tests/unit/ljcel_ut.rs"] +mod ljcel_ut; diff --git a/ljcel/tests/unit/ljcel_ut.rs b/ljcel/tests/unit/ljcel_ut.rs new file mode 100644 index 0000000..fc978b7 --- /dev/null +++ b/ljcel/tests/unit/ljcel_ut.rs @@ -0,0 +1,210 @@ +use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; +use opentelemetry_proto::tonic::common::v1::any_value::Value; +use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue}; +use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs}; +use opentelemetry_proto::tonic::resource::v1::Resource; +use prost::Message; + +use crate::CelExpression; + +fn make_log_payload(records: Vec) -> Vec { + make_log_payload_with_context(records, &[], &[], "test-svc") +} + +fn make_log_payload_with_context( + records: Vec, + resource_attrs: &[KeyValue], + scope_attrs: &[KeyValue], + service_name: &str, +) -> Vec { + let mut res_attrs = vec![KeyValue { + key: "service.name".to_string(), + value: Some(AnyValue { value: Some(Value::StringValue(service_name.to_string())) }), + }]; + res_attrs.extend_from_slice(resource_attrs); + + let batch = ExportLogsServiceRequest { + resource_logs: vec![ResourceLogs { + resource: Some(Resource { attributes: res_attrs, dropped_attributes_count: 0, entity_refs: Vec::new() }), + scope_logs: vec![ScopeLogs { + scope: Some(InstrumentationScope { + name: "test-scope".to_string(), + version: "1.0.0".to_string(), + attributes: scope_attrs.to_vec(), + dropped_attributes_count: 0, + }), + log_records: records, + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + batch.encode_to_vec() +} + +fn string_attr(key: &str, value: &str) -> KeyValue { + KeyValue { key: key.to_string(), value: Some(AnyValue { value: Some(Value::StringValue(value.to_string())) }) } +} + +fn int_attr(key: &str, value: i64) -> KeyValue { + KeyValue { key: key.to_string(), value: Some(AnyValue { value: Some(Value::IntValue(value)) }) } +} + +fn log_record(body: &str, severity_number: i32, severity_text: &str) -> LogRecord { + LogRecord { + body: Some(AnyValue { value: Some(Value::StringValue(body.to_string())) }), + severity_number, + severity_text: severity_text.to_string(), + ..Default::default() + } +} + +fn log_record_with_attrs(body: &str, severity_number: i32, severity_text: &str, attrs: &[KeyValue]) -> LogRecord { + LogRecord { + body: Some(AnyValue { value: Some(Value::StringValue(body.to_string())) }), + severity_number, + severity_text: severity_text.to_string(), + attributes: attrs.to_vec(), + ..Default::default() + } +} + +#[test] +fn compile_simple_comparison() { + let expr = CelExpression::compile("severity_number >= 13").unwrap(); + assert_eq!(expr.source(), "severity_number >= 13"); +} + +#[test] +fn compile_with_body_contains() { + let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); + assert_eq!(expr.source(), "body.contains(\"timeout\")"); +} + +#[test] +fn compile_invalid_expression_returns_error() { + let result = CelExpression::compile("this is not valid CEL @#$%"); + assert!(result.is_err()); +} + +#[test] +fn severity_number_match() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 17, "ERROR")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn severity_number_no_match() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn severity_text_match() { + let expr = CelExpression::compile("severity_text == \"ERROR\"").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 17, "ERROR")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn service_name_match() { + let expr = CelExpression::compile("service_name == \"test-svc\"").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn service_name_no_match() { + let expr = CelExpression::compile("service_name == \"other-service\"").unwrap(); + let payload = make_log_payload(vec![log_record("msg", 9, "INFO")]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn body_contains_match() { + let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); + let payload = make_log_payload(vec![log_record("connection timeout after 30s", 17, "ERROR")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn body_contains_no_match() { + let expr = CelExpression::compile("body.contains(\"timeout\")").unwrap(); + let payload = make_log_payload(vec![log_record("connection established", 9, "INFO")]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn record_attribute_string_match() { + let expr = CelExpression::compile("attributes[\"custom.thread\"] == \"worker-1\"").unwrap(); + let payload = make_log_payload(vec![log_record_with_attrs("msg", 13, "WARN", &[string_attr("custom.thread", "worker-1")])]); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn record_attribute_int_match() { + let expr = CelExpression::compile("attributes[\"custom.count\"] == 42").unwrap(); + let payload = make_log_payload(vec![log_record_with_attrs("msg", 13, "WARN", &[int_attr("custom.count", 42)])]); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn resource_attribute_match() { + let expr = CelExpression::compile("resource[\"custom.label\"].contains(\"processor-a\")").unwrap(); + let payload = make_log_payload_with_context( + vec![log_record("msg", 9, "INFO")], + &[string_attr("custom.label", "processor-a")], + &[], + "test-svc", + ); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn scope_attribute_match() { + let expr = CelExpression::compile("scope[\"custom.channel\"] >= 3").unwrap(); + let payload = make_log_payload_with_context( + vec![log_record("msg", 9, "INFO")], + &[], + &[int_attr("custom.channel", 4)], + "test-svc", + ); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn combined_conditions_and() { + let expr = CelExpression::compile("severity_number >= 17 && service_name == \"test-svc\"").unwrap(); + let payload = make_log_payload(vec![log_record("fatal error", 17, "FATAL")]); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn combined_conditions_and_fails_on_one() { + let expr = CelExpression::compile("severity_number >= 17 && service_name == \"other-service\"").unwrap(); + let payload = make_log_payload(vec![log_record("fatal error", 17, "FATAL")]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn any_record_in_batch_matches() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![ + log_record("info msg", 9, "INFO"), + log_record("warn msg", 13, "WARN"), + log_record("error msg", 17, "ERROR"), + ]); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + +#[test] +fn no_record_in_batch_matches() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![ + log_record("info msg", 9, "INFO"), + log_record("warn msg", 13, "WARN"), + ]); + assert!(!expr.matches_logs_payload(&payload).unwrap()); +} From 3ae016c6f010463884160a000ec2def8b4eeffd9 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 16:15:49 +0200 Subject: [PATCH 03/22] Integrate CEL search with the TUI --- ljx/Cargo.toml | 1 + ljx/src/commands/view/app.rs | 16 +++++++++++++--- ljx/src/commands/view/render.rs | 1 + ljx/src/predicate.rs | 19 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/ljx/Cargo.toml b/ljx/Cargo.toml index facef30..6e255b5 100644 --- a/ljx/Cargo.toml +++ b/ljx/Cargo.toml @@ -11,6 +11,7 @@ colored = "3" crossterm = "0.28" libloading = "0.8" liblogjet = { path = "../liblogjet" } +ljcel = { path = "../ljcel" } logjet = { path = ".." } opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "logs", "metrics", "trace"] } prost = "0.14" diff --git a/ljx/src/commands/view/app.rs b/ljx/src/commands/view/app.rs index cf99bb0..59b1800 100644 --- a/ljx/src/commands/view/app.rs +++ b/ljx/src/commands/view/app.rs @@ -58,7 +58,7 @@ impl ViewApp { exporters, export_formats, focus: Focus::Search, - filter_mode: FilterMode::Strings, + filter_mode: FilterMode::Cel, query_input: String::new(), applied_query: String::new(), status: "Type a filter and press Enter to scan matching records".to_string(), @@ -187,7 +187,8 @@ impl ViewApp { fn handle_search_key(&mut self, key: KeyEvent) -> Result { match key.code { KeyCode::Tab => self.focus = Focus::List, - KeyCode::Up | KeyCode::Down => self.cycle_filter_mode(), + KeyCode::Up => self.cycle_filter_mode_fwd(), + KeyCode::Down => self.cycle_filter_mode_rev(), KeyCode::Esc => { self.query_input.clear(); self.apply_filter()?; @@ -405,10 +406,19 @@ impl ViewApp { Ok(()) } - fn cycle_filter_mode(&mut self) { + fn cycle_filter_mode_fwd(&mut self) { self.filter_mode = match self.filter_mode { + FilterMode::Cel => FilterMode::Strings, FilterMode::Strings => FilterMode::Regex, + FilterMode::Regex => FilterMode::Cel, + }; + } + + fn cycle_filter_mode_rev(&mut self) { + self.filter_mode = match self.filter_mode { + FilterMode::Cel => FilterMode::Regex, FilterMode::Regex => FilterMode::Strings, + FilterMode::Strings => FilterMode::Cel, }; } diff --git a/ljx/src/commands/view/render.rs b/ljx/src/commands/view/render.rs index d144534..a1489b8 100644 --- a/ljx/src/commands/view/render.rs +++ b/ljx/src/commands/view/render.rs @@ -69,6 +69,7 @@ impl ViewApp { } let title = match self.filter_mode { + crate::predicate::FilterMode::Cel => "Filter (CEL): ", crate::predicate::FilterMode::Strings => "Filter (strings): ", crate::predicate::FilterMode::Regex => "Filter (regex): ", }; diff --git a/ljx/src/predicate.rs b/ljx/src/predicate.rs index ba071fa..fd72c5d 100644 --- a/ljx/src/predicate.rs +++ b/ljx/src/predicate.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use clap::{Args, CommandFactory, FromArgMatches, Parser, ValueEnum}; +use ljcel::CelExpression; use logjet::{OwnedRecord, RecordType}; use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; use prost::Message; @@ -10,6 +11,7 @@ use crate::error::{Error, Result}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FilterMode { + Cel, Strings, Regex, } @@ -91,6 +93,9 @@ pub struct PredicateArgs { #[arg(short = 'i', long = "ignore-case", help = "Apply case-insensitive matching to all payload matchers")] pub ignore_case: bool, + + #[arg(long = "cel", value_name = "EXPR", help = "CEL expression evaluated against decoded OTel attributes; repeat for AND semantics")] + pub cel: Vec, } #[derive(Debug, Clone)] @@ -101,6 +106,7 @@ pub struct RecordPredicate { ts_min: Option, ts_max: Option, payload_matchers: Vec, + cel_queries: Vec, pub field_filter: FieldFilter, } @@ -118,6 +124,7 @@ impl PredicateArgs { || self.ts_max.is_some() || !self.grep.is_empty() || !self.fixed_string.is_empty() + || !self.cel.is_empty() || self.ignore_case } } @@ -145,6 +152,11 @@ impl PredicateArgs { for text in self.fixed_string { payload_matchers.push(PayloadMatcher::new(&text, true, self.ignore_case)?); } + let cel_queries = self + .cel + .iter() + .map(|e| CelExpression::compile(e).map_err(|err| Error::Usage(format!("invalid CEL expression: {err}")))) + .collect::>>()?; Ok(RecordPredicate { record_type: self.record_type.map(Into::into), @@ -153,6 +165,7 @@ impl PredicateArgs { ts_min: self.ts_min, ts_max: self.ts_max, payload_matchers, + cel_queries, field_filter: FieldFilter::default(), }) } @@ -173,6 +186,7 @@ pub fn parse_filter_query(query: &str, mode: FilterMode) -> Result PredicateArgs { cel: vec![trimmed.to_string()], ..PredicateArgs::default() }.build(), FilterMode::Strings => PredicateArgs { fixed_string: vec![trimmed.to_string()], ..PredicateArgs::default() }.build(), FilterMode::Regex => PredicateArgs { grep: vec![trimmed.to_string()], ..PredicateArgs::default() }.build(), }; @@ -221,6 +235,11 @@ impl RecordPredicate { return false; } } + for cel in &self.cel_queries { + if !cel.matches_logs_payload(&record.payload).unwrap_or(false) { + return false; + } + } if !self.field_filter.matches_payload(&record.payload) { return false; } From 0cc84967b4d933cd7a9094e42220a17084d1fcd1 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 16:15:59 +0200 Subject: [PATCH 04/22] Add UT for the predicates --- ljx/tests/unit/predicate_ut.rs | 109 +++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/ljx/tests/unit/predicate_ut.rs b/ljx/tests/unit/predicate_ut.rs index 44f7568..f554e63 100644 --- a/ljx/tests/unit/predicate_ut.rs +++ b/ljx/tests/unit/predicate_ut.rs @@ -5,6 +5,50 @@ fn sample_record(payload: &[u8]) -> OwnedRecord { OwnedRecord { record_type: RecordType::Logs, seq: 42, ts_unix_ns: 1_700_000_000, payload: payload.to_vec() } } +use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; +use opentelemetry_proto::tonic::common::v1::any_value::Value; +use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue}; +use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs}; +use opentelemetry_proto::tonic::resource::v1::Resource; +use prost::Message; + +fn make_cel_test_payload(body: &str, severity_number: i32, severity_text: &str, service_name: &str) -> Vec { + let batch = ExportLogsServiceRequest { + resource_logs: vec![ResourceLogs { + resource: Some(Resource { + attributes: vec![KeyValue { + key: "service.name".to_string(), + value: Some(AnyValue { value: Some(Value::StringValue(service_name.to_string())) }), + }], + dropped_attributes_count: 0, + entity_refs: Vec::new(), + }), + scope_logs: vec![ScopeLogs { + scope: Some(InstrumentationScope { + name: "test".to_string(), + version: "1.0.0".to_string(), + attributes: vec![], + dropped_attributes_count: 0, + }), + log_records: vec![LogRecord { + body: Some(AnyValue { value: Some(Value::StringValue(body.to_string())) }), + severity_number, + severity_text: severity_text.to_string(), + ..Default::default() + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + batch.encode_to_vec() +} + +fn cel_sample(body: &str, severity_number: i32, severity_text: &str) -> OwnedRecord { + let payload = make_cel_test_payload(body, severity_number, severity_text, "test-svc"); + sample_record(&payload) +} + #[test] fn fixed_string_match_is_literal() { let predicate = PredicateArgs { fixed_string: vec!["java.crap.failed".to_string()], ..PredicateArgs::default() }.build().unwrap(); @@ -104,3 +148,68 @@ fn parse_filter_query_uses_regex_mode_for_bare_text() { assert!(predicate.matches(&sample_record(b"rebooted node"))); assert!(!predicate.matches(&sample_record(b"stopped node"))); } + +#[test] +fn cel_severity_number_match() { + let predicate = PredicateArgs { cel: vec!["severity_number == 17".to_string()], ..PredicateArgs::default() }.build().unwrap(); + assert!(predicate.matches(&cel_sample("fatal error", 17, "FATAL"))); + assert!(!predicate.matches(&cel_sample("info msg", 9, "INFO"))); +} + +#[test] +fn cel_body_contains_match() { + let predicate = + PredicateArgs { cel: vec!["body.contains(\"timeout\")".to_string()], ..PredicateArgs::default() }.build().unwrap(); + assert!(predicate.matches(&cel_sample("connection timeout", 17, "ERROR"))); + assert!(!predicate.matches(&cel_sample("connection ok", 9, "INFO"))); +} + +#[test] +fn cel_service_name_match() { + let predicate = + PredicateArgs { cel: vec!["service_name == \"test-svc\"".to_string()], ..PredicateArgs::default() }.build().unwrap(); + assert!(predicate.matches(&cel_sample("msg", 9, "INFO"))); +} + +#[test] +fn cel_combined_with_fixed_string() { + let predicate = PredicateArgs { + cel: vec!["severity_number >= 13".to_string()], + fixed_string: vec!["timeout".to_string()], + ..PredicateArgs::default() + } + .build() + .unwrap(); + assert!(predicate.matches(&cel_sample("connection timeout after 30s", 17, "ERROR"))); + assert!(!predicate.matches(&cel_sample("connection timeout after 30s", 9, "INFO"))); +} + +#[test] +fn cel_invalid_expression_is_reported() { + let error = PredicateArgs { cel: vec!["@$#% invalid".to_string()], ..PredicateArgs::default() }.build().unwrap_err(); + assert!(error.to_string().contains("invalid CEL expression")); +} + +#[test] +fn cel_repeated_exprs_are_combined_with_and_semantics() { + let predicate = PredicateArgs { + cel: vec![ + "severity_number >= 13".to_string(), + "body.contains(\"timeout\")".to_string(), + ], + ..PredicateArgs::default() + } + .build() + .unwrap(); + assert!(predicate.matches(&cel_sample("connection timeout", 17, "ERROR"))); + assert!(!predicate.matches(&cel_sample("connection timeout", 9, "INFO"))); + assert!(!predicate.matches(&cel_sample("connection ok", 17, "ERROR"))); +} + +#[test] +fn parse_filter_query_uses_cel_mode_for_bare_text() { + let predicate = parse_filter_query("severity_number >= 13", FilterMode::Cel).unwrap(); + let payload = make_cel_test_payload("error msg", 13, "ERROR", "test-svc"); + let record = sample_record(&payload); + assert!(predicate.matches(&record)); +} From 0c0ecee363c33db1f8c4a6eb3f4a519321115c27 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 18:41:39 +0200 Subject: [PATCH 05/22] Add support for metrics and traces --- Cargo.lock | 1 + ljcel/Cargo.toml | 3 +- ljcel/src/context.rs | 142 ++++++++++++++++++++++++ ljcel/src/expression.rs | 237 ++++++++++++++++++++++++++++++++++++---- ljx/src/predicate.rs | 2 +- 5 files changed, 362 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 986f52c..8f5d7e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1431,6 +1431,7 @@ name = "ljcel" version = "0.9.0" dependencies = [ "cel", + "logjet", "opentelemetry-proto", "prost", "serde_json", diff --git a/ljcel/Cargo.toml b/ljcel/Cargo.toml index eddbab7..a134310 100644 --- a/ljcel/Cargo.toml +++ b/ljcel/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true [dependencies] cel = { version = "0.14", features = ["json"] } -opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "logs"] } +logjet = { path = ".." } +opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "logs", "metrics", "trace"] } prost = "0.14" serde_json = "1" diff --git a/ljcel/src/context.rs b/ljcel/src/context.rs index fe5d650..e2b6432 100644 --- a/ljcel/src/context.rs +++ b/ljcel/src/context.rs @@ -70,6 +70,148 @@ impl LogRecordContext { } } +pub(crate) struct MetricDataPointContext { + pub metric_name: String, + pub metric_unit: String, + pub metric_description: String, + pub metric_type: String, + pub value: f64, + pub count: i64, + pub sum: f64, + pub time_unix_nano: i64, + pub service_name: String, + pub scope_name: String, + pub resource: JsonMap, + pub scope: JsonMap, + pub attributes: JsonMap, +} + +impl MetricDataPointContext { + pub(crate) fn to_cel_context(&self) -> Result, CelError> { + let mut ctx = cel::Context::default(); + ctx.add_variable("metric_name", self.metric_name.clone()) + .map_err(|e| CelError::Compile(format!("metric_name: {e}")))?; + if !self.metric_unit.is_empty() { + ctx.add_variable("metric_unit", self.metric_unit.clone()) + .map_err(|e| CelError::Compile(format!("metric_unit: {e}")))?; + } + if !self.metric_description.is_empty() { + ctx.add_variable("metric_description", self.metric_description.clone()) + .map_err(|e| CelError::Compile(format!("metric_description: {e}")))?; + } + ctx.add_variable("metric_type", self.metric_type.clone()) + .map_err(|e| CelError::Compile(format!("metric_type: {e}")))?; + ctx.add_variable("value", self.value) + .map_err(|e| CelError::Compile(format!("value: {e}")))?; + ctx.add_variable("count", self.count) + .map_err(|e| CelError::Compile(format!("count: {e}")))?; + ctx.add_variable("sum", self.sum) + .map_err(|e| CelError::Compile(format!("sum: {e}")))?; + if self.time_unix_nano > 0 { + ctx.add_variable("time_unix_nano", self.time_unix_nano) + .map_err(|e| CelError::Compile(format!("time_unix_nano: {e}")))?; + } + if !self.service_name.is_empty() { + ctx.add_variable("service_name", self.service_name.clone()) + .map_err(|e| CelError::Compile(format!("service_name: {e}")))?; + } + if !self.scope_name.is_empty() { + ctx.add_variable("scope_name", self.scope_name.clone()) + .map_err(|e| CelError::Compile(format!("scope_name: {e}")))?; + } + ctx.add_variable("resource", self.resource.clone()) + .map_err(|e| CelError::Compile(format!("resource: {e}")))?; + ctx.add_variable("scope", self.scope.clone()) + .map_err(|e| CelError::Compile(format!("scope: {e}")))?; + ctx.add_variable("attributes", self.attributes.clone()) + .map_err(|e| CelError::Compile(format!("attributes: {e}")))?; + Ok(ctx) + } +} + +pub(crate) struct SpanContext { + pub trace_id: String, + pub span_id: String, + pub parent_span_id: String, + pub name: String, + pub kind: i64, + pub start_time_unix_nano: i64, + pub end_time_unix_nano: i64, + pub duration_ns: i64, + pub status_code: i64, + pub status_message: String, + pub service_name: String, + pub scope_name: String, + pub resource: JsonMap, + pub scope: JsonMap, + pub attributes: JsonMap, +} + +impl SpanContext { + pub(crate) fn to_cel_context(&self) -> Result, CelError> { + let mut ctx = cel::Context::default(); + if !self.trace_id.is_empty() { + ctx.add_variable("trace_id", self.trace_id.clone()) + .map_err(|e| CelError::Compile(format!("trace_id: {e}")))?; + } + ctx.add_variable("span_id", self.span_id.clone()) + .map_err(|e| CelError::Compile(format!("span_id: {e}")))?; + if !self.parent_span_id.is_empty() { + ctx.add_variable("parent_span_id", self.parent_span_id.clone()) + .map_err(|e| CelError::Compile(format!("parent_span_id: {e}")))?; + } + ctx.add_variable("name", self.name.clone()) + .map_err(|e| CelError::Compile(format!("name: {e}")))?; + ctx.add_variable("kind", self.kind) + .map_err(|e| CelError::Compile(format!("kind: {e}")))?; + if self.start_time_unix_nano > 0 { + ctx.add_variable("start_time_unix_nano", self.start_time_unix_nano) + .map_err(|e| CelError::Compile(format!("start_time_unix_nano: {e}")))?; + } + if self.end_time_unix_nano > 0 { + ctx.add_variable("end_time_unix_nano", self.end_time_unix_nano) + .map_err(|e| CelError::Compile(format!("end_time_unix_nano: {e}")))?; + } + if self.duration_ns > 0 { + ctx.add_variable("duration_ns", self.duration_ns) + .map_err(|e| CelError::Compile(format!("duration_ns: {e}")))?; + } + ctx.add_variable("status_code", self.status_code) + .map_err(|e| CelError::Compile(format!("status_code: {e}")))?; + if !self.status_message.is_empty() { + ctx.add_variable("status_message", self.status_message.clone()) + .map_err(|e| CelError::Compile(format!("status_message: {e}")))?; + } + if !self.service_name.is_empty() { + ctx.add_variable("service_name", self.service_name.clone()) + .map_err(|e| CelError::Compile(format!("service_name: {e}")))?; + } + if !self.scope_name.is_empty() { + ctx.add_variable("scope_name", self.scope_name.clone()) + .map_err(|e| CelError::Compile(format!("scope_name: {e}")))?; + } + ctx.add_variable("resource", self.resource.clone()) + .map_err(|e| CelError::Compile(format!("resource: {e}")))?; + ctx.add_variable("scope", self.scope.clone()) + .map_err(|e| CelError::Compile(format!("scope: {e}")))?; + ctx.add_variable("attributes", self.attributes.clone()) + .map_err(|e| CelError::Compile(format!("attributes: {e}")))?; + Ok(ctx) + } +} + +pub(crate) fn extract_service_name(resource_attrs: &[KeyValue]) -> String { + resource_attrs + .iter() + .find(|a| a.key == "service.name") + .and_then(|a| a.value.as_ref()) + .and_then(|v| match &v.value { + Some(Value::StringValue(s)) => Some(s.clone()), + _ => None, + }) + .unwrap_or_default() +} + pub(crate) fn key_values_to_map(attrs: &[KeyValue]) -> JsonMap { let mut map = JsonMap::new(); for attr in attrs { diff --git a/ljcel/src/expression.rs b/ljcel/src/expression.rs index b637f94..5408160 100644 --- a/ljcel/src/expression.rs +++ b/ljcel/src/expression.rs @@ -1,10 +1,15 @@ use std::sync::Arc; use cel::Program; +use logjet::RecordType; use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; +use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest; +use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; +use opentelemetry_proto::tonic::common::v1::any_value::Value as AnyValueKind; +use opentelemetry_proto::tonic::metrics::v1::metric::Data as MetricData; use prost::Message; -use crate::context::{LogRecordContext, hex_encode, key_values_to_map}; +use crate::context::{LogRecordContext, MetricDataPointContext, SpanContext, extract_service_name, hex_encode, key_values_to_map}; use crate::error::CelError; #[derive(Debug, Clone)] @@ -23,38 +28,36 @@ impl CelExpression { &self.source } + pub fn matches_payload(&self, record_type: RecordType, payload: &[u8]) -> Result { + match record_type { + RecordType::Logs => self.matches_logs_payload(payload), + RecordType::Metrics => self.matches_metrics_payload(payload), + RecordType::Traces => self.matches_traces_payload(payload), + _ => Ok(true), + } + } + pub fn matches_logs_payload(&self, payload: &[u8]) -> Result { let batch = ExportLogsServiceRequest::decode(payload).map_err(|e| CelError::Decode(e.to_string()))?; for rl in &batch.resource_logs { let resource_attrs = key_values_to_map(rl.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); - - let service_name = rl - .resource - .as_ref() - .and_then(|r| r.attributes.iter().find(|a| a.key == "service.name")) - .and_then(|a| a.value.as_ref()) - .and_then(|v| match &v.value { - Some(opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(s)) => Some(s.clone()), - _ => None, - }) - .unwrap_or_default(); + let service_name = extract_service_name( + rl.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[]), + ); for sl in &rl.scope_logs { let scope_name = sl.scope.as_ref().map(|s| s.name.clone()).unwrap_or_default(); - let scope_attrs = key_values_to_map( - sl.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[]), - ); + let scope_attrs = + key_values_to_map(sl.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); for lr in &sl.log_records { let body = lr .body .as_ref() .and_then(|v| match &v.value { - Some(opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(s)) => { - Some(s.clone()) - } + Some(AnyValueKind::StringValue(s)) => Some(s.clone()), _ => None, }) .unwrap_or_default(); @@ -76,7 +79,7 @@ impl CelExpression { attributes: key_values_to_map(&lr.attributes), }; - if self.eval(&ctx)? { + if self.eval_log(&ctx)? { return Ok(true); } } @@ -85,9 +88,201 @@ impl CelExpression { Ok(false) } - fn eval(&self, ctx: &LogRecordContext) -> Result { + pub fn matches_metrics_payload(&self, payload: &[u8]) -> Result { + let batch = ExportMetricsServiceRequest::decode(payload).map_err(|e| CelError::Decode(e.to_string()))?; + + for rm in &batch.resource_metrics { + let resource_attrs = + key_values_to_map(rm.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); + let service_name = extract_service_name( + rm.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[]), + ); + + for sm in &rm.scope_metrics { + let scope_name = sm.scope.as_ref().map(|s| s.name.clone()).unwrap_or_default(); + let scope_attrs = + key_values_to_map(sm.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); + + for metric in &sm.metrics { + let dp_contexts: Vec = match &metric.data { + Some(MetricData::Gauge(g)) => g.data_points.iter().map(|dp| { + let value = dp.value.as_ref().map_or(0.0, |v| match v { + opentelemetry_proto::tonic::metrics::v1::number_data_point::Value::AsDouble(d) => *d, + opentelemetry_proto::tonic::metrics::v1::number_data_point::Value::AsInt(i) => *i as f64, + }); + MetricDataPointContext { + metric_name: metric.name.clone(), + metric_unit: metric.unit.clone(), + metric_description: metric.description.clone(), + metric_type: "Gauge".to_string(), + value, + count: 0, + sum: 0.0, + time_unix_nano: dp.time_unix_nano as i64, + service_name: service_name.clone(), + scope_name: scope_name.clone(), + resource: resource_attrs.clone(), + scope: scope_attrs.clone(), + attributes: key_values_to_map(&dp.attributes), + } + }).collect(), + Some(MetricData::Sum(s)) => s.data_points.iter().map(|dp| { + let value = dp.value.as_ref().map_or(0.0, |v| match v { + opentelemetry_proto::tonic::metrics::v1::number_data_point::Value::AsDouble(d) => *d, + opentelemetry_proto::tonic::metrics::v1::number_data_point::Value::AsInt(i) => *i as f64, + }); + MetricDataPointContext { + metric_name: metric.name.clone(), + metric_unit: metric.unit.clone(), + metric_description: metric.description.clone(), + metric_type: "Sum".to_string(), + value, + count: 0, + sum: 0.0, + time_unix_nano: dp.time_unix_nano as i64, + service_name: service_name.clone(), + scope_name: scope_name.clone(), + resource: resource_attrs.clone(), + scope: scope_attrs.clone(), + attributes: key_values_to_map(&dp.attributes), + } + }).collect(), + Some(MetricData::Histogram(h)) => h.data_points.iter().map(|dp| { + MetricDataPointContext { + metric_name: metric.name.clone(), + metric_unit: metric.unit.clone(), + metric_description: metric.description.clone(), + metric_type: "Histogram".to_string(), + value: 0.0, + count: dp.count as i64, + sum: dp.sum.unwrap_or(0.0), + time_unix_nano: dp.time_unix_nano as i64, + service_name: service_name.clone(), + scope_name: scope_name.clone(), + resource: resource_attrs.clone(), + scope: scope_attrs.clone(), + attributes: key_values_to_map(&dp.attributes), + } + }).collect(), + Some(MetricData::ExponentialHistogram(eh)) => eh.data_points.iter().map(|dp| { + MetricDataPointContext { + metric_name: metric.name.clone(), + metric_unit: metric.unit.clone(), + metric_description: metric.description.clone(), + metric_type: "ExponentialHistogram".to_string(), + value: 0.0, + count: dp.count as i64, + sum: dp.sum.unwrap_or(0.0), + time_unix_nano: dp.time_unix_nano as i64, + service_name: service_name.clone(), + scope_name: scope_name.clone(), + resource: resource_attrs.clone(), + scope: scope_attrs.clone(), + attributes: key_values_to_map(&dp.attributes), + } + }).collect(), + Some(MetricData::Summary(s)) => s.data_points.iter().map(|dp| { + MetricDataPointContext { + metric_name: metric.name.clone(), + metric_unit: metric.unit.clone(), + metric_description: metric.description.clone(), + metric_type: "Summary".to_string(), + value: 0.0, + count: dp.count as i64, + sum: dp.sum, + time_unix_nano: dp.time_unix_nano as i64, + service_name: service_name.clone(), + scope_name: scope_name.clone(), + resource: resource_attrs.clone(), + scope: scope_attrs.clone(), + attributes: key_values_to_map(&dp.attributes), + } + }).collect(), + None => continue, + }; + + for ctx in &dp_contexts { + if self.eval_metric(ctx)? { + return Ok(true); + } + } + } + } + } + Ok(false) + } + + pub fn matches_traces_payload(&self, payload: &[u8]) -> Result { + let batch = ExportTraceServiceRequest::decode(payload).map_err(|e| CelError::Decode(e.to_string()))?; + + for rs in &batch.resource_spans { + let resource_attrs = + key_values_to_map(rs.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); + let service_name = extract_service_name( + rs.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[]), + ); + + for ss in &rs.scope_spans { + let scope_name = ss.scope.as_ref().map(|s| s.name.clone()).unwrap_or_default(); + let scope_attrs = + key_values_to_map(ss.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); + + for span in &ss.spans { + let duration_ns = + if span.end_time_unix_nano > span.start_time_unix_nano { + span.end_time_unix_nano - span.start_time_unix_nano + } else { + 0 + }; + + let (status_code, status_message) = span.status.as_ref().map_or((0, String::new()), |s| { + (s.code, s.message.clone()) + }); + + let ctx = SpanContext { + trace_id: hex_encode(&span.trace_id), + span_id: hex_encode(&span.span_id), + parent_span_id: hex_encode(&span.parent_span_id), + name: span.name.clone(), + kind: span.kind as i64, + start_time_unix_nano: span.start_time_unix_nano as i64, + end_time_unix_nano: span.end_time_unix_nano as i64, + duration_ns: duration_ns as i64, + status_code: status_code as i64, + status_message, + service_name: service_name.clone(), + scope_name: scope_name.clone(), + resource: resource_attrs.clone(), + scope: scope_attrs.clone(), + attributes: key_values_to_map(&span.attributes), + }; + + if self.eval_span(&ctx)? { + return Ok(true); + } + } + } + } + Ok(false) + } + + fn eval_log(&self, ctx: &LogRecordContext) -> Result { + let cel_ctx = ctx.to_cel_context()?; + self.eval_bool(&cel_ctx) + } + + fn eval_metric(&self, ctx: &MetricDataPointContext) -> Result { + let cel_ctx = ctx.to_cel_context()?; + self.eval_bool(&cel_ctx) + } + + fn eval_span(&self, ctx: &SpanContext) -> Result { let cel_ctx = ctx.to_cel_context()?; - match self.program.execute(&cel_ctx) { + self.eval_bool(&cel_ctx) + } + + fn eval_bool(&self, cel_ctx: &cel::Context<'_>) -> Result { + match self.program.execute(cel_ctx) { Ok(cel::Value::Bool(true)) => Ok(true), Ok(cel::Value::Bool(false)) => Ok(false), Ok(other) => Err(CelError::NotBoolean(format!("{other:?}"))), diff --git a/ljx/src/predicate.rs b/ljx/src/predicate.rs index fd72c5d..88f5ef0 100644 --- a/ljx/src/predicate.rs +++ b/ljx/src/predicate.rs @@ -236,7 +236,7 @@ impl RecordPredicate { } } for cel in &self.cel_queries { - if !cel.matches_logs_payload(&record.payload).unwrap_or(false) { + if !cel.matches_payload(record.record_type, &record.payload).unwrap_or(false) { return false; } } From 7d04c986cb4202c1a7187c4a0d1095c03baf69fa Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 18:41:48 +0200 Subject: [PATCH 06/22] Add unit tests --- ljcel/tests/unit/ljcel_ut.rs | 186 +++++++++++++++++++++++++++++++++++ ljx/tests/unit/cli_utst.rs | 13 +++ 2 files changed, 199 insertions(+) diff --git a/ljcel/tests/unit/ljcel_ut.rs b/ljcel/tests/unit/ljcel_ut.rs index fc978b7..416691d 100644 --- a/ljcel/tests/unit/ljcel_ut.rs +++ b/ljcel/tests/unit/ljcel_ut.rs @@ -1,8 +1,13 @@ use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; +use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest; +use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; use opentelemetry_proto::tonic::common::v1::any_value::Value; use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue}; use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs}; +use opentelemetry_proto::tonic::metrics::v1::{Gauge, Metric, NumberDataPoint, ResourceMetrics, ScopeMetrics}; use opentelemetry_proto::tonic::resource::v1::Resource; +use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans, Span, Status}; +use logjet::RecordType; use prost::Message; use crate::CelExpression; @@ -208,3 +213,184 @@ fn no_record_in_batch_matches() { ]); assert!(!expr.matches_logs_payload(&payload).unwrap()); } + +// --- metrics helpers --- + +fn make_metric_payload(metric_name: &str, metric_type: &str, value: f64) -> Vec { + let batch = ExportMetricsServiceRequest { + resource_metrics: vec![ResourceMetrics { + resource: Some(Resource { + attributes: vec![KeyValue { + key: "service.name".to_string(), + value: Some(AnyValue { value: Some(Value::StringValue("test-svc".to_string())) }), + }], + dropped_attributes_count: 0, + entity_refs: Vec::new(), + }), + scope_metrics: vec![ScopeMetrics { + scope: Some(InstrumentationScope { + name: "test-scope".to_string(), + version: "1.0.0".to_string(), + attributes: vec![], + dropped_attributes_count: 0, + }), + metrics: vec![Metric { + name: metric_name.to_string(), + unit: "ms".to_string(), + description: String::new(), + metadata: vec![], + data: Some(match metric_type { + "Gauge" => opentelemetry_proto::tonic::metrics::v1::metric::Data::Gauge(Gauge { + data_points: vec![NumberDataPoint { + value: Some( + opentelemetry_proto::tonic::metrics::v1::number_data_point::Value::AsDouble(value), + ), + time_unix_nano: 1000, + attributes: vec![], + start_time_unix_nano: 0, + flags: 0, + exemplars: vec![], + }], + }), + _ => opentelemetry_proto::tonic::metrics::v1::metric::Data::Gauge(Gauge { + data_points: vec![NumberDataPoint { + value: Some( + opentelemetry_proto::tonic::metrics::v1::number_data_point::Value::AsDouble(value), + ), + time_unix_nano: 1000, + attributes: vec![], + start_time_unix_nano: 0, + flags: 0, + exemplars: vec![], + }], + }), + }), + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + batch.encode_to_vec() +} + +// --- traces helpers --- + +fn make_trace_payload(span_name: &str, kind: i32, status_code: i32) -> Vec { + let batch = ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + resource: Some(Resource { + attributes: vec![KeyValue { + key: "service.name".to_string(), + value: Some(AnyValue { value: Some(Value::StringValue("test-svc".to_string())) }), + }], + dropped_attributes_count: 0, + entity_refs: Vec::new(), + }), + scope_spans: vec![ScopeSpans { + scope: Some(InstrumentationScope { + name: "test-scope".to_string(), + version: "1.0.0".to_string(), + attributes: vec![], + dropped_attributes_count: 0, + }), + spans: vec![Span { + trace_id: vec![1u8; 16], + span_id: vec![2u8; 8], + name: span_name.to_string(), + kind, + start_time_unix_nano: 1_000_000_000, + end_time_unix_nano: 2_000_000_000, + status: Some(Status { code: status_code, message: String::new() }), + ..Default::default() + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + batch.encode_to_vec() +} + +// --- metrics tests --- + +#[test] +fn metric_name_match() { + let expr = CelExpression::compile("metric_name == \"http.latency\"").unwrap(); + let payload = make_metric_payload("http.latency", "Gauge", 42.0); + assert!(expr.matches_metrics_payload(&payload).unwrap()); +} + +#[test] +fn metric_name_no_match() { + let expr = CelExpression::compile("metric_name == \"http.latency\"").unwrap(); + let payload = make_metric_payload("cpu.usage", "Gauge", 42.0); + assert!(!expr.matches_metrics_payload(&payload).unwrap()); +} + +#[test] +fn metric_value_above_threshold() { + let expr = CelExpression::compile("value >= 100.0").unwrap(); + let payload = make_metric_payload("cpu.usage", "Gauge", 150.0); + assert!(expr.matches_metrics_payload(&payload).unwrap()); +} + +#[test] +fn metric_type_gauge() { + let expr = CelExpression::compile("metric_type == \"Gauge\"").unwrap(); + let payload = make_metric_payload("cpu.usage", "Gauge", 50.0); + assert!(expr.matches_metrics_payload(&payload).unwrap()); +} + +// --- traces tests --- + +#[test] +fn span_name_match() { + let expr = CelExpression::compile("name == \"GetUser\"").unwrap(); + let payload = make_trace_payload("GetUser", 2, 0); + assert!(expr.matches_traces_payload(&payload).unwrap()); +} + +#[test] +fn span_kind_server() { + let expr = CelExpression::compile("kind == 2").unwrap(); + let payload = make_trace_payload("GetUser", 2, 0); + assert!(expr.matches_traces_payload(&payload).unwrap()); +} + +#[test] +fn span_status_error() { + let expr = CelExpression::compile("status_code == 2").unwrap(); + let payload = make_trace_payload("GetUser", 2, 2); + assert!(expr.matches_traces_payload(&payload).unwrap()); +} + +#[test] +fn span_duration_above_threshold() { + let expr = CelExpression::compile("duration_ns >= 500000000").unwrap(); + let payload = make_trace_payload("GetUser", 2, 0); + assert!(expr.matches_traces_payload(&payload).unwrap()); +} + +// --- generic dispatch tests --- + +#[test] +fn matches_payload_dispatches_to_logs() { + let expr = CelExpression::compile("severity_number == 17").unwrap(); + let payload = make_log_payload(vec![log_record("error msg", 17, "ERROR")]); + assert!(expr.matches_payload(RecordType::Logs, &payload).unwrap()); +} + +#[test] +fn matches_payload_dispatches_to_metrics() { + let expr = CelExpression::compile("metric_name == \"cpu.usage\"").unwrap(); + let payload = make_metric_payload("cpu.usage", "Gauge", 50.0); + assert!(expr.matches_payload(RecordType::Metrics, &payload).unwrap()); +} + +#[test] +fn matches_payload_dispatches_to_traces() { + let expr = CelExpression::compile("name == \"GetUser\"").unwrap(); + let payload = make_trace_payload("GetUser", 2, 0); + assert!(expr.matches_payload(RecordType::Traces, &payload).unwrap()); +} diff --git a/ljx/tests/unit/cli_utst.rs b/ljx/tests/unit/cli_utst.rs index d66f2b7..52463a8 100644 --- a/ljx/tests/unit/cli_utst.rs +++ b/ljx/tests/unit/cli_utst.rs @@ -134,3 +134,16 @@ fn help_lists_current_export_formats() { assert!(!help.contains("Discovered plugin export formats:")); assert!(!help.contains("All export formats now:")); } + +#[test] +fn top_level_query_parses_cel_flag() { + let cli = Cli::try_parse_from(["ljx", "input.logjet", "--cel", "severity_number >= 13"]).expect("cli parses"); + assert_eq!(cli.predicate.cel, vec!["severity_number >= 13".to_string()]); +} + +#[test] +fn top_level_query_parses_repeated_cel_flags() { + let cli = Cli::try_parse_from(["ljx", "input.logjet", "--cel", "severity_number >= 13", "--cel", "body.contains(\"err\")"]) + .expect("cli parses"); + assert_eq!(cli.predicate.cel, vec!["severity_number >= 13".to_string(), "body.contains(\"err\")".to_string()]); +} From e226e4ebcdb4db29c0b8d88f7b808a71204b4f80 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 19:32:14 +0200 Subject: [PATCH 07/22] Fix CEL query on maps --- Cargo.lock | 3 -- ljcel/Cargo.toml | 3 +- ljcel/src/context.rs | 75 +++++++++++++++++++----------------- ljcel/src/expression.rs | 35 ++++++++--------- ljcel/tests/unit/ljcel_ut.rs | 12 ++++++ 5 files changed, 68 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f5d7e0..d28e781 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,14 +414,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed39583e427bf41d93c28c7f27c943a93bcb8697220ff3575fd53a9e13f3814" dependencies = [ "antlr4rust", - "base64", "chrono", "lazy_static", "nom", "pastey", "regex", "serde", - "serde_json", "thiserror 1.0.69", ] @@ -1434,7 +1432,6 @@ dependencies = [ "logjet", "opentelemetry-proto", "prost", - "serde_json", ] [[package]] diff --git a/ljcel/Cargo.toml b/ljcel/Cargo.toml index a134310..13ef7bb 100644 --- a/ljcel/Cargo.toml +++ b/ljcel/Cargo.toml @@ -5,8 +5,7 @@ edition.workspace = true license.workspace = true [dependencies] -cel = { version = "0.14", features = ["json"] } +cel = "0.14" logjet = { path = ".." } opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "logs", "metrics", "trace"] } prost = "0.14" -serde_json = "1" diff --git a/ljcel/src/context.rs b/ljcel/src/context.rs index e2b6432..ee26077 100644 --- a/ljcel/src/context.rs +++ b/ljcel/src/context.rs @@ -1,6 +1,8 @@ -use opentelemetry_proto::tonic::common::v1::any_value::Value; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use opentelemetry_proto::tonic::common::v1::any_value::Value as OtelValue; use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue}; -use serde_json::{Map as JsonMap, Value as JsonValue}; use crate::error::CelError; @@ -16,9 +18,9 @@ pub(crate) struct LogRecordContext { pub trace_id: String, pub span_id: String, pub flags: i32, - pub resource: JsonMap, - pub scope: JsonMap, - pub attributes: JsonMap, + pub resource: cel::Value, + pub scope: cel::Value, + pub attributes: cel::Value, } impl LogRecordContext { @@ -81,9 +83,9 @@ pub(crate) struct MetricDataPointContext { pub time_unix_nano: i64, pub service_name: String, pub scope_name: String, - pub resource: JsonMap, - pub scope: JsonMap, - pub attributes: JsonMap, + pub resource: cel::Value, + pub scope: cel::Value, + pub attributes: cel::Value, } impl MetricDataPointContext { @@ -142,9 +144,9 @@ pub(crate) struct SpanContext { pub status_message: String, pub service_name: String, pub scope_name: String, - pub resource: JsonMap, - pub scope: JsonMap, - pub attributes: JsonMap, + pub resource: cel::Value, + pub scope: cel::Value, + pub attributes: cel::Value, } impl SpanContext { @@ -206,47 +208,50 @@ pub(crate) fn extract_service_name(resource_attrs: &[KeyValue]) -> String { .find(|a| a.key == "service.name") .and_then(|a| a.value.as_ref()) .and_then(|v| match &v.value { - Some(Value::StringValue(s)) => Some(s.clone()), + Some(OtelValue::StringValue(s)) => Some(s.clone()), _ => None, }) .unwrap_or_default() } -pub(crate) fn key_values_to_map(attrs: &[KeyValue]) -> JsonMap { - let mut map = JsonMap::new(); +pub(crate) fn key_values_to_cel_map(attrs: &[KeyValue]) -> cel::Value { + let mut seen: HashSet = HashSet::new(); + let mut map: HashMap, cel::Value> = HashMap::new(); for attr in attrs { - if map.contains_key(&attr.key) { + if !seen.insert(attr.key.clone()) { continue; } - if let Some(value) = attr.value.as_ref() - && let Some(json) = any_value_to_json(value) - { - map.insert(attr.key.clone(), json); + if let Some(value) = attr.value.as_ref() { + map.insert(Arc::new(attr.key.clone()), any_value_to_cel(value)); } } - map -} + cel::Value::Map(map.into()) + } -pub(crate) fn any_value_to_json(value: &AnyValue) -> Option { + +fn any_value_to_cel(value: &AnyValue) -> cel::Value { match &value.value { - Some(Value::StringValue(s)) => Some(JsonValue::String(s.clone())), - Some(Value::BoolValue(b)) => Some(JsonValue::Bool(*b)), - Some(Value::IntValue(n)) => Some(JsonValue::Number((*n).into())), - Some(Value::DoubleValue(d)) => serde_json::Number::from_f64(*d).map(JsonValue::Number), - Some(Value::BytesValue(b)) => Some(JsonValue::String(format!("<{} bytes>", b.len()))), - Some(Value::ArrayValue(a)) => { - Some(JsonValue::Array(a.values.iter().filter_map(any_value_to_json).collect())) + Some(OtelValue::StringValue(s)) => cel::Value::String(Arc::new(s.clone())), + Some(OtelValue::BoolValue(b)) => cel::Value::Bool(*b), + Some(OtelValue::IntValue(n)) => cel::Value::Int(*n), + Some(OtelValue::DoubleValue(d)) => cel::Value::Float(*d), + Some(OtelValue::BytesValue(b)) => { + cel::Value::String(Arc::new(format!("<{} bytes>", b.len()))) + } + Some(OtelValue::ArrayValue(a)) => { + let list: Vec = a.values.iter().map(any_value_to_cel).collect(); + cel::Value::List(Arc::new(list)) } - Some(Value::KvlistValue(k)) => { - let mut obj = JsonMap::new(); + Some(OtelValue::KvlistValue(k)) => { + let mut map: HashMap, cel::Value> = HashMap::new(); for item in &k.values { - if let Some(inner) = item.value.as_ref().and_then(any_value_to_json) { - obj.insert(item.key.clone(), inner); + if let Some(value) = item.value.as_ref() { + map.insert(Arc::new(item.key.clone()), any_value_to_cel(value)); } } - Some(JsonValue::Object(obj)) + cel::Value::Map(map.into()) } - None => None, + None => cel::Value::String(Arc::new(String::new())), } } diff --git a/ljcel/src/expression.rs b/ljcel/src/expression.rs index 5408160..ca14373 100644 --- a/ljcel/src/expression.rs +++ b/ljcel/src/expression.rs @@ -9,7 +9,7 @@ use opentelemetry_proto::tonic::common::v1::any_value::Value as AnyValueKind; use opentelemetry_proto::tonic::metrics::v1::metric::Data as MetricData; use prost::Message; -use crate::context::{LogRecordContext, MetricDataPointContext, SpanContext, extract_service_name, hex_encode, key_values_to_map}; +use crate::context::{LogRecordContext, MetricDataPointContext, SpanContext, extract_service_name, hex_encode, key_values_to_cel_map}; use crate::error::CelError; #[derive(Debug, Clone)] @@ -42,7 +42,7 @@ impl CelExpression { for rl in &batch.resource_logs { let resource_attrs = - key_values_to_map(rl.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); + key_values_to_cel_map(rl.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); let service_name = extract_service_name( rl.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[]), ); @@ -50,7 +50,7 @@ impl CelExpression { for sl in &rl.scope_logs { let scope_name = sl.scope.as_ref().map(|s| s.name.clone()).unwrap_or_default(); let scope_attrs = - key_values_to_map(sl.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); + key_values_to_cel_map(sl.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); for lr in &sl.log_records { let body = lr @@ -76,7 +76,7 @@ impl CelExpression { flags: lr.flags as i32, resource: resource_attrs.clone(), scope: scope_attrs.clone(), - attributes: key_values_to_map(&lr.attributes), + attributes: key_values_to_cel_map(&lr.attributes), }; if self.eval_log(&ctx)? { @@ -93,7 +93,7 @@ impl CelExpression { for rm in &batch.resource_metrics { let resource_attrs = - key_values_to_map(rm.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); + key_values_to_cel_map(rm.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); let service_name = extract_service_name( rm.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[]), ); @@ -101,7 +101,7 @@ impl CelExpression { for sm in &rm.scope_metrics { let scope_name = sm.scope.as_ref().map(|s| s.name.clone()).unwrap_or_default(); let scope_attrs = - key_values_to_map(sm.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); + key_values_to_cel_map(sm.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); for metric in &sm.metrics { let dp_contexts: Vec = match &metric.data { @@ -123,7 +123,7 @@ impl CelExpression { scope_name: scope_name.clone(), resource: resource_attrs.clone(), scope: scope_attrs.clone(), - attributes: key_values_to_map(&dp.attributes), + attributes: key_values_to_cel_map(&dp.attributes), } }).collect(), Some(MetricData::Sum(s)) => s.data_points.iter().map(|dp| { @@ -144,7 +144,7 @@ impl CelExpression { scope_name: scope_name.clone(), resource: resource_attrs.clone(), scope: scope_attrs.clone(), - attributes: key_values_to_map(&dp.attributes), + attributes: key_values_to_cel_map(&dp.attributes), } }).collect(), Some(MetricData::Histogram(h)) => h.data_points.iter().map(|dp| { @@ -161,7 +161,7 @@ impl CelExpression { scope_name: scope_name.clone(), resource: resource_attrs.clone(), scope: scope_attrs.clone(), - attributes: key_values_to_map(&dp.attributes), + attributes: key_values_to_cel_map(&dp.attributes), } }).collect(), Some(MetricData::ExponentialHistogram(eh)) => eh.data_points.iter().map(|dp| { @@ -178,7 +178,7 @@ impl CelExpression { scope_name: scope_name.clone(), resource: resource_attrs.clone(), scope: scope_attrs.clone(), - attributes: key_values_to_map(&dp.attributes), + attributes: key_values_to_cel_map(&dp.attributes), } }).collect(), Some(MetricData::Summary(s)) => s.data_points.iter().map(|dp| { @@ -195,7 +195,7 @@ impl CelExpression { scope_name: scope_name.clone(), resource: resource_attrs.clone(), scope: scope_attrs.clone(), - attributes: key_values_to_map(&dp.attributes), + attributes: key_values_to_cel_map(&dp.attributes), } }).collect(), None => continue, @@ -217,7 +217,7 @@ impl CelExpression { for rs in &batch.resource_spans { let resource_attrs = - key_values_to_map(rs.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); + key_values_to_cel_map(rs.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[])); let service_name = extract_service_name( rs.resource.as_ref().map(|r| r.attributes.as_slice()).unwrap_or(&[]), ); @@ -225,15 +225,10 @@ impl CelExpression { for ss in &rs.scope_spans { let scope_name = ss.scope.as_ref().map(|s| s.name.clone()).unwrap_or_default(); let scope_attrs = - key_values_to_map(ss.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); + key_values_to_cel_map(ss.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); for span in &ss.spans { - let duration_ns = - if span.end_time_unix_nano > span.start_time_unix_nano { - span.end_time_unix_nano - span.start_time_unix_nano - } else { - 0 - }; + let duration_ns = span.end_time_unix_nano.saturating_sub(span.start_time_unix_nano); let (status_code, status_message) = span.status.as_ref().map_or((0, String::new()), |s| { (s.code, s.message.clone()) @@ -254,7 +249,7 @@ impl CelExpression { scope_name: scope_name.clone(), resource: resource_attrs.clone(), scope: scope_attrs.clone(), - attributes: key_values_to_map(&span.attributes), + attributes: key_values_to_cel_map(&span.attributes), }; if self.eval_span(&ctx)? { diff --git a/ljcel/tests/unit/ljcel_ut.rs b/ljcel/tests/unit/ljcel_ut.rs index 416691d..135aed7 100644 --- a/ljcel/tests/unit/ljcel_ut.rs +++ b/ljcel/tests/unit/ljcel_ut.rs @@ -167,6 +167,18 @@ fn resource_attribute_match() { assert!(expr.matches_logs_payload(&payload).unwrap()); } +#[test] +fn resource_attribute_strict_equality() { + let expr = CelExpression::compile("resource[\"custom.label\"] == \"processor-a\"").unwrap(); + let payload = make_log_payload_with_context( + vec![log_record("msg", 9, "INFO")], + &[string_attr("custom.label", "processor-a")], + &[], + "test-svc", + ); + assert!(expr.matches_logs_payload(&payload).unwrap()); +} + #[test] fn scope_attribute_match() { let expr = CelExpression::compile("scope[\"custom.channel\"] >= 3").unwrap(); From d129da34fa89f3b474cede8e3dbfa797b3b64f7f Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 19:43:14 +0200 Subject: [PATCH 08/22] Add docs --- doc/cel.md | 225 ++++++++++++++++++++++++++++++++++++ doc/{README.md => index.md} | 2 + doc/ljx.md | 4 + docs/requirements.txt | 1 + 4 files changed, 232 insertions(+) create mode 100644 doc/cel.md rename doc/{README.md => index.md} (86%) create mode 100644 docs/requirements.txt diff --git a/doc/cel.md b/doc/cel.md new file mode 100644 index 0000000..b285694 --- /dev/null +++ b/doc/cel.md @@ -0,0 +1,225 @@ +# CEL Querying + +`ljx` supports [CEL (Common Expression Language)][cel-spec] for querying +OpenTelemetry attributes inside `.logjet` files. CEL queries are evaluated +against decoded OTLP records — resource attributes, scope attributes, +record-level attributes, and signal-specific fields. + +The CEL engine is provided by the `ljcel` crate, embedded directly in `ljx`. + +[cel-spec]: https://github.com/google/cel-spec + +## CLI Usage + +Use `--cel` on any `ljx` command that accepts filters. Repeat for AND semantics +(all expressions must match): + +```text +ljx telemetry.logjet --cel 'severity_number >= 17' +ljx count telemetry.logjet --cel 'service_name == "my-service"' +ljx filter telemetry.logjet -o errors.logjet --cel 'severity_number >= 13 && body.contains("timeout")' +ljx telemetry.logjet --cel 'severity_number >= 13' --cel 'body.contains("error")' +``` + +`--cel` works alongside `--grep`, `--fixed-string`, and field filters: + +```text +ljx telemetry.logjet --cel 'severity_number >= 13' --service my-service -F timeout +``` + +## TUI Usage + +In `ljx view`, the filter mode cycles between **CEL**, **strings**, and **regex**. +Press Up or Down while the search bar is focused. The default mode is CEL. + +``` +CEL → strings → regex (Up / Down cycles through all three) +``` + +The search bar label reflects the active mode: + +```text +Filter (CEL): +Filter (strings): +Filter (regex): +``` + +Typing a filter expression and pressing Enter applies it immediately. Bare +text without leading `-` is dispatched to the active filter mode: + +| Mode | Input | Treated as | +|------|-------|-----------| +| CEL | `severity_number >= 17` | CEL expression | +| strings | `timeout` | literal substring | +| regex | `timeout\|deadline` | regex | + +## Variable Reference + +CEL variables are exposed for every log record, metric data point, or span. +Type names follow CEL conventions: `int`, `double`, `string`, `bool`, +`map(string, dyn)`, `list(dyn)`. + +### Log Records + +| Variable | Type | Source | +|----------|------|--------| +| `body` | string | `LogRecord.body.string_value` | +| `severity_text` | string | `LogRecord.severity_text` | +| `severity_number` | int | `LogRecord.severity_number` | +| `service_name` | string | `Resource.attributes["service.name"]` | +| `scope_name` | string | `InstrumentationScope.name` | +| `event_name` | string | `LogRecord.event_name` | +| `time_unix_nano` | int | `LogRecord.time_unix_nano` | +| `observed_time_unix_nano` | int | `LogRecord.observed_time_unix_nano` | +| `trace_id` | string | `LogRecord.trace_id` (hex) | +| `span_id` | string | `LogRecord.span_id` (hex) | +| `flags` | int | `LogRecord.flags` | +| `resource` | map | `Resource.attributes` → `resource["key"]` | +| `scope` | map | `InstrumentationScope.attributes` → `scope["key"]` | +| `attributes` | map | `LogRecord.attributes` → `attributes["key"]` | + +Map keys preserve the original attribute key including dots. +Use bracket notation: `resource["service.name"]`, `attributes["http.status_code"]`. + +### Metric Data Points + +| Variable | Type | Source | +|----------|------|--------| +| `metric_name` | string | `Metric.name` | +| `metric_unit` | string | `Metric.unit` | +| `metric_description` | string | `Metric.description` | +| `metric_type` | string | `"Gauge"`, `"Sum"`, `"Histogram"`, `"ExponentialHistogram"`, or `"Summary"` | +| `value` | double | `NumberDataPoint.value` | +| `count` | int | `HistogramDataPoint.count` / `SummaryDataPoint.count` (0 for Gauge/Sum) | +| `sum` | double | `HistogramDataPoint.sum` / `SummaryDataPoint.sum` (0 for Gauge/Sum) | +| `time_unix_nano` | int | `NumberDataPoint.time_unix_nano` | +| `service_name` | string | `Resource.attributes["service.name"]` | +| `scope_name` | string | `InstrumentationScope.name` | +| `resource` | map | `Resource.attributes` → `resource["key"]` | +| `scope` | map | `InstrumentationScope.attributes` → `scope["key"]` | +| `attributes` | map | `NumberDataPoint.attributes` → `attributes["key"]` | + +### Span Records + +| Variable | Type | Source | +|----------|------|--------| +| `name` | string | `Span.name` | +| `kind` | int | `Span.kind` (1=Internal, 2=Server, 3=Client, 4=Producer, 5=Consumer) | +| `trace_id` | string | `Span.trace_id` (hex) | +| `span_id` | string | `Span.span_id` (hex) | +| `parent_span_id` | string | `Span.parent_span_id` (hex, empty for root spans) | +| `start_time_unix_nano` | int | `Span.start_time_unix_nano` | +| `end_time_unix_nano` | int | `Span.end_time_unix_nano` | +| `duration_ns` | int | `end_time_unix_nano - start_time_unix_nano` | +| `status_code` | int | `Status.code` (0=Unset, 1=Ok, 2=Error) | +| `status_message` | string | `Status.message` | +| `service_name` | string | `Resource.attributes["service.name"]` | +| `scope_name` | string | `InstrumentationScope.name` | +| `resource` | map | `Resource.attributes` → `resource["key"]` | +| `scope` | map | `InstrumentationScope.attributes` → `scope["key"]` | +| `attributes` | map | `Span.attributes` → `attributes["key"]` | + +## Query Examples + +### Scalars + +Simple comparisons on top-level fields: + +```cel +severity_number >= 17 +severity_text == "ERROR" +service_name == "my-service" +event_name == "myapp.log" +body.contains("timeout") +metric_name == "cpu.usage" +value >= 100.0 +name == "GetUser" +kind == 2 +duration_ns >= 1_000_000_000 +``` + +### Map bracket access + +Access attributes by key using bracket notation. String and integer values +support equality and comparison: + +```cel +resource["service.name"] == "my-service" +resource["host.name"].startsWith("k8s-node-") +attributes["http.status_code"] >= 400 +attributes["custom.flags"] == 42 +``` + +### List membership + +For attribute values that are arrays, use `.exists()` with a predicate: + +```cel +resource["custom.component"].exists(e, e == "processor-a") +scope["custom.channel"].exists(e, e == "control") +attributes["custom.thread"].exists(e, e == "worker-1") +``` + +List size and index access also work: + +```cel +resource["custom.component"].size() >= 2 +scope["custom.channel"].size() >= 3 +resource["custom.component"][0] == "processor-a" +scope["custom.thread"][0] == "worker-1" +``` + +### Combined conditions + +Logical AND (`&&`), OR (`||`), and negation (`!`) with parentheses: + +```cel +severity_number >= 13 && service_name == "processor-a" +service_name == "processor-a" && body.contains("discard") +resource["custom.component"].exists(e, e == "processor-a") && severity_number >= 13 +scope["custom.channel"].exists(e, e == "control") && scope["custom.thread"].exists(e, e == "worker-1") +attributes["custom.flags"] == 42 && attributes["custom.msg_type"] == 1 +severity_number >= 13 || body.contains("timeout") +``` + +## CEL Functions + +The `cel` crate provides these built-in functions: + +| Function | Applies to | Example | +|----------|-----------|---------| +| `.contains(substr)` | string | `body.contains("error")` | +| `.startsWith(prefix)` | string | `service_name.startsWith("prod-")` | +| `.endsWith(suffix)` | string | `body.endsWith("failed")` | +| `.matches(regex)` | string | `body.matches(".*timeout.*")` | +| `.size()` | string, list, map | `scope["custom.channel"].size()` | +| `.exists(e, predicate)` | list | `.exists(e, e == "value")` | +| `has(variable)` | any | `has(attributes.error_message)` | + +## Limitations + +**`String`-only `.contains()`.** The `.contains()` function only operates on +strings. For list membership, use `.exists(e, e == "value")` instead. This is +a limitation of the `cel` crate v0.14. + +```cel +# Works (string match) +body.contains("timeout") + +# Does NOT work (list match) +resource["custom.component"].contains("processor-a") + +# Use this instead +resource["custom.component"].exists(e, e == "processor-a") +``` + +**No `in` operator.** The CEL `in` operator is not yet available. + +```cel +# Does NOT work +"processor-a" in resource["custom.component"] +``` + +**Error visibility in the TUI.** If a CEL expression fails to evaluate +(e.g., invalid syntax, type mismatch), the record is silently excluded. +Use the CLI `--cel` flag for immediate feedback on expression errors. diff --git a/doc/README.md b/doc/index.md similarity index 86% rename from doc/README.md rename to doc/index.md index 12cd75f..dc4a862 100644 --- a/doc/README.md +++ b/doc/index.md @@ -2,7 +2,9 @@ - [overview.md](./overview.md): project overview - [c-cpp-integration.md](./c-cpp-integration.md): minimal `liblogjet` usage from C and C++ +- [cel.md](./cel.md): CEL querying for OTel attributes - [ljx.md](./ljx.md): `ljx` offline CLI scope and command plan +- [perfetto-ingest.md](./perfetto-ingest.md): Perfetto trace ingest plugin - [daemon.md](./daemon.md): `ljd` behaviour and current limits - [configuration.md](./configuration.md): YAML keys and defaults - [features.md](./features.md): current implemented daemon features and use cases diff --git a/doc/ljx.md b/doc/ljx.md index 0cde140..314e9d0 100644 --- a/doc/ljx.md +++ b/doc/ljx.md @@ -40,6 +40,7 @@ Documented command set: - `split` - `join` - `dedup` +- CEL querying Current implementation status for release `0.1`: @@ -91,6 +92,7 @@ ljx telemetry.logjet ljx telemetry.logjet -F error -i ljx telemetry.logjet -F error -e 'customer-123|customer-456' -i ljx telemetry.logjet --fields body,timestamp,service_name +ljx telemetry.logjet --cel 'severity_number >= 17' ljx --grep 'timeout|deadline' ./logs/*.logjet ``` @@ -198,6 +200,7 @@ Supported payload matching modes: - `-F`, `--fixed-string` for literal payload substring matching - `-e`, `--grep` for grep-style regex matching - `-i`, `--ignore-case` to make either payload matcher case-insensitive +- `--cel` for CEL expressions evaluated against decoded OTel attributes. See [CEL Querying](cel.md). Payload matchers are repeatable and combined with logical AND: @@ -253,6 +256,7 @@ Current shape: - dynamic details for the selected record on the right - `Enter` opens a full-record popup, `Esc` closes it - bounded-memory scan that spools matched records to a temp file instead of loading the whole input +- filter mode cycles between CEL, strings, and regex with Up / Down; default is CEL. See [CEL Querying](cel.md). ## `ljx split` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..4c8f017 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +mkdocs-material From f7815a229201867065e7c7aadb7fa164e33feb53 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:05:55 +0200 Subject: [PATCH 09/22] Add mxrun --- Cargo.lock | 786 +++++++++++++++++++---------------------- Makefile | 154 +++++++- README.md | 2 +- mkdocs.yml | 39 ++ mxrun.conf | 1 + scripts/maybe-mxrun.sh | 18 + scripts/setup-rust.sh | 3 + 7 files changed, 558 insertions(+), 445 deletions(-) create mode 100644 mkdocs.yml create mode 100644 mxrun.conf create mode 100755 scripts/maybe-mxrun.sh diff --git a/Cargo.lock b/Cargo.lock index d28e781..cb07d42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,15 +115,24 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] [[package]] name = "arrow-array" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd33d3e92f207444098c75b42de99d329562be0cf686b307b097cc52b4e999e" +checksum = "ae33dad492b7df00a217563a7b0ef2874df68a0deea1b1a3acf628152f7f7a69" dependencies = [ "ahash", "arrow-buffer", @@ -139,9 +148,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6cd424c2693bcdbc150d843dc9d4d137dd2de4782ce6df491ad11a3a0416c0" +checksum = "b9552f96391c005e6ab449fa941420935e7e062489b12b8b1b08879b2163f5b5" dependencies = [ "bytes", "half", @@ -151,9 +160,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c88210023a2bfee1896af366309a3028fc3bcbd6515fa29a7990ee1baa08ee0" +checksum = "2b24852db04738907e06c04ea61e42fe7fda962a34513022dc0d0e754fb7976b" dependencies = [ "arrow-buffer", "arrow-schema", @@ -164,9 +173,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238438f0834483703d88896db6fe5a7138b2230debc31b34c0336c2996e3c64f" +checksum = "29a908a11fcfb3fb2f6730f4ac15e367bc644e419155e96238f68cf3adde572b" dependencies = [ "arrow-array", "arrow-buffer", @@ -178,15 +187,15 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f633dbfdf39c039ada1bf9e34c694816eb71fbb7dc78f613993b7245e078a1ed" +checksum = "21ca356ad6425cecb6eb7b28e4f659f1ee7880fbb1a16127de7dd62901efee9e" [[package]] name = "arrow-select" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd065c54172ac787cf3f2f8d4107e0d3fdc26edba76fdf4f4cc170258942222" +checksum = "c58da39eb3d8350ad4a549e5c2bc49284dac554016c69829310350f1731b0aad" dependencies = [ "ahash", "arrow-array", @@ -204,13 +213,13 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -230,15 +239,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -246,14 +255,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -349,9 +359,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -364,15 +374,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -382,9 +398,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "castaway" @@ -397,14 +413,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.62" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -431,15 +447,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -455,9 +471,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "clap" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -465,9 +481,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -478,14 +494,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -520,9 +536,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -534,9 +550,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.19.0" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" dependencies = [ "cfg-if", "cpufeatures", @@ -606,13 +622,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "crossterm_winapi", "mio", "parking_lot", @@ -628,7 +650,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "crossterm_winapi", "derive_more", "document-features", @@ -695,7 +717,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -706,7 +728,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -720,9 +742,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive_more" @@ -743,7 +762,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -773,9 +792,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "equivalent" @@ -824,6 +843,12 @@ dependencies = [ "regex", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "filedescriptor" version = "0.8.3" @@ -859,7 +884,7 @@ version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "rustc_version", ] @@ -899,24 +924,24 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -925,32 +950,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-macro", @@ -995,22 +1020,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", ] [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1062,6 +1085,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -1086,9 +1114,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1096,9 +1124,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1106,9 +1134,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1131,9 +1159,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -1208,12 +1236,6 @@ dependencies = [ "cc", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1228,8 +1250,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", ] [[package]] @@ -1251,7 +1271,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1283,23 +1303,22 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1311,7 +1330,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1326,17 +1345,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -1381,7 +1394,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", ] [[package]] @@ -1481,7 +1494,7 @@ dependencies = [ "regex", "serde", "serde_json", - "shlex", + "shlex 1.3.0", "xxhash-rust", ] @@ -1509,9 +1522,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "logjet" @@ -1523,17 +1536,17 @@ dependencies = [ "opentelemetry-proto", "parquet", "prost", - "rand 0.9.4", + "rand 0.9.5", "zstd", ] [[package]] name = "lru" -version = "0.16.4" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -1560,9 +1573,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmem" @@ -1603,9 +1616,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -1628,7 +1641,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -1647,9 +1660,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -1666,9 +1679,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -1678,7 +1691,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1731,7 +1744,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -1763,8 +1776,8 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.4", - "thiserror 2.0.18", + "rand 0.9.5", + "thiserror 2.0.19", ] [[package]] @@ -1802,6 +1815,30 @@ dependencies = [ "tonic", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1827,9 +1864,9 @@ dependencies = [ [[package]] name = "parquet" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dafa7d01085b62a47dd0c1829550a0a36710ea9c4fe358a05a85477cec8a908" +checksum = "d298093b2dec60289dce0684c986d0f7679e9dd15771c2c65406e1aaf604a704" dependencies = [ "ahash", "arrow-array", @@ -1883,9 +1920,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" dependencies = [ "memchr", "ucd-trie", @@ -1893,9 +1930,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" dependencies = [ "pest", "pest_generator", @@ -1903,25 +1940,24 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" dependencies = [ "pest", - "sha2", ] [[package]] @@ -1951,7 +1987,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -1964,7 +2000,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1993,7 +2029,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2010,9 +2046,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "powerfmt" @@ -2029,21 +2065,11 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -2054,9 +2080,9 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "num-traits", - "rand 0.9.4", + "rand 0.9.5", "rand_chacha", "rand_xorshift", "regex-syntax", @@ -2065,9 +2091,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -2075,22 +2101,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2109,18 +2135,18 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -2162,33 +2188,37 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.30.0" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ "instability", "ratatui-core", "ratatui-crossterm", "ratatui-macros", + "ratatui-termina", "ratatui-termwiz", "ratatui-widgets", + "serde", ] [[package]] name = "ratatui-core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "compact_str", - "hashbrown 0.16.1", - "indoc", + "critical-section", + "hashbrown 0.17.1", "itertools", "kasuari", "lru", + "palette", + "serde", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -2196,9 +2226,9 @@ dependencies = [ [[package]] name = "ratatui-crossterm" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" dependencies = [ "cfg-if", "crossterm 0.29.0", @@ -2208,19 +2238,30 @@ dependencies = [ [[package]] name = "ratatui-macros" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" dependencies = [ "ratatui-core", "ratatui-widgets", ] [[package]] -name = "ratatui-termwiz" +name = "ratatui-termina" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" dependencies = [ "ratatui-core", "termwiz", @@ -2228,17 +2269,18 @@ dependencies = [ [[package]] name = "ratatui-widgets" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.16.1", + "bitflags 2.13.1", + "hashbrown 0.17.1", "indoc", "instability", "itertools", "line-clipping", "ratatui-core", + "serde", "strum", "time", "unicode-segmentation", @@ -2263,14 +2305,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2280,9 +2322,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -2291,9 +2333,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "ring" @@ -2315,7 +2357,7 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2338,7 +2380,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2351,7 +2393,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -2360,9 +2402,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "log", @@ -2385,9 +2427,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] @@ -2406,9 +2448,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -2436,9 +2478,9 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -2446,29 +2488,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2507,6 +2549,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook" version = "0.3.18" @@ -2540,9 +2588,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "siphasher" @@ -2558,15 +2606,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2586,23 +2634,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2624,9 +2672,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -2639,6 +2698,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.1", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "windows-sys 0.61.2", +] + [[package]] name = "terminal_size" version = "0.4.4" @@ -2678,7 +2750,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.11.1", + "bitflags 2.13.1", "fancy-regex", "filedescriptor", "finl_unicode", @@ -2723,11 +2795,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -2738,18 +2810,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -2765,9 +2837,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "libc", @@ -2780,9 +2852,9 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tiny-keccak" @@ -2807,9 +2879,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -2822,13 +2894,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2843,9 +2915,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -2854,13 +2926,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -2956,7 +3029,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2976,9 +3049,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twox-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" [[package]] name = "typed-arena" @@ -2988,9 +3061,9 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -3012,9 +3085,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" @@ -3033,12 +3106,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3059,12 +3126,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -3107,27 +3174,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3138,9 +3196,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3148,60 +3206,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "wezterm-bidi" version = "0.2.3" @@ -3317,7 +3341,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3328,7 +3352,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3446,105 +3470,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "xxhash-rust" -version = "0.8.15" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" [[package]] name = "yasna" @@ -3557,35 +3493,35 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" diff --git a/Makefile b/Makefile index 8e269c5..90b6755 100644 --- a/Makefile +++ b/Makefile @@ -1,64 +1,170 @@ -.PHONY: build dev devel check fix test test-unit test-integration test-abi-matrix test-exporter-release-smoke setup clean stats arm-devel arm x86-devel x86 setup-arm setup-x86 demo man advisory +.PHONY: help _release release _dev dev _check check _fix fix _test test _test-unit test-unit _test-integration test-integration _test-abi-matrix test-abi-matrix _test-exp-smoke test-exp-smoke _arm arm _arm-devel arm-devel _x86 x86 _x86-devel x86-devel mxrun-init mxrun mxrun-toggle setup clean stats demo man advisory docs docs-serve docs-clean setup-arm setup-x86 -DEFAULT_TARGET := build +C_GROUP := \033[1;38;5;200m +C_ENTRY := \033[1;38;5;253m +C_DESCR := \033[38;5;43m +C_RESET := \033[0m + +MXRUN_BIN ?= mxrun ARM_TARGET ?= aarch64-unknown-linux-musl X86_TARGET ?= x86_64-unknown-linux-musl CORE_WORKSPACE := --workspace --exclude otlp-demo MANPAGE_MD := $(wildcard doc/manpage/*.1.md) MANPAGE_OUT := $(MANPAGE_MD:.md=) -build: setup +.DEFAULT_GOAL := help + +help: + @printf "$(C_GROUP)Usage: make \n\n$(C_RESET)" + @printf "$(C_GROUP)Build$(C_RESET)\n" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "release" "Build release binaries" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "dev" "Build debug binaries (verbose)" + @printf "\n" + @printf "$(C_GROUP)Code quality$(C_RESET)\n" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "check" "Run clippy (strict)" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "fix" "Auto-fix clippy warnings" + @printf "\n" + @printf "$(C_GROUP)Test$(C_RESET)\n" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "test" "Run full test suite" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "test-unit" "Run unit tests" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "test-integration" "Run integration tests" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "test-abi-matrix" "Test exporter ABI compatibility" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "test-exp-smoke" "Smoke test exporter plugins" + @printf "\n" + @printf "$(C_GROUP)Cross-compile$(C_RESET)\n" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "arm" "Build release for arm64-musl" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "arm-devel" "Build debug for arm64-musl" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "x86" "Build release for x86_64-musl" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "x86-devel" "Build debug for x86_64-musl" + @printf "\n" + @printf "$(C_GROUP)mxrun$(C_RESET)\n" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "mxrun-init" "Initialize mxrun config" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "mxrun" "Show mxrun status" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "mxrun-toggle" "Toggle local/remote builds" + @printf "\n" + @printf "$(C_GROUP)Docs$(C_RESET)\n" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "docs" "Build docs site (mkdocs)" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "docs-serve" "Serve docs at localhost:8000" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "docs-clean" "Remove docs output" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "man" "Build manpages" + @printf "\n" + @printf "$(C_GROUP)Misc$(C_RESET)\n" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "demo" "Build all demo binaries" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "advisory" "Run cargo-audit" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "clean" "Remove build artifacts" + @printf " $(C_ENTRY)%-24s$(C_RESET) $(C_DESCR)%s$(C_RESET)\n" "stats" "Show lines of code" + +_release: cargo build $(CORE_WORKSPACE) --release -dev: setup +release: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _release; else scripts/maybe-mxrun.sh release || $(MAKE) _release; fi + +_dev: cargo build --verbose $(CORE_WORKSPACE) -devel: dev +dev: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _dev; else scripts/maybe-mxrun.sh dev || $(MAKE) _dev; fi -check: setup +_check: cargo clippy --workspace --all-targets --all-features -- -D warnings -fix: setup +check: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _check; else scripts/maybe-mxrun.sh check || $(MAKE) _check; fi + +_fix: cargo clippy --workspace --fix --all-targets --all-features --allow-dirty --allow-staged -- -D warnings -test: setup +fix: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _fix; else scripts/maybe-mxrun.sh fix || $(MAKE) _fix; fi + +_test: cargo build -p ljd -p ljx -p ljx-parquet-exporter cargo build -p otlp-demo --bin otlp-bofh-emitter cargo nextest run $(CORE_WORKSPACE) -test-unit: setup +test: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _test; else scripts/maybe-mxrun.sh test || $(MAKE) _test; fi + +_test-unit: cargo build -p ljx-parquet-exporter -p lj-perfetto-ingest cargo nextest run -p logjet --lib -p ljd --bins -p ljx --bin ljx -p lj-perfetto-ingest -test-integration: setup +test-unit: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _test-unit; else scripts/maybe-mxrun.sh test-unit || $(MAKE) _test-unit; fi + +_test-integration: cargo build -p ljd -p ljx -p ljx-parquet-exporter cargo build -p otlp-demo --bin otlp-bofh-emitter cargo nextest run -p ljd --test bridge_flows cargo nextest run -p logjet --test ljx_cli cargo nextest run -p logjet --test ljx_export -test-abi-matrix: setup +test-integration: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _test-integration; else scripts/maybe-mxrun.sh test-integration || $(MAKE) _test-integration; fi + +_test-abi-matrix: bash scripts/test-exporter-abi-matrix.sh -test-exporter-release-smoke: setup +test-abi-matrix: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _test-abi-matrix; else scripts/maybe-mxrun.sh test-abi-matrix || $(MAKE) _test-abi-matrix; fi + +_test-exp-smoke: HOST_TOOLCHAIN=$$(awk -F'"' '/channel = / { print $$2; exit }' rust-toolchain.toml) \ PLUGIN_TOOLCHAIN=$$(awk -F'"' '/channel = / { print $$2; exit }' rust-toolchain.toml) \ PROFILE=release \ bash scripts/test-exporter-abi-matrix.sh -arm-devel: setup setup-arm - cargo build $(CORE_WORKSPACE) --target $(ARM_TARGET) +test-exp-smoke: setup + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _test-exp-smoke; else scripts/maybe-mxrun.sh test-exp-smoke || $(MAKE) _test-exp-smoke; fi -arm: setup setup-arm +_arm: cargo build $(CORE_WORKSPACE) --release --target $(ARM_TARGET) -x86-devel: setup setup-x86 - cargo build $(CORE_WORKSPACE) --target $(X86_TARGET) +arm: setup setup-arm + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _arm; else scripts/maybe-mxrun.sh arm || $(MAKE) _arm; fi -x86: setup setup-x86 +_arm-devel: + cargo build $(CORE_WORKSPACE) --target $(ARM_TARGET) + +arm-devel: setup setup-arm + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _arm-devel; else scripts/maybe-mxrun.sh arm-devel || $(MAKE) _arm-devel; fi + +_x86: cargo build $(CORE_WORKSPACE) --release --target $(X86_TARGET) -demo: devel +x86: setup setup-x86 + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _x86; else scripts/maybe-mxrun.sh x86 || $(MAKE) _x86; fi + +_x86-devel: + cargo build $(CORE_WORKSPACE) --target $(X86_TARGET) + +x86-devel: setup setup-x86 + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _x86-devel; else scripts/maybe-mxrun.sh x86-devel || $(MAKE) _x86-devel; fi + +mxrun-init: setup + @command -v mxrun >/dev/null 2>&1 || { echo "mxrun not installed. Run: make setup" >&2; exit 1; } + @if [ ! -f mxrun.conf ]; then echo "local" > mxrun.conf; fi + @printf 'active=yes\n' > .mxrun-env + @MXRUN_CONFIG=mxrun.conf mxrun init || true + +mxrun: + @if [ -f .mxrun-env ] && grep -q '^active=yes' .mxrun-env; then \ + echo "mxrun: enabled (distributed builds)"; \ + elif [ -f .mxrun-env ]; then \ + echo "mxrun: disabled (local builds)"; \ + else \ + echo "mxrun: unconfigured — run make mxrun-init"; \ + fi + +mxrun-toggle: + @if [ -f .mxrun-env ] && grep -q '^active=yes' .mxrun-env; then \ + printf 'active=no\n' > .mxrun-env && echo "mxrun: disabled (local builds)"; \ + else \ + printf 'active=yes\n' > .mxrun-env && echo "mxrun: enabled (distributed builds)"; \ + fi + +demo: dev cargo build -p otlp-demo cargo build -p otlp-demo --bin traces-emitter cargo build -p otlp-demo --bin traces-grpc-emitter @@ -67,6 +173,16 @@ demo: devel cargo build -p lj-syslog-ingest cargo build -p lj-logcat-ingest cargo build -p lj-stress-ingest + +docs: + mkdocs build --clean + +docs-serve: + mkdocs serve + +docs-clean: + rm -rf site/ + man: $(MANPAGE_OUT) $(MANPAGE_OUT): doc/manpage/%.1: doc/manpage/%.1.md diff --git a/README.md b/README.md index 95bab8f..2d31fc7 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ The demos are useful as small reference systems. They show file-backed retention ## Build And Explore -From the project root, `make` builds the main release binaries. `make demo` builds the demo artefacts. `make test` runs the full test path, and `make check` runs clippy through the Makefile. Readers who prefer orientation before compilation should begin with [doc/README.md](./doc/README.md), then continue to [doc/overview.md](./doc/overview.md) for the system shape, [doc/configuration.md](./doc/configuration.md) for the YAML configuration surface, [doc/features.md](./doc/features.md) for the daemon feature set, and [doc/c-cpp-integration.md](./doc/c-cpp-integration.md) for the shared-library boundary. Standalone Rust examples remain in [examples](./examples), and the scenario-driven material remains in [demo](./demo). +From the project root, `make` builds the main release binaries. `make demo` builds the demo artefacts. `make test` runs the full test path, and `make check` runs clippy through the Makefile. Readers who prefer orientation before compilation should begin with [doc/index.md](./doc/index.md), then continue to [doc/overview.md](./doc/overview.md) for the system shape, [doc/configuration.md](./doc/configuration.md) for the YAML configuration surface, [doc/features.md](./doc/features.md) for the daemon feature set, and [doc/c-cpp-integration.md](./doc/c-cpp-integration.md) for the shared-library boundary. Standalone Rust examples remain in [examples](./examples), and the scenario-driven material remains in [demo](./demo). ## Final Word diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..177d0c2 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,39 @@ +site_name: Logjet +site_description: Append-only OTLP-native log persistence +repo_url: https://github.com/tinythings/logjet +theme: + name: material + features: + - navigation.instant + - navigation.tracking + - toc.follow +markdown_extensions: + - admonition + - tables + - toc: + permalink: true + - attr_list + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.inlinehilite + - pymdownx.snippets +docs_dir: doc/ +exclude_docs: | + manpage/ljd.1 + manpage/ljx.1 +nav: + - Home: index.md + - Overview: overview.md + - Guides: + - ljx Toolbox: ljx.md + - CEL Querying: cel.md + - Daemon: daemon.md + - Perfetto Ingest: perfetto-ingest.md + - Parquet Export: parquet/export-parquet.md + - Reference: + - Configuration: configuration.md + - Protocol: protocol.md + - Config Samples: config-samples.md + - C/C++ Integration: c-cpp-integration.md + - Features: features.md diff --git a/mxrun.conf b/mxrun.conf new file mode 100644 index 0000000..4083037 --- /dev/null +++ b/mxrun.conf @@ -0,0 +1 @@ +local diff --git a/scripts/maybe-mxrun.sh b/scripts/maybe-mxrun.sh new file mode 100755 index 0000000..53592b2 --- /dev/null +++ b/scripts/maybe-mxrun.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env sh +set -eu + +ENTRY="${1:-}" + +# mxrun blanks MXRUN_LOCAL_MAKE when calling back — run local build. +[ "${MXRUN_LOCAL_MAKE+set}" = "set" ] && exit 1 + +ACTIVE=no +[ -f .mxrun-env ] && ACTIVE=$(awk -F= '/^active=/ {print $2}' .mxrun-env 2>/dev/null) + +if [ "$ACTIVE" = "yes" ] && [ -f mxrun.conf ]; then + command -v mxrun >/dev/null 2>&1 || { echo "mxrun not installed. Run: cargo install mxrun" >&2; exit 1; } + MXRUN_CONFIG=mxrun.conf mxrun run "$ENTRY" || true + exit 0 +fi + +exit 1 diff --git a/scripts/setup-rust.sh b/scripts/setup-rust.sh index 401a6f4..d2d59df 100644 --- a/scripts/setup-rust.sh +++ b/scripts/setup-rust.sh @@ -164,4 +164,7 @@ ensure_cc echo "Installing cargo-nextest..." cargo install cargo-nextest --locked +echo "Installing mxrun..." +cargo install mxrun || true + echo "Setup complete." From c9514e3dc3bcd4eb09bc974dec88c4695a9475af Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:07:15 +0200 Subject: [PATCH 10/22] Enable mxrun by default --- .mxrun-env | 1 + 1 file changed, 1 insertion(+) create mode 100644 .mxrun-env diff --git a/.mxrun-env b/.mxrun-env new file mode 100644 index 0000000..a62a4f7 --- /dev/null +++ b/.mxrun-env @@ -0,0 +1 @@ +active=yes From fe9d2e09cdc8ba365dd49dcfd4abcd0cd6f9bca8 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:07:27 +0200 Subject: [PATCH 11/22] Run mxrun with labels --- scripts/maybe-mxrun.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/maybe-mxrun.sh b/scripts/maybe-mxrun.sh index 53592b2..cc4568b 100755 --- a/scripts/maybe-mxrun.sh +++ b/scripts/maybe-mxrun.sh @@ -11,7 +11,23 @@ ACTIVE=no if [ "$ACTIVE" = "yes" ] && [ -f mxrun.conf ]; then command -v mxrun >/dev/null 2>&1 || { echo "mxrun not installed. Run: cargo install mxrun" >&2; exit 1; } - MXRUN_CONFIG=mxrun.conf mxrun run "$ENTRY" || true + case "$ENTRY" in + release) LABEL="Release Build" ;; + dev) LABEL="Development Build" ;; + check) LABEL="Clippy Check" ;; + fix) LABEL="Clippy Fix" ;; + test) LABEL="Full Test Suite" ;; + test-unit) LABEL="Unit Tests" ;; + test-integration) LABEL="Integration Tests" ;; + test-abi-matrix) LABEL="ABI Matrix Tests" ;; + test-exp-smoke) LABEL="Exporter Smoke Tests" ;; + arm) LABEL="ARM Release Build" ;; + arm-devel) LABEL="ARM Development Build" ;; + x86) LABEL="x86 Release Build" ;; + x86-devel) LABEL="x86 Development Build" ;; + *) LABEL="$ENTRY" ;; + esac + MXRUN_CONFIG=mxrun.conf mxrun run --label="$LABEL" "$ENTRY" || true exit 0 fi From 84b7f3bbf9f8f2dff8a2a2ff5d83ed29238a06ee Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:32:38 +0200 Subject: [PATCH 12/22] Update opentelemetry to address CVEs --- Cargo.lock | 68 ++++++++++++++++--- Cargo.toml | 2 +- ljcel/tests/unit/ljcel_ut.rs | 6 +- ljx/Cargo.toml | 2 +- ljx/src/commands/export.rs | 2 + ljx/src/commands/view/detail.rs | 2 + ljx/src/dedup/emit.rs | 12 +++- ljx/tests/unit/commands/discover_ut.rs | 1 + ljx/tests/unit/commands/export_ut.rs | 8 ++- ljx/tests/unit/commands/top_level_query_ut.rs | 1 + ljx/tests/unit/commands/view_ut.rs | 18 +++-- ljx/tests/unit/dataset_index_ut.rs | 1 + ljx/tests/unit/dedup/dedup_utst.rs | 1 + ljx/tests/unit/predicate_ut.rs | 1 + tests/ljx_export.rs | 6 +- 15 files changed, 106 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb07d42..d247155 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1365,7 +1365,7 @@ dependencies = [ name = "liblogjet" version = "0.9.0" dependencies = [ - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "prost", "tokio", "tonic", @@ -1423,7 +1423,7 @@ version = "0.9.0" name = "lj-perfetto-ingest" version = "0.9.0" dependencies = [ - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "prost", "rusqlite", "serde_json", @@ -1443,7 +1443,7 @@ version = "0.9.0" dependencies = [ "cel", "logjet", - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "prost", ] @@ -1463,7 +1463,7 @@ dependencies = [ "liblogjet", "logjet", "lz4_flex", - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "prost", "rcgen", "rustls", @@ -1488,7 +1488,7 @@ dependencies = [ "liblogjet", "ljcel", "logjet", - "opentelemetry-proto", + "opentelemetry-proto 0.32.0", "prost", "ratatui", "regex", @@ -1505,7 +1505,7 @@ dependencies = [ "arrow-array", "arrow-schema", "liblogjet", - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "parquet", "prost", "serde_json", @@ -1533,7 +1533,7 @@ dependencies = [ "arrow-array", "crc32c", "lz4_flex", - "opentelemetry-proto", + "opentelemetry-proto 0.32.0", "parquet", "prost", "rand 0.9.5", @@ -1748,6 +1748,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.19", + "tracing", +] + [[package]] name = "opentelemetry-proto" version = "0.31.0" @@ -1756,8 +1770,8 @@ checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ "base64", "const-hex", - "opentelemetry", - "opentelemetry_sdk", + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", "prost", "serde", "serde_json", @@ -1765,6 +1779,22 @@ dependencies = [ "tonic-prost", ] +[[package]] +name = "opentelemetry-proto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" +dependencies = [ + "base64", + "const-hex", + "opentelemetry 0.32.0", + "opentelemetry_sdk 0.32.1", + "prost", + "serde", + "tonic", + "tonic-prost", +] + [[package]] name = "opentelemetry_sdk" version = "0.31.0" @@ -1774,12 +1804,28 @@ dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry", + "opentelemetry 0.31.0", "percent-encoding", "rand 0.9.5", "thiserror 2.0.19", ] +[[package]] +name = "opentelemetry_sdk" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry 0.32.0", + "percent-encoding", + "portable-atomic", + "rand 0.9.5", + "thiserror 2.0.19", +] + [[package]] name = "ordered-float" version = "2.10.1" @@ -1806,7 +1852,7 @@ dependencies = [ "liblogjet", "logjet", "lz4_flex", - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "prost", "rustls", "rustls-pemfile", diff --git a/Cargo.toml b/Cargo.toml index 08eb182..0fb31d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ zstd = { version = "0.13", default-features = false } [dev-dependencies] arrow-array = "58.0.0" -opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "logs"] } +opentelemetry-proto = { version = "0.32", features = ["gen-tonic", "logs"] } parquet = { version = "58.0.0", default-features = false, features = ["arrow", "zstd"] } prost = "0.14" rand = "0.9.3" diff --git a/ljcel/tests/unit/ljcel_ut.rs b/ljcel/tests/unit/ljcel_ut.rs index 135aed7..32e0fef 100644 --- a/ljcel/tests/unit/ljcel_ut.rs +++ b/ljcel/tests/unit/ljcel_ut.rs @@ -48,11 +48,11 @@ fn make_log_payload_with_context( } fn string_attr(key: &str, value: &str) -> KeyValue { - KeyValue { key: key.to_string(), value: Some(AnyValue { value: Some(Value::StringValue(value.to_string())) }) } + KeyValue { key: key.to_string(), value: Some(AnyValue { value: Some(Value::StringValue(value.to_string())) }), ..Default::default() } } fn int_attr(key: &str, value: i64) -> KeyValue { - KeyValue { key: key.to_string(), value: Some(AnyValue { value: Some(Value::IntValue(value)) }) } + KeyValue { key: key.to_string(), value: Some(AnyValue { value: Some(Value::IntValue(value)) }), ..Default::default() } } fn log_record(body: &str, severity_number: i32, severity_text: &str) -> LogRecord { @@ -235,6 +235,7 @@ fn make_metric_payload(metric_name: &str, metric_type: &str, value: f64) -> Vec< attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("test-svc".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), @@ -295,6 +296,7 @@ fn make_trace_payload(span_name: &str, kind: i32, status_code: i32) -> Vec { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("test-svc".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), diff --git a/ljx/Cargo.toml b/ljx/Cargo.toml index 6e255b5..ccdc036 100644 --- a/ljx/Cargo.toml +++ b/ljx/Cargo.toml @@ -13,7 +13,7 @@ libloading = "0.8" liblogjet = { path = "../liblogjet" } ljcel = { path = "../ljcel" } logjet = { path = ".." } -opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "logs", "metrics", "trace"] } +opentelemetry-proto = { version = "0.32", features = ["gen-tonic", "logs", "metrics", "trace"] } prost = "0.14" ratatui = "0.30" regex = "1.12" diff --git a/ljx/src/commands/export.rs b/ljx/src/commands/export.rs index 2abb243..825673f 100644 --- a/ljx/src/commands/export.rs +++ b/ljx/src/commands/export.rs @@ -493,6 +493,7 @@ fn flatten_otlp_attrs_into_json( fn any_value_to_json(value: &AnyValue, preview_bytes: Option) -> Option { match &value.value { Some(Value::StringValue(text)) => Some(JsonValue::String(truncate_preview(text, preview_bytes))), + Some(Value::StringValueStrindex(_)) => Some(JsonValue::Null), Some(Value::BoolValue(flag)) => Some(JsonValue::Bool(*flag)), Some(Value::IntValue(number)) => Some(JsonValue::Number((*number).into())), Some(Value::DoubleValue(number)) => serde_json::Number::from_f64(*number).map(JsonValue::Number), @@ -521,6 +522,7 @@ fn format_any_value(value: Option<&AnyValue>) -> String { }; match &value.value { Some(Value::StringValue(text)) => text.clone(), + Some(Value::StringValueStrindex(_)) => "".to_string(), Some(Value::BoolValue(flag)) => flag.to_string(), Some(Value::IntValue(number)) => number.to_string(), Some(Value::DoubleValue(number)) => number.to_string(), diff --git a/ljx/src/commands/view/detail.rs b/ljx/src/commands/view/detail.rs index 74e3566..6b0a279 100644 --- a/ljx/src/commands/view/detail.rs +++ b/ljx/src/commands/view/detail.rs @@ -1135,6 +1135,7 @@ fn flatten_otlp_attrs_into_json(target: &mut JsonMap, attrs: fn any_value_to_json(value: &AnyValue) -> Option { match &value.value { Some(Value::StringValue(text)) => Some(JsonValue::String(text.clone())), + Some(Value::StringValueStrindex(_)) => None, Some(Value::BoolValue(flag)) => Some(JsonValue::Bool(*flag)), Some(Value::IntValue(number)) => Some(JsonValue::Number((*number).into())), Some(Value::DoubleValue(number)) => serde_json::Number::from_f64(*number).map(JsonValue::Number), @@ -1233,6 +1234,7 @@ fn format_any_value(value: Option<&AnyValue>) -> String { }; match &value.value { Some(Value::StringValue(text)) => text.clone(), + Some(Value::StringValueStrindex(_)) => "".to_string(), Some(Value::BoolValue(flag)) => flag.to_string(), Some(Value::IntValue(number)) => number.to_string(), Some(Value::DoubleValue(number)) => number.to_string(), diff --git a/ljx/src/dedup/emit.rs b/ljx/src/dedup/emit.rs index 007dc10..8bd6cb7 100644 --- a/ljx/src/dedup/emit.rs +++ b/ljx/src/dedup/emit.rs @@ -142,9 +142,17 @@ fn dedup_metadata(mode_label: &str, used_drain3: bool) -> DedupMetadata<'_> { } fn push_attr_str(attrs: &mut Vec, key: &str, val: &str) { - attrs.push(KeyValue { key: key.to_string(), value: Some(AnyValue { value: Some(Value::StringValue(val.to_string())) }) }); + attrs.push(KeyValue { + key: key.to_string(), + value: Some(AnyValue { value: Some(Value::StringValue(val.to_string())) }), + ..Default::default() + }); } fn push_attr_i64(attrs: &mut Vec, key: &str, val: i64) { - attrs.push(KeyValue { key: key.to_string(), value: Some(AnyValue { value: Some(Value::IntValue(val)) }) }); + attrs.push(KeyValue { + key: key.to_string(), + value: Some(AnyValue { value: Some(Value::IntValue(val)) }), + ..Default::default() + }); } diff --git a/ljx/tests/unit/commands/discover_ut.rs b/ljx/tests/unit/commands/discover_ut.rs index 2d67566..5b79aa1 100644 --- a/ljx/tests/unit/commands/discover_ut.rs +++ b/ljx/tests/unit/commands/discover_ut.rs @@ -24,6 +24,7 @@ fn write_logjet(path: &std::path::Path, rows: &[(u64, u64, &str, &str, &str)]) { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue((*service).to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), diff --git a/ljx/tests/unit/commands/export_ut.rs b/ljx/tests/unit/commands/export_ut.rs index 7fcf394..5933313 100644 --- a/ljx/tests/unit/commands/export_ut.rs +++ b/ljx/tests/unit/commands/export_ut.rs @@ -48,6 +48,7 @@ fn export_ndjson_objects_includes_core_otlp_log_fields() { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("demo-svc".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), @@ -101,6 +102,7 @@ fn export_ndjson_objects_with_preview_truncates_long_fields() { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("demo-svc".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), @@ -121,6 +123,7 @@ fn export_ndjson_objects_with_preview_truncates_long_fields() { attributes: vec![KeyValue { key: "http.target".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("/api/v1/very/long/path".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, flags: 3, @@ -149,7 +152,7 @@ fn export_ndjson_objects_includes_otlp_metrics_fields() { unit: "%".to_string(), data: Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Gauge(Gauge { data_points: vec![NumberDataPoint { - attributes: vec![KeyValue { key: "cpu".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("all".to_string())) }) }], + attributes: vec![KeyValue { key: "cpu".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("all".to_string())) }), ..Default::default() }], start_time_unix_nano: 0, time_unix_nano: 1_700_000_000_000_000_000, value: Some(DataPointValue::AsDouble(45.5)), @@ -165,6 +168,7 @@ fn export_ndjson_objects_includes_otlp_metrics_fields() { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("metrics-svc".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: vec![], @@ -205,6 +209,7 @@ fn export_ndjson_objects_includes_otlp_traces_fields() { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("traces-svc".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: vec![], @@ -227,6 +232,7 @@ fn export_ndjson_objects_includes_otlp_traces_fields() { attributes: vec![KeyValue { key: "http.method".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("GET".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, events: vec![], diff --git a/ljx/tests/unit/commands/top_level_query_ut.rs b/ljx/tests/unit/commands/top_level_query_ut.rs index 4d074c8..10cd9c5 100644 --- a/ljx/tests/unit/commands/top_level_query_ut.rs +++ b/ljx/tests/unit/commands/top_level_query_ut.rs @@ -23,6 +23,7 @@ fn write_logjet(path: &std::path::Path, rows: &[(u64, u64, &str, &str, &str)]) { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue((*service).to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), diff --git a/ljx/tests/unit/commands/view_ut.rs b/ljx/tests/unit/commands/view_ut.rs index 62be42a..2bd0104 100644 --- a/ljx/tests/unit/commands/view_ut.rs +++ b/ljx/tests/unit/commands/view_ut.rs @@ -101,6 +101,7 @@ fn modal_info_lists_otlp_attributes() { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("cpp-appliance".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), @@ -119,6 +120,7 @@ fn modal_info_lists_otlp_attributes() { ], })), }), + ..Default::default() }], dropped_attributes_count: 0, }), @@ -131,6 +133,7 @@ fn modal_info_lists_otlp_attributes() { attributes: vec![KeyValue { key: "character".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("Bender".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, flags: 0, @@ -166,12 +169,13 @@ fn modal_info_lists_otlp_attributes() { #[test] fn modal_info_caps_attribute_entries_per_kind() { let record_attributes = (0..40) - .map(|index| KeyValue { key: format!("custom.{index}"), value: Some(AnyValue { value: Some(Value::StringValue(format!("value-{index}"))) }) }) + .map(|index| KeyValue { key: format!("custom.{index}"), value: Some(AnyValue { value: Some(Value::StringValue(format!("value-{index}"))) }), ..Default::default() }) .collect::>(); let scope_attributes = (0..40) .map(|index| KeyValue { key: format!("scope.custom.{index}"), value: Some(AnyValue { value: Some(Value::StringValue(format!("scope-value-{index}"))) }), + ..Default::default() }) .collect::>(); let batch = ExportLogsServiceRequest { @@ -272,6 +276,7 @@ fn write_test_logjet(path: &std::path::Path, bodies: &[&str]) { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("fake-service".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), @@ -326,21 +331,23 @@ fn write_test_logjet_rows(path: &std::path::Path, rows: &[(&str, &[&str], &str)] attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("fake-service".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), }), scope_logs: vec![ScopeLogs { scope: Some(InstrumentationScope { - name: "fake-scope".to_string(), - version: String::new(), - attributes: vec![KeyValue { + name: "fake-scope".to_string(), + version: String::new(), + attributes: vec![KeyValue { key: "demo.channel".to_string(), value: Some(AnyValue { value: Some(Value::ArrayValue(opentelemetry_proto::tonic::common::v1::ArrayValue { values: channel.iter().map(|part| AnyValue { value: Some(Value::StringValue((*part).to_string())) }).collect(), })), }), + ..Default::default() }], dropped_attributes_count: 0, }), @@ -382,6 +389,7 @@ fn write_test_logjet_records(path: &std::path::Path, rows: &[(u64, u64, &str)]) attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("fake-service".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), @@ -1037,6 +1045,7 @@ fn summary_decodes_otlp_traces_payload() { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("trace-demo".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: vec![], @@ -1169,6 +1178,7 @@ fn modal_info_entries_decodes_otlp_traces_payload() { attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue("trace-demo".to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: vec![], diff --git a/ljx/tests/unit/dataset_index_ut.rs b/ljx/tests/unit/dataset_index_ut.rs index 00462b7..9a46d81 100644 --- a/ljx/tests/unit/dataset_index_ut.rs +++ b/ljx/tests/unit/dataset_index_ut.rs @@ -23,6 +23,7 @@ fn write_star_wars_logjet(path: &std::path::Path, rows: &[(u64, u64, &str, &str, attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue((*service).to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), diff --git a/ljx/tests/unit/dedup/dedup_utst.rs b/ljx/tests/unit/dedup/dedup_utst.rs index 9edfb14..62c77b6 100644 --- a/ljx/tests/unit/dedup/dedup_utst.rs +++ b/ljx/tests/unit/dedup/dedup_utst.rs @@ -36,6 +36,7 @@ fn make_batch(service: &str, records: Vec) -> ExportLogsServiceReques attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue(service.to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), diff --git a/ljx/tests/unit/predicate_ut.rs b/ljx/tests/unit/predicate_ut.rs index f554e63..cd258e6 100644 --- a/ljx/tests/unit/predicate_ut.rs +++ b/ljx/tests/unit/predicate_ut.rs @@ -19,6 +19,7 @@ fn make_cel_test_payload(body: &str, severity_number: i32, severity_text: &str, attributes: vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue(service_name.to_string())) }), + ..Default::default() }], dropped_attributes_count: 0, entity_refs: Vec::new(), diff --git a/tests/ljx_export.rs b/tests/ljx_export.rs index 911e0fa..754ab28 100644 --- a/tests/ljx_export.rs +++ b/tests/ljx_export.rs @@ -351,7 +351,7 @@ fn encode_logs_request(message: &str, service_name: Option<&str>) -> io::Result< resource: Some(opentelemetry_proto::tonic::resource::v1::Resource { attributes: service_name .map(|name| { - vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue(name.to_string())) }) }] + vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue(name.to_string())) }), ..Default::default() }] }) .unwrap_or_default(), dropped_attributes_count: 0, @@ -383,7 +383,7 @@ fn encode_metrics_request(service_name: Option<&str>) -> io::Result> { let resource = opentelemetry_proto::tonic::resource::v1::Resource { attributes: service_name .map(|name| { - vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue(name.to_string())) }) }] + vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue(name.to_string())) }), ..Default::default() }] }) .unwrap_or_default(), dropped_attributes_count: 0, @@ -462,7 +462,7 @@ fn encode_traces_request(service_name: Option<&str>) -> io::Result> { let resource = opentelemetry_proto::tonic::resource::v1::Resource { attributes: service_name .map(|name| { - vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue(name.to_string())) }) }] + vec![KeyValue { key: "service.name".to_string(), value: Some(AnyValue { value: Some(Value::StringValue(name.to_string())) }), ..Default::default() }] }) .unwrap_or_default(), dropped_attributes_count: 0, From 124cefdb7ea711885c110c93864f68162e7afeb0 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:32:58 +0200 Subject: [PATCH 13/22] Shut up clippy on needless updates --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 90b6755..32d152f 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ dev: setup @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _dev; else scripts/maybe-mxrun.sh dev || $(MAKE) _dev; fi _check: - cargo clippy --workspace --all-targets --all-features -- -D warnings + cargo clippy --workspace --all-targets --all-features -- -D warnings -A clippy::needless-update check: setup @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _check; else scripts/maybe-mxrun.sh check || $(MAKE) _check; fi From b7edac2c416a776c2402122875286233675ed181 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:36:18 +0200 Subject: [PATCH 14/22] Fix gh action --- .github/workflows/it-is-alive.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/it-is-alive.yml b/.github/workflows/it-is-alive.yml index 3184087..248bfb5 100644 --- a/.github/workflows/it-is-alive.yml +++ b/.github/workflows/it-is-alive.yml @@ -21,4 +21,4 @@ jobs: run: make - name: Smoke test release exporter loading - run: make test-exporter-release-smoke + run: make test-exp-smoke From fa7f32b61df975353d2f4a6d74fdd53c119923fc Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:42:45 +0200 Subject: [PATCH 15/22] Update docs style --- doc/stylesheets/extra.css | 23 +++++++++++++++++++++++ mkdocs.yml | 17 +++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 doc/stylesheets/extra.css diff --git a/doc/stylesheets/extra.css b/doc/stylesheets/extra.css new file mode 100644 index 0000000..e7d0a00 --- /dev/null +++ b/doc/stylesheets/extra.css @@ -0,0 +1,23 @@ +.md-nav__item--section > .md-nav__link { + color: #900000; +} + +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #900000; + --md-primary-fg-color--light: #c62828; + --md-primary-fg-color--dark: #5a0000; + --md-default-bg-color: #1a1c1d; + --md-default-bg-color--light: #222425; + --md-default-bg-color--lighter: #2a2c2d; + --md-default-bg-color--lightest: #323435; + --md-code-bg-color: #1e2021; + --md-code-fg-color: #d5d5d5; + --md-footer-bg-color: #111213; + --md-footer-bg-color--dark: #0a0b0c; +} + +[data-md-color-scheme="default"] { + --md-primary-fg-color: #900000; + --md-primary-fg-color--light: #c62828; + --md-primary-fg-color--dark: #5a0000; +} diff --git a/mkdocs.yml b/mkdocs.yml index 177d0c2..c64c7ce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,10 +3,27 @@ site_description: Append-only OTLP-native log persistence repo_url: https://github.com/tinythings/logjet theme: name: material + palette: + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: red + toggle: + icon: material/brightness-4 + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: red + toggle: + icon: material/brightness-7 + name: Switch to dark mode features: - navigation.instant - navigation.tracking - toc.follow +extra_css: + - stylesheets/extra.css markdown_extensions: - admonition - tables From 85a5ad2c06abf78721a1289bd67d24f251b7a2c7 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:44:33 +0200 Subject: [PATCH 16/22] Bump version 0.11.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0fb31d7..fc98e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ ] [workspace.package] -version = "0.9.0" +version = "0.11.0" edition = "2024" license = "Apache-2.0" default-members = [ From 9fdb037b9f13934b411fc6881d92bdc81bd55b3c Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:44:43 +0200 Subject: [PATCH 17/22] Update rust-toolchain.toml to Rust 1.97.1 --- rust-toolchain.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 95e32ea..7243604 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,2 @@ [toolchain] -channel = "1.94.0" -components = ["clippy"] +channel = "1.97.1" From 0bde8573798da3b16027bc51b37ef96da1269b24 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:45:41 +0200 Subject: [PATCH 18/22] Update deps --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d247155..4661116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1363,7 +1363,7 @@ dependencies = [ [[package]] name = "liblogjet" -version = "0.9.0" +version = "0.11.0" dependencies = [ "opentelemetry-proto 0.31.0", "prost", @@ -1417,11 +1417,11 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lj-logcat-ingest" -version = "0.9.0" +version = "0.11.0" [[package]] name = "lj-perfetto-ingest" -version = "0.9.0" +version = "0.11.0" dependencies = [ "opentelemetry-proto 0.31.0", "prost", @@ -1431,15 +1431,15 @@ dependencies = [ [[package]] name = "lj-stress-ingest" -version = "0.9.0" +version = "0.11.0" [[package]] name = "lj-syslog-ingest" -version = "0.9.0" +version = "0.11.0" [[package]] name = "ljcel" -version = "0.9.0" +version = "0.11.0" dependencies = [ "cel", "logjet", @@ -1449,7 +1449,7 @@ dependencies = [ [[package]] name = "ljd" -version = "0.9.0" +version = "0.11.0" dependencies = [ "clap", "colored", @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "ljx" -version = "0.9.0" +version = "0.11.0" dependencies = [ "chrono", "clap", @@ -1500,7 +1500,7 @@ dependencies = [ [[package]] name = "ljx-parquet-exporter" -version = "0.9.0" +version = "0.11.0" dependencies = [ "arrow-array", "arrow-schema", @@ -1528,7 +1528,7 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "logjet" -version = "0.9.0" +version = "0.11.0" dependencies = [ "arrow-array", "crc32c", @@ -1846,7 +1846,7 @@ dependencies = [ [[package]] name = "otlp-demo" -version = "0.9.0" +version = "0.11.0" dependencies = [ "colored", "liblogjet", From a5c7aa74973137a2af74deccacb80ce1618133e8 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:46:31 +0200 Subject: [PATCH 19/22] Update badges --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d31fc7..f15eeec 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # logjet +[![Release](https://img.shields.io/github/v/release/tinythings/logjet)](https://github.com/tinythings/logjet/releases) +[![Rust](https://img.shields.io/badge/rust-2024%20edition-orange)](https://doc.rust-lang.org/edition-guide/) [![It is alive!](https://github.com/tinythings/logjet/actions/workflows/it-is-alive.yml/badge.svg)](https://github.com/tinythings/logjet/actions/workflows/it-is-alive.yml) [![Unit Tests](https://github.com/tinythings/logjet/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/tinythings/logjet/actions/workflows/unit-tests.yml) [![Integration Tests](https://github.com/tinythings/logjet/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/tinythings/logjet/actions/workflows/integration-tests.yml) [![Insanity Check](https://github.com/tinythings/logjet/actions/workflows/insanity-check.yml/badge.svg)](https://github.com/tinythings/logjet/actions/workflows/insanity-check.yml) -[![Licence: Apache-2.0](https://img.shields.io/badge/licence-Apache--2.0-blue.svg)](./LICENSE) +[![Licence](https://img.shields.io/badge/licence-Apache--2.0-blue.svg)](./LICENSE) `logjet` is a block-oriented binary format and Rust library for storing raw OTLP protobuf batches as an append-only stream. The project is aimed at telemetry relay, local backlog retention, and later replay on systems where sequential I/O is far easier to afford than elaborate indexing, background compaction, or large in-memory state. It is also intended for the less well-behaved parts of real deployments: links that are intermittent, slow, lossy, or simply unavailable for long enough that local backlog ceases to be optional. The emphasis throughout is on predictable writes, bounded reader memory, and partial recovery after corruption. From 67d6946e0c1cdc9886ac5ffc0dfcdf8af6e96f30 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:47:50 +0200 Subject: [PATCH 20/22] Update rust version on the badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f15eeec..5b79181 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # logjet [![Release](https://img.shields.io/github/v/release/tinythings/logjet)](https://github.com/tinythings/logjet/releases) -[![Rust](https://img.shields.io/badge/rust-2024%20edition-orange)](https://doc.rust-lang.org/edition-guide/) +[![Rust](https://img.shields.io/badge/rust-1.97.1-orange)](https://www.rust-lang.org) [![It is alive!](https://github.com/tinythings/logjet/actions/workflows/it-is-alive.yml/badge.svg)](https://github.com/tinythings/logjet/actions/workflows/it-is-alive.yml) [![Unit Tests](https://github.com/tinythings/logjet/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/tinythings/logjet/actions/workflows/unit-tests.yml) [![Integration Tests](https://github.com/tinythings/logjet/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/tinythings/logjet/actions/workflows/integration-tests.yml) From 853578e918a884ca20fee2450930dfb72563cce8 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 20:52:19 +0200 Subject: [PATCH 21/22] lintfixes --- ljx/src/dedup/drain3.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/ljx/src/dedup/drain3.rs b/ljx/src/dedup/drain3.rs index b3300fe..c085879 100644 --- a/ljx/src/dedup/drain3.rs +++ b/ljx/src/dedup/drain3.rs @@ -143,20 +143,18 @@ impl Drain { // Walk prefix tree following token values (or wildcard). let mut current = first_layer; - let mut depth: i64 = 1; - for token in tokens { + for (i, token) in tokens.iter().enumerate() { + let depth = i as i64 + 1; if depth >= self.max_node_depth || depth >= token_count as i64 { break; } if let Some(child) = current.children.get(token.as_str()) { current = child; - } else if let Some(child) = current.children.get(&self.param_str) { - current = child; } else { - return None; + let child = current.children.get(&self.param_str)?; + current = child; } - depth += 1; } self.fast_match(¤t.cluster_ids, tokens, false) @@ -192,9 +190,9 @@ impl Drain { } let mut current = first_layer as *mut Node; - let mut depth: i64 = 1; - for token in &cluster.template_tokens { + for (i, token) in cluster.template_tokens.iter().enumerate() { + let depth = i as i64 + 1; // Safety: we only hold one mutable reference at a time through // the pointer, never aliasing. The tree structure is owned by // self and not accessed concurrently. @@ -225,8 +223,6 @@ impl Drain { } else { node.children.get_mut(&self.param_str).unwrap() as *mut Node }; - - depth += 1; } } From f82c4ee7b5b9f48df7233edd9602c01dd2225dc7 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 21:05:55 +0200 Subject: [PATCH 22/22] Fix UT port collisions --- logjetd/tests/bridge_flows.rs | 94 ++++++++++++++++++++--------------- logjetd/tests/common/mod.rs | 19 ++----- 2 files changed, 57 insertions(+), 56 deletions(-) diff --git a/logjetd/tests/bridge_flows.rs b/logjetd/tests/bridge_flows.rs index 2e632be..cc55e69 100644 --- a/logjetd/tests/bridge_flows.rs +++ b/logjetd/tests/bridge_flows.rs @@ -72,6 +72,7 @@ fn bridge_drain_consumes_upstream_records() -> io::Result<()> { let ingest_port = free_port()?; let replay_port = free_port()?; let collector_port = reserve_port()?; + let _collector_addr = collector_port.port(); let appliance_config = dir.write( "appliance.conf", @@ -119,7 +120,8 @@ fn bridge_keep_forwards_backlog_over_grpc() -> io::Result<()> { let dir = TestDir::new("bridge-keep-grpc")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let appliance_config = dir.write( "appliance.conf", @@ -129,7 +131,7 @@ fn bridge_keep_forwards_backlog_over_grpc() -> io::Result<()> { )?; let bridge_config = dir.write( "bridge.conf", - &format!("collector.url: grpc://127.0.0.1:{collector_port}\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n"), + &format!("collector.url: grpc://127.0.0.1:{collector_addr}\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n"), )?; let _appliance = ChildGuard::spawn({ @@ -144,7 +146,7 @@ fn bridge_keep_forwards_backlog_over_grpc() -> io::Result<()> { post_otlp_http(&format!("127.0.0.1:{ingest_port}"), "bridge-grpc", message)?; } - let collector = MockGrpcCollector::start(collector_port)?; + let collector = MockGrpcCollector::start_with_listener(collector_port.into_listener())?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -162,8 +164,10 @@ fn bridge_keep_fans_out_to_http_and_grpc() -> io::Result<()> { let dir = TestDir::new("bridge-fanout")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let http_port = free_port()?; - let grpc_port = free_port()?; + let http_port = reserve_port()?; + let grpc_port = reserve_port()?; + let http_addr = http_port.port(); + let grpc_addr = grpc_port.port(); let appliance_config = dir.write( "appliance.conf", @@ -174,7 +178,7 @@ fn bridge_keep_fans_out_to_http_and_grpc() -> io::Result<()> { let bridge_config = dir.write( "bridge.conf", &format!( - "collector.url:\n - http://127.0.0.1:{http_port}/v1/logs\n - grpc://127.0.0.1:{grpc_port}\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n" + "collector.url:\n - http://127.0.0.1:{http_addr}/v1/logs\n - grpc://127.0.0.1:{grpc_addr}\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n" ), )?; @@ -190,10 +194,10 @@ fn bridge_keep_fans_out_to_http_and_grpc() -> io::Result<()> { post_otlp_http(&format!("127.0.0.1:{ingest_port}"), "bridge-fanout", message)?; } - let http = MockCollector::start(http_port)?; - let grpc = MockGrpcCollector::start(grpc_port)?; - wait_for_tcp(&format!("127.0.0.1:{http_port}"), Duration::from_secs(5))?; - wait_for_tcp(&format!("127.0.0.1:{grpc_port}"), Duration::from_secs(5))?; + let http = MockCollector::start_with_listener(http_port.into_listener(), Duration::ZERO)?; + let grpc = MockGrpcCollector::start_with_listener(grpc_port.into_listener())?; + wait_for_tcp(&format!("127.0.0.1:{http_addr}"), Duration::from_secs(5))?; + wait_for_tcp(&format!("127.0.0.1:{grpc_addr}"), Duration::from_secs(5))?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -213,7 +217,8 @@ fn bridge_keep_forwards_backlog_over_grpcs() -> io::Result<()> { let dir = TestDir::new("bridge-keep-grpcs")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let tls = write_fake_grpc_tls_files(&dir)?; let appliance_config = dir.write( @@ -225,7 +230,7 @@ fn bridge_keep_forwards_backlog_over_grpcs() -> io::Result<()> { let bridge_config = dir.write( "bridge.conf", &format!( - "collector.url: grpcs://127.0.0.1:{collector_port}\ncollector.ca-file: {}\ncollector.server-name: collector.test.invalid\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n", + "collector.url: grpcs://127.0.0.1:{collector_addr}\ncollector.ca-file: {}\ncollector.server-name: collector.test.invalid\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n", tls.ca.display() ), )?; @@ -242,7 +247,7 @@ fn bridge_keep_forwards_backlog_over_grpcs() -> io::Result<()> { post_otlp_http(&format!("127.0.0.1:{ingest_port}"), "bridge-jedi", message)?; } - let collector = MockGrpcCollector::start_tls(collector_port, &tls)?; + let collector = MockGrpcCollector::start_tls_with_listener(collector_port.into_listener(), &tls)?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -261,7 +266,8 @@ fn bridge_keep_forwards_backlog_over_grpcs_mtls() -> io::Result<()> { let dir = TestDir::new("bridge-keep-grpcs-mtls")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let tls = write_fake_grpc_tls_files(&dir)?; let appliance_config = dir.write( @@ -273,7 +279,7 @@ fn bridge_keep_forwards_backlog_over_grpcs_mtls() -> io::Result<()> { let bridge_config = dir.write( "bridge.conf", &format!( - "collector.url: grpcs://127.0.0.1:{collector_port}\ncollector.ca-file: {}\ncollector.cert-file: {}\ncollector.key-file: {}\ncollector.server-name: collector.test.invalid\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n", + "collector.url: grpcs://127.0.0.1:{collector_addr}\ncollector.ca-file: {}\ncollector.cert-file: {}\ncollector.key-file: {}\ncollector.server-name: collector.test.invalid\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n", tls.ca.display(), tls.client_cert.display(), tls.client_key.display() @@ -292,7 +298,7 @@ fn bridge_keep_forwards_backlog_over_grpcs_mtls() -> io::Result<()> { post_otlp_http(&format!("127.0.0.1:{ingest_port}"), "bridge-sith", message)?; } - let collector = MockGrpcCollector::start_mtls(collector_port, &tls)?; + let collector = MockGrpcCollector::start_mtls_with_listener(collector_port.into_listener(), &tls)?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -311,7 +317,8 @@ fn bridge_keep_rejects_grpcs_with_bad_ca() -> io::Result<()> { let other = TestDir::new("bridge-keep-grpcs-bad-ca-other")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let tls = write_fake_grpc_tls_files(&dir)?; let wrong_tls = write_fake_grpc_tls_files(&other)?; @@ -324,7 +331,7 @@ fn bridge_keep_rejects_grpcs_with_bad_ca() -> io::Result<()> { let bridge_config = dir.write( "bridge.conf", &format!( - "collector.url: grpcs://127.0.0.1:{collector_port}\ncollector.ca-file: {}\ncollector.server-name: collector.test.invalid\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n", + "collector.url: grpcs://127.0.0.1:{collector_addr}\ncollector.ca-file: {}\ncollector.server-name: collector.test.invalid\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n", wrong_tls.ca.display() ), )?; @@ -339,7 +346,7 @@ fn bridge_keep_rejects_grpcs_with_bad_ca() -> io::Result<()> { post_otlp_http(&format!("127.0.0.1:{ingest_port}"), "bridge-bad-ca", "BAD CA 001")?; - let collector = MockGrpcCollector::start_tls(collector_port, &tls)?; + let collector = MockGrpcCollector::start_tls_with_listener(collector_port.into_listener(), &tls)?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -357,7 +364,8 @@ fn bridge_keep_rejects_grpcs_with_wrong_server_name() -> io::Result<()> { let dir = TestDir::new("bridge-keep-grpcs-wrong-name")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let tls = write_fake_grpc_tls_files(&dir)?; let appliance_config = dir.write( @@ -369,7 +377,7 @@ fn bridge_keep_rejects_grpcs_with_wrong_server_name() -> io::Result<()> { let bridge_config = dir.write( "bridge.conf", &format!( - "collector.url: grpcs://127.0.0.1:{collector_port}\ncollector.ca-file: {}\ncollector.server-name: wrong.test.invalid\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n", + "collector.url: grpcs://127.0.0.1:{collector_addr}\ncollector.ca-file: {}\ncollector.server-name: wrong.test.invalid\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n", tls.ca.display() ), )?; @@ -384,7 +392,7 @@ fn bridge_keep_rejects_grpcs_with_wrong_server_name() -> io::Result<()> { post_otlp_http(&format!("127.0.0.1:{ingest_port}"), "bridge-wrong-name", "WRONG NAME 001")?; - let collector = MockGrpcCollector::start_tls(collector_port, &tls)?; + let collector = MockGrpcCollector::start_tls_with_listener(collector_port.into_listener(), &tls)?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -402,7 +410,8 @@ fn bridge_resume_state_survives_restart() -> io::Result<()> { let dir = TestDir::new("bridge-resume")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let state_path = dir.path().join("bridge.state"); let appliance_config = dir.write( @@ -414,7 +423,7 @@ fn bridge_resume_state_survives_restart() -> io::Result<()> { let bridge_config = dir.write( "bridge.conf", &format!( - "collector.url: http://127.0.0.1:{collector_port}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\nupstream.state-file: {}\n", + "collector.url: http://127.0.0.1:{collector_addr}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\nupstream.state-file: {}\n", state_path.display() ), )?; @@ -427,7 +436,7 @@ fn bridge_resume_state_survives_restart() -> io::Result<()> { wait_for_tcp(&format!("127.0.0.1:{ingest_port}"), Duration::from_secs(5))?; wait_for_tcp(&format!("127.0.0.1:{replay_port}"), Duration::from_secs(5))?; - let collector = MockCollector::start(collector_port)?; + let collector = MockCollector::start_with_listener(collector_port.into_listener(), Duration::ZERO)?; for message in ["RESUME 001", "RESUME 002", "RESUME 003"] { post_otlp_http(&format!("127.0.0.1:{ingest_port}"), "bridge-resume", message)?; @@ -481,7 +490,8 @@ fn bridge_keep_works_with_file_rotation() -> io::Result<()> { fs::create_dir_all(&spool_dir)?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let appliance_config = dir.write( "appliance-file.conf", @@ -492,7 +502,7 @@ fn bridge_keep_works_with_file_rotation() -> io::Result<()> { )?; let bridge_config = dir.write( "bridge-file.conf", - &format!("collector.url: http://127.0.0.1:{collector_port}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n"), + &format!("collector.url: http://127.0.0.1:{collector_addr}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n"), )?; let _appliance = ChildGuard::spawn({ @@ -508,7 +518,7 @@ fn bridge_keep_works_with_file_rotation() -> io::Result<()> { post_otlp_http(&format!("127.0.0.1:{ingest_port}"), "bridge-file", &message)?; } - let collector = MockCollector::start(collector_port)?; + let collector = MockCollector::start_with_listener(collector_port.into_listener(), Duration::ZERO)?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -541,7 +551,8 @@ fn bridge_resets_saved_state_when_upstream_stream_changes() -> io::Result<()> { let dir = TestDir::new("bridge-upstream-reset")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let state_path = dir.path().join("bridge.state"); let appliance_alpha = dir.write( @@ -559,12 +570,12 @@ fn bridge_resets_saved_state_when_upstream_stream_changes() -> io::Result<()> { let bridge_config = dir.write( "bridge-reset.conf", &format!( - "collector.url: http://127.0.0.1:{collector_port}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\nupstream.state-file: {}\n", + "collector.url: http://127.0.0.1:{collector_addr}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\nupstream.state-file: {}\n", state_path.display() ), )?; - let collector = MockCollector::start(collector_port)?; + let collector = MockCollector::start_with_listener(collector_port.into_listener(), Duration::ZERO)?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); @@ -610,7 +621,8 @@ fn bridge_block_mode_handles_slow_collector_without_losing_order() -> io::Result let dir = TestDir::new("bridge-slow-collector")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let appliance_config = dir.write( "appliance.conf", @@ -621,7 +633,7 @@ fn bridge_block_mode_handles_slow_collector_without_losing_order() -> io::Result let bridge_config = dir.write( "bridge.conf", &format!( - "collector.url: http://127.0.0.1:{collector_port}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\nbackpressure.enabled: true\nbackpressure.mode: block\nbackpressure.max-buffered-records: 2\n" + "collector.url: http://127.0.0.1:{collector_addr}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\nbackpressure.enabled: true\nbackpressure.mode: block\nbackpressure.max-buffered-records: 2\n" ), )?; @@ -633,7 +645,7 @@ fn bridge_block_mode_handles_slow_collector_without_losing_order() -> io::Result wait_for_tcp(&format!("127.0.0.1:{ingest_port}"), Duration::from_secs(5))?; wait_for_tcp(&format!("127.0.0.1:{replay_port}"), Duration::from_secs(5))?; - let collector = MockCollector::start_with_delay(collector_port, Duration::from_millis(150))?; + let collector = MockCollector::start_with_listener(collector_port.into_listener(), Duration::from_millis(150))?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -666,7 +678,8 @@ fn replay_recovers_after_middle_of_file_is_removed() -> io::Result<()> { let spool_dir = dir.path().join("spool"); fs::create_dir_all(&spool_dir)?; let ingest_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let appliance_config = dir.write( "appliance.conf", @@ -699,10 +712,10 @@ fn replay_recovers_after_middle_of_file_is_removed() -> io::Result<()> { damaged.extend_from_slice(&original[second_cut..]); fs::write(&spool_file, damaged)?; - let collector = MockCollector::start(collector_port)?; + let collector = MockCollector::start_with_listener(collector_port.into_listener(), Duration::ZERO)?; let status = { let mut cmd = ljd_command(); - cmd.arg("replay").arg("--path").arg(&spool_dir).arg("--name").arg("recover.logjet").arg("--dest").arg(format!("127.0.0.1:{collector_port}")); + cmd.arg("replay").arg("--path").arg(&spool_dir).arg("--name").arg("recover.logjet").arg("--dest").arg(format!("127.0.0.1:{collector_addr}")); cmd.status()? }; assert!(status.success()); @@ -720,7 +733,8 @@ fn bridge_forwards_large_payloads_end_to_end() -> io::Result<()> { let dir = TestDir::new("bridge-large-payload")?; let ingest_port = free_port()?; let replay_port = free_port()?; - let collector_port = free_port()?; + let collector_port = reserve_port()?; + let collector_addr = collector_port.port(); let appliance_config = dir.write( "appliance.conf", @@ -730,7 +744,7 @@ fn bridge_forwards_large_payloads_end_to_end() -> io::Result<()> { )?; let bridge_config = dir.write( "bridge.conf", - &format!("collector.url: http://127.0.0.1:{collector_port}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n"), + &format!("collector.url: http://127.0.0.1:{collector_addr}/v1/logs\nupstream.replay: 127.0.0.1:{replay_port}\nupstream.mode: keep\n"), )?; let _appliance = ChildGuard::spawn({ @@ -741,7 +755,7 @@ fn bridge_forwards_large_payloads_end_to_end() -> io::Result<()> { wait_for_tcp(&format!("127.0.0.1:{ingest_port}"), Duration::from_secs(5))?; wait_for_tcp(&format!("127.0.0.1:{replay_port}"), Duration::from_secs(5))?; - let collector = MockCollector::start(collector_port)?; + let collector = MockCollector::start_with_listener(collector_port.into_listener(), Duration::ZERO)?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); diff --git a/logjetd/tests/common/mod.rs b/logjetd/tests/common/mod.rs index 3a06927..3cc600a 100644 --- a/logjetd/tests/common/mod.rs +++ b/logjetd/tests/common/mod.rs @@ -206,16 +206,6 @@ pub struct MockCollector { } impl MockCollector { - pub fn start(port: u16) -> io::Result { - Self::start_with_delay(port, Duration::ZERO) - } - - pub fn start_with_delay(port: u16, delay: Duration) -> io::Result { - let addr = format!("127.0.0.1:{port}"); - let listener = TcpListener::bind(&addr)?; - Self::start_with_listener(listener, delay) - } - pub fn start_with_listener(listener: TcpListener, delay: Duration) -> io::Result { listener.set_nonblocking(false)?; let received = Arc::new(Mutex::new(Vec::new())); @@ -244,8 +234,7 @@ pub struct MockGrpcCollector { } impl MockGrpcCollector { - pub fn start(port: u16) -> io::Result { - let listener = TcpListener::bind(("127.0.0.1", port))?; + pub fn start_with_listener(listener: TcpListener) -> io::Result { Self::start_with_listener_and_mode(listener, GrpcTlsMode::Plain) } @@ -253,13 +242,11 @@ impl MockGrpcCollector { self.received.lock().unwrap().iter().flat_map(extract_messages).collect() } - pub fn start_tls(port: u16, tls: &GrpcTlsFiles) -> io::Result { - let listener = TcpListener::bind(("127.0.0.1", port))?; + pub fn start_tls_with_listener(listener: TcpListener, tls: &GrpcTlsFiles) -> io::Result { Self::start_with_listener_and_mode(listener, GrpcTlsMode::Tls { tls: tls.clone() }) } - pub fn start_mtls(port: u16, tls: &GrpcTlsFiles) -> io::Result { - let listener = TcpListener::bind(("127.0.0.1", port))?; + pub fn start_mtls_with_listener(listener: TcpListener, tls: &GrpcTlsFiles) -> io::Result { Self::start_with_listener_and_mode(listener, GrpcTlsMode::Mtls { tls: tls.clone() }) }