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 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 diff --git a/Cargo.lock b/Cargo.lock index e5d701a..4661116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,17 +96,43 @@ 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" +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", @@ -122,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", @@ -134,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", @@ -147,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", @@ -161,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", @@ -187,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]] @@ -213,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", @@ -229,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]] @@ -288,13 +315,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 +345,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" @@ -311,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" @@ -326,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 = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +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" @@ -344,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" @@ -359,14 +413,30 @@ 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]] +name = "cel" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed39583e427bf41d93c28c7f27c943a93bcb8697220ff3575fd53a9e13f3814" +dependencies = [ + "antlr4rust", + "chrono", + "lazy_static", + "nom", + "pastey", + "regex", + "serde", + "thiserror 1.0.69", ] [[package]] @@ -377,18 +447,19 @@ 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", + "serde", "windows-link", ] @@ -400,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", @@ -410,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", @@ -423,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]] @@ -465,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", @@ -479,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", @@ -551,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", @@ -573,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", @@ -640,7 +717,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -651,7 +728,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -665,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" @@ -688,7 +762,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -718,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" @@ -765,10 +839,16 @@ 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", ] +[[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" @@ -804,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", ] @@ -844,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", @@ -870,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", @@ -940,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", @@ -1007,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" @@ -1031,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", @@ -1041,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", @@ -1051,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", @@ -1076,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", @@ -1153,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" @@ -1173,8 +1250,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", ] [[package]] @@ -1196,7 +1271,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1228,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", ] @@ -1256,7 +1330,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1271,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" @@ -1295,9 +1363,9 @@ dependencies = [ [[package]] name = "liblogjet" -version = "0.9.0" +version = "0.11.0" dependencies = [ - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "prost", "tokio", "tonic", @@ -1326,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]] @@ -1349,13 +1417,13 @@ 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", + "opentelemetry-proto 0.31.0", "prost", "rusqlite", "serde_json", @@ -1363,15 +1431,25 @@ 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.11.0" +dependencies = [ + "cel", + "logjet", + "opentelemetry-proto 0.31.0", + "prost", +] [[package]] name = "ljd" -version = "0.9.0" +version = "0.11.0" dependencies = [ "clap", "colored", @@ -1385,7 +1463,7 @@ dependencies = [ "liblogjet", "logjet", "lz4_flex", - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "prost", "rcgen", "rustls", @@ -1400,7 +1478,7 @@ dependencies = [ [[package]] name = "ljx" -version = "0.9.0" +version = "0.11.0" dependencies = [ "chrono", "clap", @@ -1408,25 +1486,26 @@ dependencies = [ "crossterm 0.28.1", "libloading", "liblogjet", + "ljcel", "logjet", - "opentelemetry-proto", + "opentelemetry-proto 0.32.0", "prost", "ratatui", "regex", "serde", "serde_json", - "shlex", + "shlex 1.3.0", "xxhash-rust", ] [[package]] name = "ljx-parquet-exporter" -version = "0.9.0" +version = "0.11.0" dependencies = [ "arrow-array", "arrow-schema", "liblogjet", - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "parquet", "prost", "serde_json", @@ -1443,31 +1522,31 @@ 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" -version = "0.9.0" +version = "0.11.0" dependencies = [ "arrow-array", "crc32c", "lz4_flex", - "opentelemetry-proto", + "opentelemetry-proto 0.32.0", "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]] @@ -1494,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" @@ -1537,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", @@ -1547,13 +1626,22 @@ 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" 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", @@ -1572,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", @@ -1591,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" @@ -1603,7 +1691,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1656,7 +1744,21 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.19", + "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", ] @@ -1668,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", @@ -1677,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" @@ -1686,10 +1804,26 @@ dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry", + "opentelemetry 0.31.0", "percent-encoding", - "rand 0.9.4", - "thiserror 2.0.18", + "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]] @@ -1712,13 +1846,13 @@ dependencies = [ [[package]] name = "otlp-demo" -version = "0.9.0" +version = "0.11.0" dependencies = [ "colored", "liblogjet", "logjet", "lz4_flex", - "opentelemetry-proto", + "opentelemetry-proto 0.31.0", "prost", "rustls", "rustls-pemfile", @@ -1727,6 +1861,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" @@ -1752,9 +1910,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", @@ -1784,6 +1942,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" @@ -1802,9 +1966,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", @@ -1812,9 +1976,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", @@ -1822,25 +1986,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]] @@ -1870,7 +2033,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]] @@ -1883,7 +2046,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1912,7 +2075,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1929,9 +2092,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" @@ -1948,21 +2111,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", ] @@ -1973,9 +2126,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", @@ -1984,9 +2137,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", @@ -1994,22 +2147,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", ] @@ -2028,18 +2181,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", @@ -2081,33 +2234,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", @@ -2115,9 +2272,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", @@ -2127,19 +2284,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", @@ -2147,17 +2315,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", @@ -2182,14 +2351,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", @@ -2199,9 +2368,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", @@ -2210,9 +2379,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" @@ -2234,7 +2403,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", @@ -2257,7 +2426,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", @@ -2270,7 +2439,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", @@ -2279,9 +2448,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", @@ -2304,9 +2473,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", ] @@ -2325,9 +2494,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" @@ -2355,9 +2524,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", @@ -2365,29 +2534,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", @@ -2426,6 +2595,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" @@ -2459,9 +2634,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" @@ -2477,15 +2652,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", @@ -2505,23 +2680,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]] @@ -2543,9 +2718,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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 = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -2558,6 +2744,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" @@ -2597,7 +2796,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.11.1", + "bitflags 2.13.1", "fancy-regex", "filedescriptor", "finl_unicode", @@ -2642,11 +2841,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]] @@ -2657,18 +2856,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]] @@ -2684,9 +2883,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", @@ -2699,9 +2898,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" @@ -2726,9 +2925,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", @@ -2741,13 +2940,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]] @@ -2762,9 +2961,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", @@ -2773,13 +2972,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", ] @@ -2875,7 +3075,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2895,15 +3095,21 @@ 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 = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" + +[[package]] +name = "typed-arena" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +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" @@ -2925,9 +3131,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" @@ -2946,12 +3152,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" @@ -2972,12 +3172,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", ] @@ -3020,27 +3220,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" 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", @@ -3051,9 +3242,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", @@ -3061,60 +3252,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" @@ -3230,7 +3387,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3241,7 +3398,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3359,105 +3516,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" @@ -3470,35 +3539,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/Cargo.toml b/Cargo.toml index 3fc3183..fc98e07 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", @@ -19,7 +20,7 @@ members = [ ] [workspace.package] -version = "0.9.0" +version = "0.11.0" edition = "2024" license = "Apache-2.0" default-members = [ @@ -28,6 +29,7 @@ default-members = [ "demo", "ljx", "liblogjet", + "ljcel", "plugins/syslog-ingest", "plugins/logcat-ingest", "plugins/stress-ingest", @@ -50,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/Makefile b/Makefile index 8e269c5..32d152f 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: + cargo clippy --workspace --all-targets --all-features -- -D warnings -A clippy::needless-update check: setup - cargo clippy --workspace --all-targets --all-features -- -D warnings + @if [ -n "$$SSH_CONNECTION" ]; then $(MAKE) _check; else scripts/maybe-mxrun.sh check || $(MAKE) _check; fi -fix: setup +_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..5b79181 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-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) [![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. @@ -104,7 +106,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/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/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/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 diff --git a/ljcel/Cargo.toml b/ljcel/Cargo.toml new file mode 100644 index 0000000..13ef7bb --- /dev/null +++ b/ljcel/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ljcel" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +cel = "0.14" +logjet = { path = ".." } +opentelemetry-proto = { version = "0.31", features = ["gen-tonic", "logs", "metrics", "trace"] } +prost = "0.14" diff --git a/ljcel/src/context.rs b/ljcel/src/context.rs new file mode 100644 index 0000000..ee26077 --- /dev/null +++ b/ljcel/src/context.rs @@ -0,0 +1,269 @@ +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 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: cel::Value, + pub scope: cel::Value, + pub attributes: cel::Value, +} + +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) 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: cel::Value, + pub scope: cel::Value, + pub attributes: cel::Value, +} + +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: cel::Value, + pub scope: cel::Value, + pub attributes: cel::Value, +} + +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(OtelValue::StringValue(s)) => Some(s.clone()), + _ => None, + }) + .unwrap_or_default() +} + +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 !seen.insert(attr.key.clone()) { + continue; + } + if let Some(value) = attr.value.as_ref() { + map.insert(Arc::new(attr.key.clone()), any_value_to_cel(value)); + } + } + cel::Value::Map(map.into()) + } + + +fn any_value_to_cel(value: &AnyValue) -> cel::Value { + match &value.value { + 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(OtelValue::KvlistValue(k)) => { + let mut map: HashMap, cel::Value> = HashMap::new(); + for item in &k.values { + if let Some(value) = item.value.as_ref() { + map.insert(Arc::new(item.key.clone()), any_value_to_cel(value)); + } + } + cel::Value::Map(map.into()) + } + None => cel::Value::String(Arc::new(String::new())), + } +} + +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..ca14373 --- /dev/null +++ b/ljcel/src/expression.rs @@ -0,0 +1,287 @@ +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, MetricDataPointContext, SpanContext, extract_service_name, hex_encode, key_values_to_cel_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_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_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(&[]), + ); + + 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_cel_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(AnyValueKind::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_cel_map(&lr.attributes), + }; + + if self.eval_log(&ctx)? { + return Ok(true); + } + } + } + } + Ok(false) + } + + 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_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(&[]), + ); + + 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_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 { + 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_cel_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_cel_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_cel_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_cel_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_cel_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_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(&[]), + ); + + 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_cel_map(ss.scope.as_ref().map(|s| s.attributes.as_slice()).unwrap_or(&[])); + + for span in &ss.spans { + 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()) + }); + + 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_cel_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()?; + 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:?}"))), + 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..1dc05a3 --- /dev/null +++ b/ljcel/src/lib.rs @@ -0,0 +1,10 @@ +mod context; +mod error; +mod expression; + +pub use expression::CelExpression; +pub use error::CelError; + +#[cfg(test)] +#[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..32e0fef --- /dev/null +++ b/ljcel/tests/unit/ljcel_ut.rs @@ -0,0 +1,410 @@ +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; + +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())) }), ..Default::default() } +} + +fn int_attr(key: &str, value: i64) -> KeyValue { + 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 { + 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 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(); + 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()); +} + +// --- 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())) }), + ..Default::default() + }], + 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())) }), + ..Default::default() + }], + 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/Cargo.toml b/ljx/Cargo.toml index facef30..ccdc036 100644 --- a/ljx/Cargo.toml +++ b/ljx/Cargo.toml @@ -11,8 +11,9 @@ 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"] } +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/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/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/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/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; } } 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/src/predicate.rs b/ljx/src/predicate.rs index ba071fa..88f5ef0 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_payload(record.record_type, &record.payload).unwrap_or(false) { + return false; + } + } if !self.field_filter.matches_payload(&record.payload) { return false; } 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()]); +} 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 44f7568..cd258e6 100644 --- a/ljx/tests/unit/predicate_ut.rs +++ b/ljx/tests/unit/predicate_ut.rs @@ -5,6 +5,51 @@ 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())) }), + ..Default::default() + }], + 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 +149,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)); +} 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() }) } diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c64c7ce --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,56 @@ +site_name: Logjet +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 + - 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/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" diff --git a/scripts/maybe-mxrun.sh b/scripts/maybe-mxrun.sh new file mode 100755 index 0000000..cc4568b --- /dev/null +++ b/scripts/maybe-mxrun.sh @@ -0,0 +1,34 @@ +#!/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; } + 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 + +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." 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,