From 6afca09dd1bb8a420bdaad9e252198301a4b9d72 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Mon, 27 Jul 2026 16:08:56 +0200 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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() }) } From 2697abc1103c3c17500eee610df5665505291de0 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Wed, 12 Aug 2026 13:24:12 +0200 Subject: [PATCH 23/26] Bugfix: ARM builds with musl do not support cdylib --- .cargo/config.toml | 3 +++ Makefile | 2 +- logjetd/tests/common/mod.rs | 33 +++++++++++++++++++++++++++++ plugins/logcat-ingest/Cargo.toml | 2 +- plugins/parquet-exporter/Cargo.toml | 2 +- plugins/perfetto-ingest/Cargo.toml | 2 +- plugins/stress-ingest/Cargo.toml | 2 +- plugins/syslog-ingest/Cargo.toml | 2 +- 8 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index e417745..23eea80 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,8 @@ [target.aarch64-unknown-linux-musl] linker = "rust-lld" +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" + [target.x86_64-unknown-linux-musl] linker = "rust-lld" diff --git a/Makefile b/Makefile index 32d152f..b445779 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ C_DESCR := \033[38;5;43m C_RESET := \033[0m MXRUN_BIN ?= mxrun -ARM_TARGET ?= aarch64-unknown-linux-musl +ARM_TARGET ?= aarch64-unknown-linux-gnu X86_TARGET ?= x86_64-unknown-linux-musl CORE_WORKSPACE := --workspace --exclude otlp-demo MANPAGE_MD := $(wildcard doc/manpage/*.1.md) diff --git a/logjetd/tests/common/mod.rs b/logjetd/tests/common/mod.rs index 3cc600a..b87752d 100644 --- a/logjetd/tests/common/mod.rs +++ b/logjetd/tests/common/mod.rs @@ -162,6 +162,39 @@ pub fn free_port() -> io::Result { Ok(port) } +pub struct Appliance { + _guard: ChildGuard, + pub ingest_port: u16, + pub replay_port: u16, +} + +pub fn start_serve(dir: &TestDir, config_name: &str, config_template: &str) -> io::Result { + let deadline = Instant::now() + Duration::from_secs(10); + loop { + let ingest_port = free_port()?; + let replay_port = free_port()?; + let config = config_template + .replace("{ingest_port}", &ingest_port.to_string()) + .replace("{replay_port}", &replay_port.to_string()); + let config_path = dir.write(config_name, &config)?; + + let mut cmd = ljd_command(); + cmd.arg("--config").arg(&config_path).arg("serve"); + let Ok(guard) = ChildGuard::spawn(cmd) else { continue; }; + + if wait_for_tcp(&format!("127.0.0.1:{ingest_port}"), Duration::from_secs(3)).is_ok() + && wait_for_tcp(&format!("127.0.0.1:{replay_port}"), Duration::from_secs(3)).is_ok() + { + return Ok(Appliance { _guard: guard, ingest_port, replay_port }); + } + + drop(guard); + if Instant::now() >= deadline { + return Err(io::Error::new(io::ErrorKind::TimedOut, "ljd serve did not start within 10s")); + } + } +} + pub fn wait_for_tcp(addr: &str, timeout: Duration) -> io::Result<()> { let deadline = Instant::now() + timeout; loop { diff --git a/plugins/logcat-ingest/Cargo.toml b/plugins/logcat-ingest/Cargo.toml index 9387f15..98029d9 100644 --- a/plugins/logcat-ingest/Cargo.toml +++ b/plugins/logcat-ingest/Cargo.toml @@ -5,4 +5,4 @@ edition.workspace = true license.workspace = true [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] diff --git a/plugins/parquet-exporter/Cargo.toml b/plugins/parquet-exporter/Cargo.toml index ce982cc..77c1c16 100644 --- a/plugins/parquet-exporter/Cargo.toml +++ b/plugins/parquet-exporter/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true license.workspace = true [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] [dependencies] arrow-array = "58.0.0" diff --git a/plugins/perfetto-ingest/Cargo.toml b/plugins/perfetto-ingest/Cargo.toml index 318d1a7..3da1f52 100644 --- a/plugins/perfetto-ingest/Cargo.toml +++ b/plugins/perfetto-ingest/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true license.workspace = true [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] [dependencies] opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "trace", "metrics", "logs"] } diff --git a/plugins/stress-ingest/Cargo.toml b/plugins/stress-ingest/Cargo.toml index 5f6e683..e0d3c48 100644 --- a/plugins/stress-ingest/Cargo.toml +++ b/plugins/stress-ingest/Cargo.toml @@ -5,4 +5,4 @@ edition.workspace = true license.workspace = true [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] diff --git a/plugins/syslog-ingest/Cargo.toml b/plugins/syslog-ingest/Cargo.toml index 277cacb..e32e181 100644 --- a/plugins/syslog-ingest/Cargo.toml +++ b/plugins/syslog-ingest/Cargo.toml @@ -5,4 +5,4 @@ edition.workspace = true license.workspace = true [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] From 3ea2a62a7df3a228819a148c2bc0f99b5b1ec245 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Wed, 12 Aug 2026 13:24:25 +0200 Subject: [PATCH 24/26] Add a demo CEL search for CLI --- demo/cel-search-cli/README.md | 30 ++ demo/cel-search-cli/cel-search-cli.conf | 3 + demo/cel-search-cli/run-demo.sh | 129 ++++++++ .../bin/cel-search-cli-logjet-generator.rs | 299 ++++++++++++++++++ 4 files changed, 461 insertions(+) create mode 100644 demo/cel-search-cli/README.md create mode 100644 demo/cel-search-cli/cel-search-cli.conf create mode 100755 demo/cel-search-cli/run-demo.sh create mode 100644 demo/src/bin/cel-search-cli-logjet-generator.rs diff --git a/demo/cel-search-cli/README.md b/demo/cel-search-cli/README.md new file mode 100644 index 0000000..7c8f03e --- /dev/null +++ b/demo/cel-search-cli/README.md @@ -0,0 +1,30 @@ +# CEL Search CLI Demo + +Generates a `.logjet` file with 200 diverse log records and +runs a battery of CEL search queries against them via `ljx`. + +## Build First + +```bash +make demo +``` + +## Run + +```bash +cd demo/cel-search-cli +./run-demo.sh +``` + +The script demonstrates CEL searches over: + +- severity number / text comparisons +- body substring matching +- service name filtering +- attribute access (`attributes["key"]`) +- resource attribute access (`resource["key"]`) +- event name filtering +- combined AND conditions +- CEL with `--grep` and `--service` flags +- time-range filtering (`--ts-min` / `--ts-max`) +- `ljx count` mode diff --git a/demo/cel-search-cli/cel-search-cli.conf b/demo/cel-search-cli/cel-search-cli.conf new file mode 100644 index 0000000..f5c4559 --- /dev/null +++ b/demo/cel-search-cli/cel-search-cli.conf @@ -0,0 +1,3 @@ +# shell-style demo config +COUNT=200 +OUTPUT_FILE="logs/cel-search-cli.logjet" diff --git a/demo/cel-search-cli/run-demo.sh b/demo/cel-search-cli/run-demo.sh new file mode 100755 index 0000000..3f11b94 --- /dev/null +++ b/demo/cel-search-cli/run-demo.sh @@ -0,0 +1,129 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd) +GENERATOR="$ROOT_DIR/target/debug/cel-search-cli-logjet-generator" +LJX="$ROOT_DIR/target/debug/ljx" +CONFIG="$SCRIPT_DIR/cel-search-cli.conf" + +if [ ! -x "$GENERATOR" ] || [ ! -x "$LJX" ]; then + echo "missing demo binaries" + echo "build them first with: cargo build -p otlp-demo --bin cel-search-cli-logjet-generator -p ljx" + exit 1 +fi + +. "$CONFIG" + +cd "$SCRIPT_DIR" +mkdir -p logs +rm -f "$OUTPUT_FILE" + +echo "generating $COUNT diverse log records into $OUTPUT_FILE" +"$GENERATOR" "$OUTPUT_FILE" "$COUNT" + +echo +printf '\033[1;38;5;200m=== CEL Search Demos ===\033[0m\n' +echo "File: $OUTPUT_FILE ($COUNT records)" +echo + +run_query() { + local desc="$1" + shift + printf '\033[1;38;5;253m%s\033[0m\n' "$desc" + printf ' \033[38;5;43mcmd:\033[0m ljx count %s %s\n' "$OUTPUT_FILE" "$*" + printf ' \033[38;5;43mres:\033[0m ' + "$LJX" count "$OUTPUT_FILE" "$@" + echo +} + +echo "--- Severity queries ---" + +run_query "ERROR or FATAL (severity_number >= 17)" --cel 'severity_number >= 17' + +run_query "INFO only (severity_text == INFO)" --cel 'severity_text == "INFO"' + +run_query "WARN and above" --cel 'severity_number >= 13' + +run_query "DEBUG only" --cel 'severity_text == "DEBUG"' + +echo "--- Body substring searches ---" + +run_query "body contains timeout (payment timeouts)" --cel 'body.contains("timeout")' + +run_query "body contains failed (payment or auth failures)" --cel 'body.contains("failed")' + +run_query "body contains cache (cache hits and misses)" --cel 'body.contains("cache")' + +run_query "body contains login (auth events)" --cel 'body.contains("login")' + +echo "--- Service name filtering ---" + +run_query "payment-worker only" --cel 'service_name == "payment-worker"' + +run_query "auth-gateway only" --cel 'service_name == "auth-gateway"' + +run_query "core-api or cache-layer" --cel 'service_name == "core-api" || service_name == "cache-layer"' + +echo "--- Attribute access ---" + +run_query "HTTP 5xx status codes" --cel 'attributes["http.status_code"] >= 500' + +run_query "HTTP 404 status" --cel 'attributes["http.status_code"] == 404' + +run_query "has error.code attribute (non-empty)" --cel 'attributes["error.code"] != ""' + +run_query "specific error code AUTH_FAILED" --cel 'attributes["error.code"] == "AUTH_FAILED"' + +echo "--- Resource attribute access ---" + +run_query "eu-west-1 region" --cel 'resource["deploy.region"] == "eu-west-1"' + +run_query "staging environment" --cel 'resource["deploy.env"] == "staging"' + +echo "--- Event name filtering ---" + +run_query "auth login events" --cel 'event_name == "auth.login"' + +run_query "http request events" --cel 'event_name == "http.request"' + +echo "--- Combined conditions ---" + +run_query "auth failed (body + event_name)" --cel 'body.contains("failed") && event_name == "auth.login"' + +run_query "cache miss on cache-layer" --cel 'body.contains("miss") && service_name == "cache-layer"' + +run_query "ERROR with consumer_lag critical" --cel 'severity_number >= 17 && body.contains("critical")' + +echo "--- CEL + other filters (AND semantics) ---" + +run_query "CEL + --grep (WARN+ with 'login' in raw bytes)" --cel 'severity_number >= 13' --grep login + +run_query "CEL + --fixed-string (ERROR+ with 'email' exact match)" --cel 'severity_number >= 17' --fixed-string email + +echo "--- Record preview (NDJSON) ---" + +printf '\033[1;38;5;253m%s\033[0m\n' "CEL search with NDJSON output (matching records)" +printf ' \033[38;5;43mcmd:\033[0m ljx %s --cel '\''severity_number >= 17'\'' --format ndjson\n' "$OUTPUT_FILE" +printf ' \033[38;5;43mres:\033[0m\n' +"$LJX" "$OUTPUT_FILE" --cel 'severity_number >= 17' --format ndjson 2>/dev/null | head -5 +echo " ..." + +echo +echo "--- Time range queries ---" + +T1=1773000000000000000 +T2=1773009000000000000 + +run_query "records in first 100 records (time window)" --ts-min "$T1" --ts-max "$T2" --cel 'event_name == "http.request"' +run_query "all records in first 100 records" --ts-min "$T1" --ts-max "$T2" + +echo +echo "--- Multiple CEL expressions (AND semantics) ---" + +run_query "two --cel flags: cache AND not hit (misses)" --cel 'body.contains("cache")' --cel '!body.contains("hit")' + +run_query "two --cel flags: ERROR severity + has error code" --cel 'severity_number >= 17' --cel 'attributes["error.code"] != ""' + +echo +printf '\033[1;38;5;200mDone. Try TUI mode: %s view %s\033[0m\n' "$LJX" "$OUTPUT_FILE" diff --git a/demo/src/bin/cel-search-cli-logjet-generator.rs b/demo/src/bin/cel-search-cli-logjet-generator.rs new file mode 100644 index 0000000..260475f --- /dev/null +++ b/demo/src/bin/cel-search-cli-logjet-generator.rs @@ -0,0 +1,299 @@ +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::PathBuf; + +use logjet::{LogjetWriter, RecordType, WriterConfig}; +use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; +use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue}; +use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs, SeverityNumber}; +use opentelemetry_proto::tonic::resource::v1::Resource; +use prost::Message; + +fn main() -> Result<(), Box> { + let mut args = std::env::args().skip(1); + let output = match args.next() { + Some(path) => PathBuf::from(path), + None => { + eprintln!("usage: cel-search-cli-logjet-generator [count]"); + std::process::exit(2); + } + }; + let count = args.next().map(|value| value.parse::()).transpose()?.unwrap_or(200); + + let file = File::create(&output)?; + let writer = BufWriter::new(file); + let mut logjet = LogjetWriter::with_config(writer, WriterConfig::default()); + + let services = ["core-api", "payment-worker", "auth-gateway", "event-bus", "cache-layer"]; + let severities: &[(&str, i32)] = &[ + ("DEBUG", SeverityNumber::Debug as i32), + ("INFO", SeverityNumber::Info as i32), + ("WARN", SeverityNumber::Warn as i32), + ("ERROR", SeverityNumber::Error as i32), + ("FATAL", SeverityNumber::Fatal as i32), + ]; + let scopes = [ + "com.example.http-middleware", + "com.example.payment-engine", + "com.example.auth-module", + "com.example.event-bus", + "com.example.cache-manager", + ]; + let regions = ["us-east-1", "us-east-1", "eu-west-1", "ap-southeast-1", "eu-west-1"]; + let envs = ["prod", "prod", "staging", "prod", "prod"]; + + for seq in 1..=count { + let idx = ((seq - 1) % 5) as usize; + let service = services[idx]; + let (severity_text, severity_number) = severities[idx]; + let scope = scopes[idx]; + let region = regions[idx]; + let env = envs[idx]; + let ts_nanos = unix_time_nanos(seq); + + let (event_name, body, attrs) = build_event_data(idx, seq); + + let request = ExportLogsServiceRequest { + resource_logs: vec![ResourceLogs { + resource: Some(Resource { + attributes: vec![ + string_attr("service.name", service), + string_attr("service.namespace", "logjet-demo"), + string_attr("host.name", "garage-rig"), + string_attr("deploy.region", region), + string_attr("deploy.env", env), + ], + dropped_attributes_count: 0, + entity_refs: Vec::new(), + }), + scope_logs: vec![ScopeLogs { + scope: Some(InstrumentationScope { + name: scope.to_string(), + version: "0.1.0".to_string(), + attributes: vec![string_attr("library.language", "rust")], + dropped_attributes_count: 0, + }), + log_records: vec![LogRecord { + time_unix_nano: ts_nanos, + observed_time_unix_nano: ts_nanos, + severity_number, + severity_text: severity_text.to_string(), + body: Some(AnyValue { + value: Some(opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue( + body.to_string(), + )), + }), + attributes: attrs, + dropped_attributes_count: 0, + flags: 0, + trace_id: Vec::new(), + span_id: Vec::new(), + event_name: event_name.to_string(), + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + + logjet.push(RecordType::Logs, seq, ts_nanos, &request.encode_to_vec())?; + } + + let mut writer = logjet.into_inner()?; + writer.flush()?; + println!("wrote {count} log records to {}", output.display()); + Ok(()) +} + +fn pivot(seq: u64) -> usize { + ((seq - 1) / 5) as usize +} + +fn build_event_data(idx: usize, seq: u64) -> (&'static str, String, Vec) { + match idx { + 0 => http_request(seq), + 1 => payment_process(seq), + 2 => auth_login(seq), + 3 => event_bus(seq), + _ => cache_access(seq), + } +} + +fn http_request(seq: u64) -> (&'static str, String, Vec) { + let bodies: &[(&str, i64, &str, &str)] = &[ + ("GET /api/items returned 200 in 12ms", 200, "GET", "/api/items"), + ("POST /api/orders returned 201 in 45ms", 201, "POST", "/api/orders"), + ("GET /api/items returned 500: internal server error", 500, "GET", "/api/items"), + ("DELETE /api/sessions returned 404: not found", 404, "DELETE", "/api/sessions"), + ("POST /api/users returned 200 in 8ms", 200, "POST", "/api/users"), + ("GET /api/health returned 200 in 1ms", 200, "GET", "/api/health"), + ("GET /api/orders returned 504: gateway timeout", 504, "GET", "/api/orders"), + ("POST /api/items returned 500: internal server error", 500, "POST", "/api/items"), + ("GET /api/users returned 403: forbidden", 403, "GET", "/api/users"), + ("DELETE /api/sessions returned 200 in 5ms", 200, "DELETE", "/api/sessions"), + ]; + let (body, status, method, route) = bodies[pivot(seq) % bodies.len()]; + let dur = 1 + (seq % 100); + let mut attrs = vec![ + string_attr("http.method", method), + string_attr("http.route", route), + int_attr("http.status_code", status), + int_attr("http.duration_ms", dur as i64), + ]; + if status >= 500 { + attrs.push(string_attr("error.code", "INTERNAL_ERROR")); + } else if status >= 400 { + attrs.push(string_attr("error.code", "CLIENT_ERROR")); + } + ("http.request", body.to_string(), attrs) +} + +fn payment_process(seq: u64) -> (&'static str, String, Vec) { + let amounts = [19.99, 45.50, 30.00, 12.75, 99.99, 22.00, 5.00, 18.50, 67.30, 41.20]; + let templates: &[&str] = &[ + "payment for order #{} succeeded: amount $19.99", + "payment for order #{} succeeded: amount $45.50", + "payment for order #{} failed: timeout", + "payment for order #{} failed: insufficient funds", + "payment for order #{} succeeded: amount $99.99", + "payment for order #{} failed: gateway unavailable", + "payment for order #{} succeeded: amount $5.00", + "payment for order #{} failed: timeout", + "payment for order #{} succeeded: amount $67.30", + "payment for order #{} failed: rate limited", + ]; + let i = pivot(seq) % templates.len(); + let order_id = seq * 100 + 42; + let body = templates[i].replace("{}", &order_id.to_string()); + let is_ok = !body.contains("failed"); + let mut attrs = vec![ + int_attr("order.id", order_id as i64), + double_attr("payment.amount", amounts[i]), + ]; + if !is_ok { + attrs.push(string_attr("error.code", "PAYMENT_FAILED")); + } + ("payment.process", body, attrs) +} + +fn auth_login(seq: u64) -> (&'static str, String, Vec) { + let users = ["bob@example.com", "alice@corp.io", "ops@startup.dev", "dev@local.host", "admin@system.org"]; + let templates: &[(&str, &str)] = &[ + ("user {} logged in via password", "password"), + ("user {} logged in via oauth2", "oauth2"), + ("user {} login failed: invalid credentials", "password"), + ("user {} logged in via sso", "sso"), + ("user {} logged in via token", "token"), + ("user {} login failed: expired session", "token"), + ("user {} logged in via password", "password"), + ("user {} login failed: invalid credentials", "password"), + ("user {} logged in via oauth2", "oauth2"), + ("user {} logged in via sso", "sso"), + ]; + let i = pivot(seq) % templates.len(); + let (template, method) = templates[i]; + let user = users[pivot(seq) % users.len()]; + let body = template.replace("{}", user); + let mut attrs = vec![ + string_attr("user.id", user), + string_attr("auth.method", method), + int_attr("auth.session_ttl", 3600), + ]; + if body.contains("failed") { + attrs.push(string_attr("error.code", "AUTH_FAILED")); + } + ("auth.login", body, attrs) +} + +fn event_bus(seq: u64) -> (&'static str, String, Vec) { + let topics = ["order.created", "payment.completed", "user.registered", "cache.invalidated", "email.sent"]; + let templates: &[&str] = &[ + "event bus processed message on topic {} ok", + "event bus processed message on topic {} ok", + "event bus consumer lag high on topic {}: 3500ms", + "event bus processed message on topic {} ok", + "event bus consumer lag critical on topic {}: 12000ms behind", + "event bus processed message on topic {} ok", + "event bus processed message on topic {} ok", + "event bus consumer lag high on topic {}: 4200ms", + "event bus consumer lag critical on topic {}: 9500ms behind", + "event bus processed message on topic {} ok", + ]; + let i = pivot(seq) % templates.len(); + let topic = topics[pivot(seq) % topics.len()]; + let body = templates[i].replace("{}", topic); + let mut attrs = vec![string_attr("messaging.topic", topic)]; + if body.contains("critical") { + attrs.push(int_attr("messaging.consumer_lag", 12000)); + attrs.push(string_attr("error.code", "CONSUMER_LAG_CRITICAL")); + } else if body.contains("high") { + attrs.push(int_attr("messaging.consumer_lag", 3500)); + attrs.push(string_attr("error.code", "CONSUMER_LAG_HIGH")); + } else { + attrs.push(int_attr("messaging.consumer_lag", 10)); + } + ("message.processed", body, attrs) +} + +fn cache_access(seq: u64) -> (&'static str, String, Vec) { + let keys = [ + "user:session:172", + "product:inventory:55", + "config:feature-flags", + "rate-limit:api:bucket-3", + "db:query:recent-orders", + ]; + let templates: &[&str] = &[ + "cache hit on key {} (ttl 300s)", + "cache hit on key {} (ttl 450s)", + "cache miss on key {}", + "cache hit on key {} (ttl 380s)", + "cache miss on key {}", + "cache hit on key {} (ttl 600s)", + "cache hit on key {} (ttl 120s)", + "cache miss on key {}", + "cache hit on key {} (ttl 420s)", + "cache hit on key {} (ttl 200s)", + ]; + let i = pivot(seq) % templates.len(); + let key = keys[pivot(seq) % keys.len()]; + let body = templates[i].replace("{}", key); + let ttl: i64 = if body.contains("miss") { 0 } else { 300 }; + let mut attrs = vec![string_attr("cache.key", key), int_attr("cache.ttl", ttl)]; + if body.contains("miss") { + attrs.push(string_attr("error.code", "CACHE_MISS")); + } + ("cache.access", body, attrs) +} + +fn string_attr(key: &str, value: &str) -> KeyValue { + KeyValue { + key: key.to_string(), + value: Some(AnyValue { + value: Some(opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(value.to_string())), + }), + } +} + +fn int_attr(key: &str, value: i64) -> KeyValue { + KeyValue { + key: key.to_string(), + value: Some(AnyValue { + value: Some(opentelemetry_proto::tonic::common::v1::any_value::Value::IntValue(value)), + }), + } +} + +fn double_attr(key: &str, value: f64) -> KeyValue { + KeyValue { + key: key.to_string(), + value: Some(AnyValue { + value: Some(opentelemetry_proto::tonic::common::v1::any_value::Value::DoubleValue(value)), + }), + } +} + +fn unix_time_nanos(seq: u64) -> u64 { + let base = 1_773_000_000_000_000_000u64; + base.saturating_add(seq.saturating_mul(90_000_000_000)) +} From b4cdf30256e1fd4e7172e0135a67251e8f577583 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Wed, 12 Aug 2026 13:35:51 +0200 Subject: [PATCH 25/26] Fix broken test racing listener --- logjetd/tests/bridge_flows.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/logjetd/tests/bridge_flows.rs b/logjetd/tests/bridge_flows.rs index cc55e69..cd77290 100644 --- a/logjetd/tests/bridge_flows.rs +++ b/logjetd/tests/bridge_flows.rs @@ -248,6 +248,7 @@ fn bridge_keep_forwards_backlog_over_grpcs() -> io::Result<()> { } let collector = MockGrpcCollector::start_tls_with_listener(collector_port.into_listener(), &tls)?; + wait_for_tcp(&format!("127.0.0.1:{collector_addr}"), Duration::from_secs(5))?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); @@ -299,6 +300,7 @@ fn bridge_keep_forwards_backlog_over_grpcs_mtls() -> io::Result<()> { } let collector = MockGrpcCollector::start_mtls_with_listener(collector_port.into_listener(), &tls)?; + wait_for_tcp(&format!("127.0.0.1:{collector_addr}"), Duration::from_secs(5))?; let _bridge = ChildGuard::spawn({ let mut cmd = ljd_command(); cmd.arg("--config").arg(&bridge_config).arg("bridge"); From c2274aa0d101f5ef4c2a85568f36f3102cccd7b7 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Wed, 12 Aug 2026 13:35:58 +0200 Subject: [PATCH 26/26] Linters --- logjetd/tests/common/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/logjetd/tests/common/mod.rs b/logjetd/tests/common/mod.rs index b87752d..0838e7d 100644 --- a/logjetd/tests/common/mod.rs +++ b/logjetd/tests/common/mod.rs @@ -162,12 +162,14 @@ pub fn free_port() -> io::Result { Ok(port) } +#[allow(dead_code)] pub struct Appliance { _guard: ChildGuard, pub ingest_port: u16, pub replay_port: u16, } +#[allow(dead_code)] pub fn start_serve(dir: &TestDir, config_name: &str, config_template: &str) -> io::Result { let deadline = Instant::now() + Duration::from_secs(10); loop {