diff --git a/Cargo.lock b/Cargo.lock index fcf55f4..1cd28e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,34 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -67,6 +95,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.10.0" @@ -129,6 +163,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "clap" version = "4.5.55" @@ -237,6 +280,18 @@ dependencies = [ "syn", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -360,12 +415,34 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hifijson" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7763b98ba8a24f59e698bf9ab197e7676c640d6455d1580b4ce7dc560f0f0d" + [[package]] name = "http" version = "1.4.0" @@ -508,6 +585,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -520,6 +607,66 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jaq-core" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6fda09ee08c84c81293fdf811d9ebaa87b327557b5391f290c926d728c2ddd4" +dependencies = [ + "aho-corasick", + "base64", + "chrono", + "hifijson", + "jaq-interpret", + "libm", + "log", + "regex", + "urlencoding", +] + +[[package]] +name = "jaq-interpret" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fe95ec3c24af3fd9f3dd1091593f5e49b003a66c496a8aa39d764d0a06ae17b" +dependencies = [ + "ahash", + "dyn-clone", + "hifijson", + "indexmap", + "jaq-syn", + "once_cell", + "serde_json", +] + +[[package]] +name = "jaq-parse" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0346d7d3146cdda8acd929581f3d6626a332356c74d5c95aeaffaac2eb6dee82" +dependencies = [ + "chumsky", + "jaq-syn", +] + +[[package]] +name = "jaq-std" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfbaa55578fd3b70433b594a370741e0c364e4afff92cc0099623fce87311bc1" +dependencies = [ + "jaq-syn", +] + +[[package]] +name = "jaq-syn" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba44fe4428c71304604261ecbae047ee9cfb60c4f1a6bd222ebbb31726d3948" +dependencies = [ + "serde", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -542,6 +689,12 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -569,6 +722,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "mio" version = "1.1.1" @@ -799,6 +958,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + [[package]] name = "rustix" version = "1.1.3" @@ -863,6 +1051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -885,6 +1074,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1182,6 +1384,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -1285,6 +1493,11 @@ dependencies = [ "chrono", "clap", "futures-util", + "jaq-core", + "jaq-interpret", + "jaq-parse", + "jaq-std", + "serde_json", "tokio", "tokio-tungstenite", "tracing", @@ -1542,3 +1755,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml index 89e0c57..dbdf883 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,8 @@ clap = { version = "4", features = ["derive"] } futures-util = "0.3" chrono = "0.4" url = "2" +jaq-core = "1" +jaq-interpret = "1" +jaq-parse = "1" +jaq-std = "1" +serde_json = "1" diff --git a/src/main.rs b/src/main.rs index 661cb38..d28ef19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ use std::fs; use std::path::PathBuf; +use std::sync::Arc; use chrono::Local; use clap::Parser; -use url::{form_urlencoded, Url}; use futures_util::StreamExt; +use jaq_interpret::{Ctx, Filter, FilterT, ParseCtx, RcIter, Val}; use tokio::task::JoinSet; use tokio_tungstenite::{ connect_async_with_config, @@ -15,6 +16,7 @@ use tokio_tungstenite::{ }, }; use tracing::{debug, error, info, warn, Level}; +use url::{form_urlencoded, Url}; #[derive(Parser)] #[command(name = "websocket-debug")] @@ -35,6 +37,10 @@ struct Args { /// Query string parameters to add to all URLs (pre-encoded, e.g., "name=First%20Last&key=value") #[arg(short = 'q', long = "query-string-all")] query_string_all: Option, + + /// jq expression to evaluate on JSON text messages for logging (e.g., ".message" or ".data.id") + #[arg(short = 'j', long = "jaq")] + jaq: Option, } #[tokio::main] @@ -58,6 +64,37 @@ async fn main() -> Result<(), Box> { fs::create_dir_all(&session_dir)?; info!("Created session directory: {}", session_dir.display()); + // Compile jaq filter if provided + let jaq_filter: Option> = if let Some(ref expr) = args.jaq { + let mut defs = ParseCtx::new(Vec::new()); + defs.insert_natives(jaq_core::core()); + defs.insert_defs(jaq_std::std()); + + let (parsed, errs) = jaq_parse::parse(expr, jaq_parse::main()); + if !errs.is_empty() { + let err_msgs: Vec = errs.iter().map(|e| format!("{:?}", e)).collect(); + return Err(format!("Failed to parse jaq expression: {}", err_msgs.join(", ")).into()); + } + + let parsed = parsed.ok_or("Failed to parse jaq expression")?; + let filter = defs.compile(parsed); + + if !defs.errs.is_empty() { + return Err( + format!( + "Failed to compile jaq expression ({} error(s))", + defs.errs.len() + ) + .into(), + ); + } + + info!("Using jaq filter: {}", expr); + Some(Arc::new(filter)) + } else { + None + }; + // Parse extra query params once if specified let extra_params: Vec<(String, String)> = args .query_string_all @@ -216,6 +253,7 @@ async fn main() -> Result<(), Box> { for (letter, url, ws_stream) in connections { let session_dir = session_dir.clone(); + let jaq_filter = jaq_filter.clone(); join_set.spawn(async move { let (_, mut read) = ws_stream.split(); @@ -226,10 +264,55 @@ async fn main() -> Result<(), Box> { Ok(message) => { match message { Message::Text(text) => { - let preview: String = text.chars().take(50).collect(); - let truncated = if text.len() > 50 { "..." } else { "" }; - info!("[{}:{}] Text: {}{}", letter, seq_num, preview, truncated); + // Determine what to log based on jaq filter + let log_content = if let Some(ref filter) = jaq_filter { + match serde_json::from_str::(&text) { + Ok(json_val) => { + let inputs = RcIter::new(core::iter::empty()); + let ctx = Ctx::new([], &inputs); + let out = + filter.run((ctx, Val::from(json_val))); + let results: Vec> = out.collect(); + let mut output_parts = Vec::new(); + for result in results { + match result { + Ok(val) => { + output_parts.push(val.to_string()); + } + Err(e) => { + warn!( + "[{}:{}] jaq error: {}", + letter, seq_num, e + ); + } + } + } + if output_parts.is_empty() { + "(no output)".to_string() + } else { + output_parts.join(", ") + } + } + Err(e) => { + warn!( + "[{}:{}] JSON parse error: {}", + letter, seq_num, e + ); + let preview: String = text.chars().take(50).collect(); + let truncated = if text.len() > 50 { "..." } else { "" }; + format!("{}{}", preview, truncated) + } + } + } else { + let preview: String = text.chars().take(50).collect(); + let truncated = if text.len() > 50 { "..." } else { "" }; + format!("{}{}", preview, truncated) + }; + + info!("[{}:{}] Text: {}", letter, seq_num, log_content); + + // Always write full message to file let filename = session_dir.join(format!("{}{}.txt", letter, seq_num)); if let Err(e) = fs::write(&filename, &text) {