diff --git a/Cargo.lock b/Cargo.lock index c8d0e56e..13a9cb6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,7 +121,7 @@ dependencies = [ "either", "k256", "once_cell", - "rand 0.8.7", + "rand 0.8.8", "secp256k1 0.30.0", "serde", "serde_json", @@ -170,9 +170,9 @@ dependencies = [ [[package]] name = "alloy-core" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8421a5ee9019b6d89f92935d0f8facd9f6ca088b41d7cc47244a02ccde4545f" +checksum = "e88cf3d065edfb29a13278215b8521d3ef72a41e2432e019c1f0dd8e30649a5d" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a04eb4abc2b5074a18e687ee63918f407cc7990083cba9b999445f839796060" +checksum = "d9f1a3f2206f2ba4206fdeeddce6640eed3e26b8a13ac41444adb66b76d8e650" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cee30dd4c2f4b23f434fdf675e7bf9681b86768141277266c6f548ef25cba0a" +checksum = "208699c66c453fbb4c50d2e602f8ceff8a5f1fa48ac8b6ee3b6357fdc93da311" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -369,9 +369,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f007e257069855bdf21d27762fd3f3705a613f805c9a08309bf353503f081d71" +checksum = "9c902f0ca3f8353c41e3e1ec3cf26be49412525bc48ab9d3c4710d7be4f01832" dependencies = [ "alloy-rlp", "bytes", @@ -381,7 +381,7 @@ dependencies = [ "fixed-cache", "foldhash 0.2.0", "hashbrown 0.17.1", - "indexmap 2.14.0", + "indexmap 2.14.1", "itoa", "k256", "keccak-asm", @@ -566,15 +566,15 @@ dependencies = [ "alloy-signer", "async-trait", "k256", - "rand 0.8.7", + "rand 0.8.8", "thiserror 2.0.20", ] [[package]] name = "alloy-sol-macro" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5655c38d5f84955bf727b2eeb62fddd91ebb98fd1d7ae6eb77f73ea88f9b9cf" +checksum = "fdcbd48d60e029be4a325c3a2f1312761caea4ed249f18ba9e8ed24ca1bf01e6" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -586,15 +586,15 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6277c780e07b76951e09a59788dde230d1582612324177d11a43a61e21a6bb83" +checksum = "59c9f7c535f99a7e7b64cc520968b09ed14cec3715572fcc277cfbff602808cd" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.14.0", + "indexmap 2.14.1", "proc-macro-error3", "proc-macro2", "quote", @@ -605,9 +605,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9762b2ad3e5a0c09886de54fe549ab0056681df843cb082e2df7e1c0eb270d30" +checksum = "1abd404fbc12f543823005146b73fd07621bdc0baaa950d26995c543a9d73811" dependencies = [ "alloy-json-abi", "const-hex", @@ -623,9 +623,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da4c7130f0f01f4719678bda3db3bc7267fc2f7f9d0565e3bd964cd2bb45050d" +checksum = "40a7fd71864526bfeca8903010d5bb7fd28a0a4f5cc55818304c9cad8f0d63ab" dependencies = [ "serde", "winnow", @@ -633,9 +633,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96e74d6213180f78dbdccddce8af02a639c160c94b0a543fa35c77c58b8a7fc" +checksum = "adfc2ba3fb0e865de4934bcad6d37fc51e9ffcd5294be1322eab38e4494e051b" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -704,7 +704,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d0626c4f3b2028f7e8db32f53c22d9ecd5939f772317b7c4b6fe5219cb0589a" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.119", @@ -1011,7 +1011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand 0.8.7", + "rand 0.8.8", ] [[package]] @@ -1021,7 +1021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.7", + "rand 0.8.8", ] [[package]] @@ -1031,7 +1031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand 0.8.7", + "rand 0.8.8", ] [[package]] @@ -1041,7 +1041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" dependencies = [ "num-traits", - "rand 0.8.7", + "rand 0.8.8", ] [[package]] @@ -1175,7 +1175,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1544,34 +1544,32 @@ dependencies = [ [[package]] name = "bon" -version = "3.9.3" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +checksum = "9e3fac94a66da67200398458a25412bcc3f9b6443b5119a6cad9cf3ccfcd8cc6" dependencies = [ "bon-macros", - "rustversion", ] [[package]] name = "bon-macros" -version = "3.9.3" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" +checksum = "d4654961ad0494e4774c5c60b4cb4cd0ae9b9d92d039d901638b1dba97ebebf5" dependencies = [ - "darling", + "darling 0.24.1", "ident_case", - "prettyplease", + "prettyplease 0.3.0", "proc-macro2", "quote", - "rustversion", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "borsh" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" dependencies = [ "borsh-derive", "bytes", @@ -1580,15 +1578,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -1679,9 +1677,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -1714,12 +1712,12 @@ dependencies = [ [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -1819,7 +1817,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1845,9 +1843,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -1959,9 +1957,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -1983,9 +1981,9 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -2177,8 +2175,18 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core 0.24.1", + "darling_macro 0.24.1", ] [[package]] @@ -2195,17 +2203,41 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.4", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn 2.0.119", ] +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core 0.24.1", + "quote", + "syn 3.0.4", +] + [[package]] name = "dashmap" version = "6.2.1" @@ -2243,7 +2275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2401,7 +2433,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2566,7 +2598,7 @@ checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2640,7 +2672,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf022360bdbe9456eda5f35718a50476d5b2a0d51a97ed4eae27420737a6fba" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.119", @@ -2757,7 +2789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand 0.8.7", + "rand 0.8.8", "rustc-hex", "static_assertions", ] @@ -2770,12 +2802,13 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -2893,7 +2926,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3051,9 +3084,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -3061,7 +3094,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.14.0", + "indexmap 2.14.1", "slab", "tokio", "tokio-util", @@ -3294,9 +3327,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -3491,9 +3524,9 @@ checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -3618,9 +3651,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -3844,12 +3877,12 @@ dependencies = [ [[package]] name = "keccak" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -3891,9 +3924,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libgit2-sys" -version = "0.18.7+1.9.6" +version = "0.18.8+1.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c7391e4b9f4ffab1a624223cc1d7385ff9a678f490768add717de7ea2f4d89" +checksum = "7f7c568b25d7489bc3fb2988ed69ab111d2944d2f5fec3d5c987fe545ea97b50" dependencies = [ "cc", "libc", @@ -3970,7 +4003,7 @@ dependencies = [ "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.7", + "rand 0.8.8", "rand_core 0.6.4", "thiserror 2.0.20", "tracing", @@ -4005,7 +4038,7 @@ dependencies = [ "parking_lot", "pin-project", "quick-protobuf", - "rand 0.8.7", + "rand 0.8.8", "rw-stream-sink", "thiserror 2.0.20", "tracing", @@ -4063,7 +4096,7 @@ dependencies = [ "k256", "multihash", "prost 0.14.4", - "rand 0.8.7", + "rand 0.8.8", "sha2", "thiserror 2.0.20", "tracing", @@ -4082,7 +4115,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand 0.8.7", + "rand 0.8.8", "smallvec", "socket2 0.5.10", "tokio", @@ -4121,7 +4154,7 @@ dependencies = [ "multiaddr", "multihash", "quick-protobuf", - "rand 0.8.7", + "rand 0.8.8", "snow", "static_assertions", "thiserror 2.0.20", @@ -4141,7 +4174,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand 0.8.7", + "rand 0.8.8", "tracing", "web-time", ] @@ -4160,7 +4193,7 @@ dependencies = [ "libp2p-tls", "quinn", "quinn-proto", - "rand 0.8.7", + "rand 0.8.8", "ring", "rustls", "socket2 0.5.10", @@ -4186,7 +4219,7 @@ dependencies = [ "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.7", + "rand 0.8.8", "static_assertions", "thiserror 2.0.20", "tracing", @@ -4205,7 +4238,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand 0.8.7", + "rand 0.8.8", "smallvec", "tracing", ] @@ -4225,7 +4258,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm-derive", "multistream-select", - "rand 0.8.7", + "rand 0.8.8", "smallvec", "tokio", "tracing", @@ -4343,9 +4376,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "loki-api" @@ -4359,9 +4392,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" +checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c" dependencies = [ "hashbrown 0.17.1", ] @@ -4385,9 +4418,9 @@ dependencies = [ [[package]] name = "match-lookup" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +checksum = "549e39695cc0b640f3cb378053832db3d2133422d49e8dcae5c866a2aaf1f730" dependencies = [ "proc-macro2", "quote", @@ -4439,9 +4472,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -4784,11 +4817,11 @@ dependencies = [ "eventsource-stream", "futures-core", "http", - "indexmap 2.14.0", + "indexmap 2.14.1", "oas3", - "prettyplease", + "prettyplease 0.2.37", "proc-macro2", - "quick-xml", + "quick-xml 0.42.0", "quote", "regex", "reqwest 0.13.4", @@ -4998,7 +5031,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.14.0", + "indexmap 2.14.1", ] [[package]] @@ -5128,7 +5161,7 @@ dependencies = [ "pluto-testutil", "prost 0.14.4", "prost-types 0.14.4", - "rand 0.8.7", + "rand 0.8.8", "regex", "reqwest 0.13.4", "serde", @@ -5181,8 +5214,8 @@ dependencies = [ "pluto-ssz", "pluto-testutil", "pluto-tracing", - "quick-xml", - "rand 0.8.7", + "quick-xml 0.41.0", + "rand 0.8.8", "reqwest 0.13.4", "serde", "serde_json", @@ -5216,7 +5249,7 @@ dependencies = [ "pluto-p2p", "pluto-ssz", "pluto-testutil", - "rand 0.8.7", + "rand 0.8.8", "reqwest 0.13.4", "serde", "serde_json", @@ -5301,7 +5334,7 @@ dependencies = [ "pluto-tracing", "prost 0.14.4", "prost-types 0.14.4", - "rand 0.8.7", + "rand 0.8.8", "regex", "reqwest 0.13.4", "serde", @@ -5325,7 +5358,7 @@ dependencies = [ "blst", "hex", "pluto-eth2api", - "rand 0.8.7", + "rand 0.8.8", "rand_core 0.6.4", "test-case", "thiserror 2.0.20", @@ -5363,7 +5396,7 @@ dependencies = [ "pluto-tracing", "prost 0.14.4", "prost-types 0.14.4", - "rand 0.8.7", + "rand 0.8.8", "serde", "serde_json", "sha2", @@ -5440,7 +5473,7 @@ dependencies = [ "pluto-k1util", "pluto-ssz", "pluto-testutil", - "rand 0.8.7", + "rand 0.8.8", "regex", "reqwest 0.13.4", "scrypt", @@ -5479,7 +5512,7 @@ version = "1.7.1" dependencies = [ "blst", "hex", - "rand 0.8.7", + "rand 0.8.8", "rand_core 0.6.4", "serde", "serde_json", @@ -5544,7 +5577,7 @@ dependencies = [ "pluto-testutil", "pluto-tracing", "prost 0.14.4", - "rand 0.8.7", + "rand 0.8.8", "reqwest 0.13.4", "serde_json", "tempfile", @@ -5639,7 +5672,7 @@ dependencies = [ "pluto-testutil", "prost 0.14.4", "prost-types 0.14.4", - "rand 0.8.7", + "rand 0.8.8", "test-case", "thiserror 2.0.20", "tokio", @@ -5660,7 +5693,7 @@ dependencies = [ "pluto-eth2util", "pluto-p2p", "pluto-tracing", - "rand 0.8.7", + "rand 0.8.8", "reqwest 0.13.4", "serde_json", "thiserror 2.0.20", @@ -5700,7 +5733,7 @@ dependencies = [ "pluto-crypto", "pluto-eth2api", "pluto-eth2util", - "rand 0.8.7", + "rand 0.8.8", "reqwest 0.13.4", "serde", "serde_json", @@ -5813,6 +5846,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.4", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -5933,7 +5976,7 @@ dependencies = [ "log", "multimap", "petgraph", - "prettyplease", + "prettyplease 0.2.37", "prost 0.14.4", "prost-types 0.14.4", "regex", @@ -6017,6 +6060,16 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quinn" version = "0.11.11" @@ -6105,9 +6158,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -6132,7 +6185,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20 0.10.1", + "chacha20 0.10.2", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -6268,7 +6321,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -6461,7 +6514,7 @@ dependencies = [ "parity-scale-codec", "primitive-types", "proptest", - "rand 0.8.7", + "rand 0.8.8", "rand 0.9.5", "rlp", "ruint-macro", @@ -6595,9 +6648,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -6719,7 +6772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", - "rand 0.8.7", + "rand 0.8.8", "secp256k1-sys 0.10.1", "serde", ] @@ -6840,7 +6893,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -6849,7 +6902,7 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.1", "itoa", "memchr", "serde", @@ -6876,7 +6929,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -6911,7 +6964,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.0", + "indexmap 2.14.1", "jiff", "schemars 0.9.0", "schemars 1.2.2", @@ -6927,7 +6980,7 @@ version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.119", @@ -6939,7 +6992,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.1", "itoa", "ryu", "serde", @@ -6984,7 +7037,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ "digest 0.11.3", - "keccak 0.2.1", + "keccak 0.2.2", ] [[package]] @@ -7199,9 +7252,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -7210,9 +7263,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "083be3061e64d362cbe6ef12cfe1307ba3884326d8856448fe8a120fa2c44ebf" +checksum = "e452eb8cb83fc8b81597eb07c8d39f770d04905af9c5bffce8bea7213df29960" dependencies = [ "paste", "proc-macro2", @@ -7412,7 +7465,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -7523,7 +7576,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -7569,7 +7622,7 @@ version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.1", "serde_core", "serde_spanned", "toml_datetime", @@ -7593,7 +7646,7 @@ version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.1", "toml_datetime", "toml_parser", "winnow", @@ -7662,7 +7715,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.14.0", + "indexmap 2.14.1", "pin-project-lite", "slab", "sync_wrapper", @@ -7814,7 +7867,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8840ad4d852e325d3afa7fde8a50b2412f89dce47d7eb291c0cc7f87cd040f38" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.119", @@ -7987,9 +8040,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.1" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -8019,7 +8072,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240e4b81c20a1d6d50d1d7265c658dfbd204e8b9ac4d80f3c931f39462196335" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro-error3", "proc-macro2", "quote", @@ -8646,7 +8699,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.8.7", + "rand 0.8.8", "static_assertions", ] @@ -8783,15 +8836,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" diff --git a/crates/cli/README.md b/crates/cli/README.md index 0e32001e..72526f22 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -6,7 +6,7 @@ Pluto enables the operation of Ethereum validators in a fault tolerant manner by ## Commands (current) -Most flags below also read a `CHARON_*` environment variable (for example `--beacon-node-endpoints` reads `CHARON_BEACON_NODE_ENDPOINTS`), mirroring charon's environment surface. Exceptions: none of the `alpha test` flags have env bindings, nor do `enr --verbose`, `version --verbose` and `relay --log-color`. A variable that is set but empty is treated as unset. Run `pluto --help` for the authoritative list — flags with a binding show an `[env: ...]` line. +Most flags below also read a `CHARON_*` environment variable (for example `--beacon-node-endpoints` reads `CHARON_BEACON_NODE_ENDPOINTS`), mirroring charon's environment surface. A variable that is set but empty is treated as unset. Run `pluto --help` for the authoritative list — flags with a binding show an `[env: ...]` line. Some flags are accepted for charon compatibility but are not yet wired up. They are marked below as **[IGNORED]** (parsed, then dropped — `pluto run` logs a warning for most of them) or **[UNSUPPORTED]** (setting the flag makes the command exit at startup with an error). @@ -59,7 +59,7 @@ Starts the long-running Pluto middleware process to perform distributed validato - `--testnet-genesis-timestamp `: Genesis timestamp of the custom test network. - `--testnet-capella-hard-fork `: Capella hard fork version of the custom test network. - The custom network is only registered when the testnet flags are fully specified; a partial set is silently ignored and pluto falls back to the built-in network registry. -- Plus the [common P2P flags](#common-p2p-flags) and [common logging flags](#common-logging-flags) (including the Loki flags). +- Plus the [common P2P flags](#common-p2p-flags) and [common logging flags](#common-logging-flags). ### `pluto relay` @@ -69,13 +69,12 @@ Starts a libp2p circuit relay that charon clients can use to discover and connec - `--data-dir `: The directory where pluto will store all its internal data. (default: `.charon`) - `--http-address `: Listening address (ip and port) for the relay http server serving runtime ENR. (default: `127.0.0.1:3640`) - `--auto-p2pkey`: Automatically generate and persist a p2p key if one does not exist. Always on: it defaults to true and cannot be switched off on the command line (`--auto-p2pkey=false` is rejected); set `CHARON_AUTO_P2PKEY=false` to require an existing key. - - `--p2p-relay-loglevel `: Log level for the upstream `libp2p_relay` crate, letting its logs be quieted (`--p2p-relay-loglevel=error`) without lowering pluto's own verbosity. Takes the same values as `--log-level`; when unset the relay crate follows `--log-level`. - `--p2p-max-reservations `: Updates max circuit reservations per peer (each valid for 1 hour). (default: `512`) - `--p2p-max-connections `: Currently applied as the relay's total reservation limit; it does not cap inbound connections. (default: `16384`) - `--p2p-advertise-private-addresses`: Enable advertising of libp2p auto-detected private addresses. - `--monitoring-address `: Listening address (ip and port) for the monitoring API (prometheus). - `--debug-address `: **[IGNORED]** Parsed but no debug listener is started (no warning is emitted). -- Plus the [common P2P flags](#common-p2p-flags) and [common logging flags](#common-logging-flags) (including the Loki flags). Note that `--p2p-relays` is accepted but unused by the relay itself. +- Plus the [common P2P flags](#common-p2p-flags) and [common logging flags](#common-logging-flags). Note that `--p2p-relays` is accepted but unused by the relay itself. ### `pluto dkg` @@ -94,7 +93,7 @@ Participate in a distributed key generation ceremony for a specific cluster defi - `--publish-address `: The URL to publish the cluster to. (default: `https://api.obol.tech/v1`) - `--publish-timeout `: Timeout for publishing a cluster; increase for clusters with more than 200 validators. (default: `30s`) - `--zipped`: Create a tar archive compressed with gzip of the target directory after creation. -- Plus the [common P2P flags](#common-p2p-flags) and the [common logging flags](#common-logging-flags) (Loki flags are **not** available on `dkg`). +- Plus the [common P2P flags](#common-p2p-flags) and the [common logging flags](#common-logging-flags). ### `pluto enr` @@ -266,17 +265,19 @@ Shared by `run`, `relay`, `dkg` and `alpha test peers`. ### Common logging flags -Shared by `run`, `relay` and `dkg`. +Global: accepted by every command, and parsed identically before or after the subcommand (`pluto --log-level=debug run` and `pluto run --log-level=debug` are equivalent). -- `--log-format `: **[IGNORED]** Accepted but not yet applied — output is always console-formatted. (default: `console`) -- `--log-level `: Log level; `off`, `trace`, `debug`, `info`, `warn` or `error`. (default: `info`) -- `--log-color `: Log color; `auto`, `force` or `disable`. (default: `auto`) -- `--log-output-path `: **[IGNORED]** Accepted but not yet applied — no log file is written. +All log output goes to stderr, leaving each command's stdout free for its own data. -`run` and `relay` additionally support Loki output: +`RUST_LOG` is not consulted; `--log-level` (or its default) always decides the filter. +- `--log-format `: **[IGNORED]** Accepted but not yet applied — output is always console-formatted. (default: `console`) +- `--log-level `: Log level; `off`, `trace`, `debug`, `info`, `warn` or `error`. Charon accepts only the last four; the two extra levels are what `tracing`'s `EnvFilter` understands. (default: `info`) +- `--log-color `: Log color; `auto`, `force` or `disable`. `auto` means "unless `NO_COLOR` is set", not TTY detection. (default: `auto`) +- `--log-output-path `: **[IGNORED]** Accepted but not yet applied — no log file is written. - `--loki-addresses `: Enables sending of logfmt structured logs to a Loki log aggregation server, in addition to normal stderr logs. Only the first address is used; extra entries are ignored with a warning (charon fans out to every address). - `--loki-service `: Service label sent with logs to Loki. (default: `pluto`) +- `--p2p-relay-loglevel `: Log level for the upstream `libp2p_relay` crate, letting its logs be quieted (`--p2p-relay-loglevel=error`) without lowering pluto's own verbosity. Takes the same values as `--log-level`; when unset the relay crate follows `--log-level`. Charon scopes this to `relay`. ## Example diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 121a201b..2e72ba7a 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; use crate::commands::{ + common::TracingArgs, create_cluster::CreateClusterArgs, create_dkg::CreateDkgArgs, create_enr::CreateEnrArgs, @@ -29,6 +30,10 @@ pub struct Cli { /// The subcommand to execute. #[command(subcommand)] pub command: Commands, + + /// Logging flags, shared by every subcommand. + #[command(flatten)] + pub tracing: TracingArgs, } /// Available commands. diff --git a/crates/cli/src/commands/common.rs b/crates/cli/src/commands/common.rs index fd2b7cb2..c4e07bd1 100644 --- a/crates/cli/src/commands/common.rs +++ b/crates/cli/src/commands/common.rs @@ -1,6 +1,6 @@ //! Shared helpers for CLI commands. -use std::fmt; +use std::{collections::HashMap, fmt, path::PathBuf}; use pluto_p2p::config::RelayAddr; use tracing::warn; @@ -53,32 +53,156 @@ impl fmt::Display for LogLevel { } } -/// Builds a tracing configuration for CLI commands, optionally enabling Loki. +/// Adds a `libp2p_relay` directive to the `base` env filter, which `EnvFilter` +/// prefix-matches against every `libp2p_relay::*` target. +fn relay_filter(base: LogLevel, relay_level: Option) -> String { + match relay_level { + Some(level) => format!("{base},libp2p_relay={level}"), + None => base.to_string(), + } +} + +/// Log output encoding +#[derive(clap::ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum LogFormat { + /// Human-readable, optionally colored. + #[default] + Console, + /// Flat `key=value` pairs. + Logfmt, + /// One JSON object per event. + Json, +} + +/// Logging and Loki flags, accepted by every subcommand. /// -/// `loki` is `Some` when the caller wants events forwarded to a Loki endpoint -/// (e.g. via `--loki-addresses`), and `None` for commands that only need -/// console output. +/// These are `global`, so they parse identically before or after the +/// subcommand and are readable from the root [`crate::cli::Cli`] before any +/// command-specific config conversion runs. That ordering is what lets +/// `main` install the subscriber before validation starts. // TODO: wire `log-output-path` (file output) and `log-format` (logfmt/json) // into the tracing layers. `pluto_tracing` supports console + Loki only, so -// `run`/`dkg`/`relay` accept these flags but do not yet apply them. -pub fn build_console_tracing_config( - level: impl Into, - color: &ConsoleColor, - loki: Option, -) -> pluto_tracing::TracingConfig { - let mut builder = pluto_tracing::TracingConfig::builder().with_default_console(); - - builder = match color { - ConsoleColor::Auto => builder.console_with_ansi(std::env::var("NO_COLOR").is_err()), - ConsoleColor::Force => builder.console_with_ansi(true), - ConsoleColor::Disable => builder.console_with_ansi(false), - }; - - if let Some(loki) = loki { - builder = builder.loki(loki); +// these flags are accepted but not yet applied. +#[derive(clap::Args, Clone, Debug)] +#[command(next_help_heading = "Logging")] +pub struct TracingArgs { + #[arg( + long = "log-format", + env = "CHARON_LOG_FORMAT", + default_value = "console", + global = true, + ignore_case = true, + display_order = 1000, + help = "Log format; console, logfmt or json" + )] + pub log_format: LogFormat, + + #[arg( + long = "log-level", + env = "CHARON_LOG_LEVEL", + default_value = "info", + global = true, + ignore_case = true, + display_order = 1001, + help = "Log level" + )] + pub log_level: LogLevel, + + #[arg( + long = "log-color", + env = "CHARON_LOG_COLOR", + default_value = "auto", + global = true, + ignore_case = true, + display_order = 1002, + help = "Log color; auto, force, disable." + )] + pub log_color: ConsoleColor, + + #[arg( + long = "log-output-path", + env = "CHARON_LOG_OUTPUT_PATH", + global = true, + display_order = 1003, + help = "Path in which to write on-disk logs." + )] + pub log_output_path: Option, + + #[arg( + long = "loki-addresses", + env = "CHARON_LOKI_ADDRESSES", + value_delimiter = ',', + global = true, + display_order = 1004, + help = "Enables sending of logfmt structured logs to these Loki log aggregation server addresses. This is in addition to normal stderr logs." + )] + pub loki_addresses: Vec, + + #[arg( + long = "loki-service", + env = "CHARON_LOKI_SERVICE", + default_value = "pluto", + global = true, + display_order = 1005, + help = "Service label sent with logs to Loki." + )] + pub loki_service: String, + + #[arg( + long = "p2p-relay-loglevel", + env = "CHARON_P2P_RELAY_LOGLEVEL", + global = true, + ignore_case = true, + display_order = 1006, + help = "Libp2p circuit relay log level. Defaults to --log-level." + )] + pub p2p_relay_log_level: Option, +} + +impl TracingArgs { + /// Builds the subscriber configuration. + /// + /// Emits nothing: this runs before the subscriber exists, so any diagnostic + /// it produced would be dropped. Deferred warnings live in + /// [`TracingArgs::warn_unused`]. + pub fn tracing_config(&self) -> pluto_tracing::TracingConfig { + let ansi = match self.log_color { + ConsoleColor::Auto => std::env::var_os("NO_COLOR").is_none(), + ConsoleColor::Force => true, + ConsoleColor::Disable => false, + }; + + let mut builder = pluto_tracing::TracingConfig::builder() + .with_default_console() + .console_with_ansi(ansi) + .override_env_filter(relay_filter(self.log_level, self.p2p_relay_log_level)); + + // Only the first address is used; see `warn_unused`. + if let Some(loki_url) = self.loki_addresses.first() { + builder = builder.loki(pluto_tracing::LokiConfig { + loki_url: loki_url.clone(), + labels: HashMap::from([("service".to_string(), self.loki_service.clone())]), + extra_fields: HashMap::new(), + }); + } + + builder.build() } - builder.override_env_filter(level.into()).build() + /// Reports flag values that were accepted but not applied. + /// + /// Call once the subscriber is installed. + pub fn warn_unused(&self) { + // Charon fans logs out to every entry in `loki-addresses`, but + // `pluto_tracing::TracingConfig` supports a single Loki layer today. + let ignored = self.loki_addresses.len().saturating_sub(1); + if ignored > 0 { + warn!( + ignored, + "Additional --loki-addresses ignored; only the first is used" + ); + } + } } /// Parses the configured relay addresses, warning about insecure ones. @@ -120,6 +244,122 @@ pub fn parse_relay_addrs(relays: &[String]) -> std::result::Result::try_parse_from([ + "pluto", + "enr", + &format!("--log-level={level}"), + ]) + .unwrap_or_else(|err| panic!("--log-level={level} should parse: {err}")); + + assert_eq!( + cli.tracing.tracing_config().override_env_filter.as_deref(), + Some("debug") + ); + } + + for color in ["disable", "DISABLE", "Disable"] { + let cli = ::try_parse_from([ + "pluto", + "enr", + &format!("--log-color={color}"), + ]) + .unwrap_or_else(|err| panic!("--log-color={color} should parse: {err}")); + + assert!( + !cli.tracing + .tracing_config() + .console + .expect("console") + .with_ansi + ); + } + + for format in ["logfmt", "LOGFMT", "Logfmt"] { + let cli = ::try_parse_from([ + "pluto", + "enr", + &format!("--log-format={format}"), + ]) + .unwrap_or_else(|err| panic!("--log-format={format} should parse: {err}")); + + assert_eq!(cli.tracing.log_format, LogFormat::Logfmt); + } + } + + #[test] + fn log_flags_reject_unknown_values() { + for flag in [ + "--log-level=nonsense", + "--log-format=nonsense", + "--p2p-relay-loglevel=fatal", + ] { + let err = match ::try_parse_from(["pluto", "enr", flag]) { + Ok(_) => panic!("{flag} should be rejected"), + Err(err) => err, + }; + + assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue); + } + } + + /// Runs `f` with a subscriber that only lets `filter` through. + fn with_filter(filter: &str, f: impl FnOnce()) { + let filter = EnvFilter::from_str(filter).expect("relay filter should be a valid EnvFilter"); + tracing::subscriber::with_default(tracing_subscriber::registry().with(filter), f); + } + + #[test] + fn relay_filter_scopes_upstream_relay_logs() { + // An unset relay level leaves the base filter alone. + with_filter(&relay_filter(LogLevel::Info, None), || { + assert!(enabled!(target: "libp2p_relay::behaviour::handler", Level::WARN)); + }); + + // A relay level silences the upstream relay crate but not our own logs. + with_filter(&relay_filter(LogLevel::Info, Some(LogLevel::Error)), || { + assert!(!enabled!(target: "libp2p_relay::behaviour::handler", Level::WARN)); + assert!(enabled!(target: "pluto_relay_server::p2p", Level::INFO)); + }); + } + + #[test] + fn every_log_level_composes_into_a_valid_filter() { + for base in LogLevel::value_variants() { + for relay in LogLevel::value_variants() { + let filter = relay_filter(*base, Some(*relay)); + EnvFilter::from_str(&filter).unwrap_or_else(|e| panic!("{filter:?}: {e}")); + } + } + } + + #[test] + fn p2p_relay_loglevel_reaches_the_env_filter() { + // The flag is global, so it composes with `--log-level` from the root + // rather than from the `relay` subcommand that used to own it. + let cli = ::try_parse_from([ + "pluto", + "relay", + "--log-level=info", + "--p2p-relay-loglevel=error", + ]) + .expect("relay args should parse"); + + assert_eq!( + cli.tracing.tracing_config().override_env_filter.as_deref(), + Some("info,libp2p_relay=error") + ); + } // Per-address parsing is covered by `RelayAddr`'s own tests; what is left // to check here is the empty-value contract and the error wrapping. diff --git a/crates/cli/src/commands/dkg.rs b/crates/cli/src/commands/dkg.rs index cc806ca1..463e1344 100644 --- a/crates/cli/src/commands/dkg.rs +++ b/crates/cli/src/commands/dkg.rs @@ -3,7 +3,7 @@ use std::{future::Future, path::PathBuf}; use crate::{ - commands::common::{ConsoleColor, LICENSE, build_console_tracing_config, parse_relay_addrs}, + commands::common::{LICENSE, parse_relay_addrs}, duration::Duration, error::{CliError, Result}, }; @@ -56,9 +56,6 @@ pub struct DkgArgs { #[command(flatten)] pub p2p: DkgP2PArgs, - #[command(flatten)] - pub log: DkgLogArgs, - #[arg( long = "publish-address", env = "CHARON_PUBLISH_ADDRESS", @@ -123,8 +120,6 @@ impl TryFrom for pluto_dkg::dkg::Config { fn try_from(args: DkgArgs) -> Result { validate_p2p_args(&args.p2p)?; - let tracing_config = - build_console_tracing_config(args.log.level.clone(), &args.log.color, None); let p2p_config = { let relays = parse_relay_addrs(&args.p2p.relays)?; @@ -143,7 +138,6 @@ impl TryFrom for pluto_dkg::dkg::Config { .no_verify(args.no_verify) .data_dir(args.data_dir) .p2p(p2p_config) - .log(tracing_config) .keymanager( pluto_dkg::dkg::KeymanagerConfig::builder() .address(args.keymanager_address) @@ -217,41 +211,6 @@ pub struct DkgP2PArgs { pub disable_reuseport: bool, } -/// Logging arguments for the `dkg` command. -#[derive(clap::Args, Clone, Debug)] -pub struct DkgLogArgs { - #[arg( - long = "log-format", - env = "CHARON_LOG_FORMAT", - default_value = "console", - help = "Log format; console, logfmt or json" - )] - pub format: String, - - #[arg( - long = "log-level", - env = "CHARON_LOG_LEVEL", - default_value = "info", - help = "Log level; debug, info, warn or error" - )] - pub level: String, - - #[arg( - long = "log-color", - env = "CHARON_LOG_COLOR", - default_value = "auto", - help = "Log color; auto, force, disable." - )] - pub color: ConsoleColor, - - #[arg( - long = "log-output-path", - env = "CHARON_LOG_OUTPUT_PATH", - help = "Path in which to write on-disk logs." - )] - pub log_output_path: Option, -} - /// Runs the `dkg` command from an already-built configuration. pub async fn run(config: pluto_dkg::dkg::Config, ct: CancellationToken) -> Result<()> { run_with_runner(config, ct, pluto_dkg::dkg::run).await @@ -283,10 +242,13 @@ fn validate_p2p_args(args: &DkgP2PArgs) -> Result<()> { #[cfg(test)] mod tests { use super::*; - use crate::cli::{Cli, Commands}; + use crate::{ + cli::{Cli, Commands}, + commands::common::{LogFormat, LogLevel}, + }; use clap::Parser; use pluto_p2p::config::RelayAddr; - use std::{sync::Arc, time::Duration as StdDuration}; + use std::{path::Path, sync::Arc, time::Duration as StdDuration}; #[test] fn dkg_is_registered_as_top_level_subcommand() { @@ -302,6 +264,9 @@ mod tests { fn dkg_defaults_match_go() { let cli = Cli::try_parse_from(["pluto", "dkg"]).expect("dkg command should parse"); + assert_eq!(cli.tracing.log_level, LogLevel::Info); + assert_eq!(cli.tracing.log_format, LogFormat::Console); + let Commands::Dkg(args) = cli.command else { panic!("expected dkg command"); }; @@ -326,8 +291,6 @@ mod tests { args.p2p.relays, pluto_p2p::config::DEFAULT_RELAYS.map(String::from).to_vec(), ); - assert_eq!(args.log.level, "info"); - assert_eq!(args.log.format, "console"); } #[test] @@ -357,7 +320,6 @@ mod tests { ("keymanager-address", "CHARON_KEYMANAGER_ADDRESS"), ("keymanager-auth-token", "CHARON_KEYMANAGER_AUTH_TOKEN"), ("p2p-relays", "CHARON_P2P_RELAYS"), - ("log-level", "CHARON_LOG_LEVEL"), ("publish", "CHARON_PUBLISH"), ("publish-timeout", "CHARON_PUBLISH_TIMEOUT"), ("timeout", "CHARON_TIMEOUT"), @@ -406,6 +368,23 @@ mod tests { ]) .expect("dkg command should parse"); + // Log flags are global, so they land on the root rather than on `dkg`. + assert_eq!(cli.tracing.log_level, LogLevel::Debug); + assert_eq!(cli.tracing.log_format, LogFormat::Json); + assert_eq!( + cli.tracing.log_output_path.as_deref(), + Some(Path::new("/tmp/pluto.log")) + ); + let tracing_config = cli.tracing.tracing_config(); + assert_eq!(tracing_config.override_env_filter.as_deref(), Some("debug")); + assert!( + tracing_config + .console + .as_ref() + .expect("console config") + .with_ansi + ); + let Commands::Dkg(args) = cli.command else { panic!("expected dkg command"); }; @@ -430,9 +409,6 @@ mod tests { assert_eq!(config.p2p.tcp_addrs, vec!["0.0.0.0:9000".to_string()]); assert_eq!(config.p2p.udp_addrs, vec!["0.0.0.0:9000".to_string()]); assert!(config.p2p.disable_reuse_port); - assert_eq!(config.log.override_env_filter.as_deref(), Some("debug")); - let console = config.log.console.as_ref().expect("console config"); - assert!(console.with_ansi); assert!(config.publish.enabled); assert_eq!(config.publish.address, "https://api.example/v1"); assert_eq!(config.publish.timeout, StdDuration::from_secs(40)); @@ -453,6 +429,17 @@ mod tests { "--log-output-path=/tmp/pluto.log", ]) .expect("dkg command should parse"); + + let tracing_config = cli.tracing.tracing_config(); + assert_eq!(tracing_config.override_env_filter.as_deref(), Some("debug")); + assert!( + !tracing_config + .console + .as_ref() + .expect("console config") + .with_ansi + ); + let Commands::Dkg(args) = cli.command else { panic!("expected dkg command"); }; @@ -466,9 +453,6 @@ mod tests { move |config, token| async move { assert!(!token.is_cancelled()); assert_eq!(config.def_file, ".charon/cluster-definition.json"); - assert_eq!(config.log.override_env_filter.as_deref(), Some("debug")); - let console = config.log.console.as_ref().expect("console config"); - assert!(!console.with_ansi); events.lock().expect("lock").push("runner"); Ok(()) } diff --git a/crates/cli/src/commands/relay.rs b/crates/cli/src/commands/relay.rs index 49001f16..f6e371a1 100644 --- a/crates/cli/src/commands/relay.rs +++ b/crates/cli/src/commands/relay.rs @@ -1,27 +1,12 @@ use crate::{ - commands::common::{ - ConsoleColor, LICENSE, LogLevel, build_console_tracing_config, parse_relay_addrs, - }, + commands::common::{LICENSE, parse_relay_addrs}, error::CliError, }; use pluto_p2p::k1; -use std::{collections::HashMap, path::PathBuf, time::Duration}; +use std::path::PathBuf; use tokio_util::sync::CancellationToken; use tracing::{error, info}; -/// Grace period given to the Loki background task to flush buffered logs -/// once `BackgroundTaskController::shutdown` has been signalled. -const LOKI_FLUSH_TIMEOUT: Duration = Duration::from_secs(3); - -/// Adds a `libp2p_relay` directive to the `base` env filter, which `EnvFilter` -/// prefix-matches against every `libp2p_relay::*` target. -fn relay_filter(base: LogLevel, relay_level: Option) -> String { - match relay_level { - Some(level) => format!("{base},libp2p_relay={level}"), - None => base.to_string(), - } -} - /// Arguments for the relay command. #[derive(clap::Args, Clone)] pub struct RelayArgs { @@ -36,12 +21,6 @@ pub struct RelayArgs { #[clap(flatten)] pub p2p: RelayP2PArgs, - - #[clap(flatten)] - pub log: RelayLogFlags, - - #[clap(flatten)] - pub loki: RelayLokiArgs, } impl TryInto for RelayArgs { @@ -76,38 +55,6 @@ impl TryInto for RelayArgs { } }; - let loki_config = match self.loki.loki_addresses.as_slice() { - [] => None, - [loki_url, rest @ ..] => { - if !rest.is_empty() { - // Charon fans logs out to every entry in `loki-addresses`, but - // `pluto_tracing::TracingConfig` only supports a single Loki - // layer today. `tracing::warn!` would be a no-op here because - // no subscriber is installed yet (init happens later inside - // `commands::relay::run`), so write directly to stderr. - eprintln!( - "warning: {extra} additional --loki-addresses ignored; only the first is used", - extra = rest.len(), - ); - } - - let labels = - HashMap::from([("service".to_string(), self.loki.loki_service.clone())]); - - Some(pluto_tracing::LokiConfig { - loki_url: loki_url.clone(), - labels, - extra_fields: HashMap::new(), - }) - } - }; - - let log_config = build_console_tracing_config( - relay_filter(self.log.level, self.relay.p2p_relay_log_level), - &self.log.color, - loki_config, - ); - let builder = pluto_relay_server::config::Config::builder() .data_dir(self.data_dir.data_dir) .http_addr(self.relay.http_address) @@ -120,8 +67,7 @@ impl TryInto for RelayArgs { .filter_private_addrs(!self.relay.advertise_priv) .maybe_monitoring_addr(self.debug_monitoring.monitor_addr) .maybe_debug_addr(self.debug_monitoring.debug_addr) - .p2p_config(p2p_config) - .log_config(log_config); + .p2p_config(p2p_config); Ok(builder.build()) } @@ -156,14 +102,6 @@ pub struct RelayRelayArgs { )] pub auto_p2p_key: bool, - #[arg( - long = "p2p-relay-loglevel", - env = "CHARON_P2P_RELAY_LOGLEVEL", - ignore_case = true, - help = "Libp2p circuit relay log level. Defaults to --log-level." - )] - pub p2p_relay_log_level: Option, - // TODO: Check if https://github.com/libp2p/go-libp2p/issues/1713 is relevant for the Rust libp2p implementation // If so, decrease defaults after this has been addressed #[arg( @@ -258,112 +196,16 @@ pub struct RelayP2PArgs { pub disable_reuseport: bool, } -#[derive(clap::Args, Clone)] -pub struct RelayLogFlags { - #[arg( - long = "log-format", - env = "CHARON_LOG_FORMAT", - default_value = "console", - help = "Log format; console, logfmt or json" - )] - pub format: String, - - #[arg( - long = "log-level", - env = "CHARON_LOG_LEVEL", - default_value = "info", - ignore_case = true, - help = "Log level" - )] - pub level: LogLevel, - - #[arg(long = "log-color", default_value = "auto", help = "Log color")] - pub color: ConsoleColor, - - #[arg( - long = "log-output-path", - env = "CHARON_LOG_OUTPUT_PATH", - help = "Path in which to write on-disk logs." - )] - pub log_output_path: Option, -} - -#[derive(clap::Args, Clone)] -pub struct RelayLokiArgs { - #[arg( - long = "loki-addresses", - env = "CHARON_LOKI_ADDRESSES", - value_delimiter = ',', - help = "Enables sending of logfmt structured logs to these Loki log aggregation server addresses. This is in addition to normal stderr logs." - )] - pub loki_addresses: Vec, - - #[arg( - long = "loki-service", - env = "CHARON_LOKI_SERVICE", - default_value = "pluto", - help = "Service label sent with logs to Loki." - )] - pub loki_service: String, -} - pub async fn run( config: pluto_relay_server::config::Config, ct: CancellationToken, -) -> Result<(), CliError> { - let loki_shutdown = match pluto_tracing::init(&config.log_config) { - Ok(Some(loki)) => { - let controller = loki.controller; - let handle = tokio::spawn(loki.task); - Some((controller, handle)) - } - Ok(None) => None, - // In tests, the global tracing subscriber is shared across runs in the - // same process, so reinitializing fails. In production this would mean - // the relay silently uses an unrelated subscriber and Loki forwarding - // is dropped — fail loudly instead. - #[cfg(test)] - Err(pluto_tracing::init::Error::Init(_)) => None, - Err(err) => return Err(err.into()), - }; - - // Run the relay in an inner scope so every early `?` / `return Err(..)` is - // captured into `result` and the Loki cleanup below always runs. - let result = serve_relay(&config, ct).await; - - if let Err(err) = &result { - // Surface the shutdown reason through the subscriber so it reaches - // Loki before we close the worker; `main` only `eprintln!`s the - // returned error and that path bypasses the tracing subscriber. - error!(error = %err, "relay exited with error"); - } - - // Drain the Loki worker under a single budget so a hung Loki endpoint - // (e.g. `controller.shutdown` blocked on a full mpsc) cannot wedge - // process exit. After the budget elapses we hard-abort the worker. - if let Some((controller, handle)) = loki_shutdown { - let abort_handle = handle.abort_handle(); - let _ = tokio::time::timeout(LOKI_FLUSH_TIMEOUT, async { - controller.shutdown().await; - let _ = handle.await; - }) - .await; - abort_handle.abort(); - } - - result -} - -async fn serve_relay( - config: &pluto_relay_server::config::Config, - ct: CancellationToken, ) -> Result<(), CliError> { info!("{LICENSE}"); info!(config = ?config); - let key = load_or_create_key(config)?; + let key = load_or_create_key(&config)?; - pluto_relay_server::p2p::run_relay_p2p_node(config, key, ct) + pluto_relay_server::p2p::run_relay_p2p_node(&config, key, ct) .await .map_err(Into::into) } @@ -401,7 +243,6 @@ fn load_or_create_key( #[cfg(test)] mod tests { - use clap::{Parser as _, ValueEnum as _}; use std::{ net::{Ipv4Addr, SocketAddr}, path::Path, @@ -411,10 +252,6 @@ mod tests { }; use tokio::{net, task::JoinHandle}; use tokio_util::sync::CancellationToken; - use tracing::{Level, enabled}; - use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _}; - - use crate::cli::Cli; /// Args mirroring the clap defaults (notably `debug_addr: Some("")`), /// plus a TCP address so the baseline conversion succeeds. @@ -426,7 +263,6 @@ mod tests { relay: super::RelayRelayArgs { http_address: "127.0.0.1:3640".into(), auto_p2p_key: true, - p2p_relay_log_level: None, max_res_per_peer: 512, max_conns: 16384, advertise_priv: false, @@ -443,16 +279,6 @@ mod tests { udp_addrs: vec![], disable_reuseport: false, }, - log: super::RelayLogFlags { - format: "console".into(), - level: super::LogLevel::Error, - color: super::ConsoleColor::Disable, - log_output_path: None, - }, - loki: super::RelayLokiArgs { - loki_addresses: vec![], - loki_service: "pluto".into(), - }, } } @@ -522,9 +348,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let args = relay_args(dir.path()); - // Covers the CLI entry point that the fixture bypasses: tracing init and - // the Loki drain. A pre-cancelled token is deterministic because the - // shutdown arm of the serve loop is the `biased` first branch. + // A pre-cancelled token is deterministic because the shutdown arm of + // the serve loop is the `biased` first branch. let ct = CancellationToken::new(); ct.cancel(); @@ -768,7 +593,7 @@ mod tests { static CLIENT: LazyLock = LazyLock::new(reqwest::Client::new); /// Relay arguments every test starts from: all listeners on [`ANY_ADDR`], - /// quiet logs, no relays to dial. + /// no relays to dial. /// /// `advertise_priv` is load-bearing: without it, `filter_private_addrs` /// drops the loopback listen addresses, and `/enr` answers 500 forever. @@ -780,7 +605,6 @@ mod tests { relay: super::RelayRelayArgs { http_address: ANY_ADDR.into(), auto_p2p_key: true, - p2p_relay_log_level: None, max_res_per_peer: 0, max_conns: 0, advertise_priv: true, @@ -797,16 +621,6 @@ mod tests { udp_addrs: vec![ANY_ADDR.into()], disable_reuseport: false, }, - log: super::RelayLogFlags { - format: "console".into(), - level: super::LogLevel::Error, - color: super::ConsoleColor::Disable, - log_output_path: None, - }, - loki: super::RelayLokiArgs { - loki_addresses: vec![], - loki_service: "".into(), - }, } } @@ -944,46 +758,4 @@ mod tests { let addr = listener.local_addr().unwrap().to_string(); (listener, addr) } - - /// Runs `f` with a subscriber that only lets `filter` through. - fn with_filter(filter: &str, f: impl FnOnce()) { - let filter = EnvFilter::from_str(filter).expect("relay filter should be a valid EnvFilter"); - tracing::subscriber::with_default(tracing_subscriber::registry().with(filter), f); - } - - #[test] - fn relay_filter_scopes_upstream_relay_logs() { - // An unset relay level leaves the base filter alone. - with_filter(&super::relay_filter(super::LogLevel::Info, None), || { - assert!(enabled!(target: "libp2p_relay::behaviour::handler", Level::WARN)); - }); - - // A relay level silences the upstream relay crate but not our own logs. - with_filter( - &super::relay_filter(super::LogLevel::Info, Some(super::LogLevel::Error)), - || { - assert!(!enabled!(target: "libp2p_relay::behaviour::handler", Level::WARN)); - assert!(enabled!(target: "pluto_relay_server::p2p", Level::INFO)); - }, - ); - } - - #[test] - fn every_log_level_composes_into_a_valid_filter() { - for base in super::LogLevel::value_variants() { - for relay in super::LogLevel::value_variants() { - let filter = super::relay_filter(*base, Some(*relay)); - EnvFilter::from_str(&filter).unwrap_or_else(|e| panic!("{filter:?}: {e}")); - } - } - } - #[test] - fn unknown_log_level_is_rejected() { - let err = match Cli::try_parse_from(["pluto", "relay", "--p2p-relay-loglevel=fatal"]) { - Ok(_) => panic!("`fatal` is not an EnvFilter level"), - Err(err) => err, - }; - - assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue); - } } diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index 500e71fa..78c60c92 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -26,7 +26,6 @@ //! ignored with a warning. use std::{ - collections::HashMap, net::{SocketAddr, ToSocketAddrs}, path::{Path, PathBuf}, time::Duration as StdDuration, @@ -35,10 +34,10 @@ use std::{ use pluto_eth2util::helpers::validate_http_headers; use pluto_featureset::{Feature, FeaturesetError, Status}; use tokio_util::sync::CancellationToken; -use tracing::{error, info, warn}; +use tracing::{info, warn}; use crate::{ - commands::common::{ConsoleColor, LICENSE, build_console_tracing_config, parse_relay_addrs}, + commands::common::{LICENSE, parse_relay_addrs}, duration::Duration, error::{CliError, Result}, }; @@ -50,8 +49,6 @@ const MAX_GRAFFITI_BYTES: usize = 28; const MAX_GRAFFITI_BYTES_NO_APPEND: usize = 32; /// Maximum peer nickname length in bytes. const MAX_NICKNAME_BYTES: usize = 32; -/// Grace period for the Loki background task to flush buffered logs on exit. -const LOKI_FLUSH_TIMEOUT: StdDuration = StdDuration::from_secs(3); /// Default `--monitoring-address`. const DEFAULT_MONITORING_ADDR: &str = "127.0.0.1:3620"; /// Default `--simnet-validator-keys-dir` (matches Charon). @@ -83,12 +80,6 @@ pub struct RunArgs { #[command(flatten)] pub p2p: RunP2PArgs, - #[command(flatten)] - pub log: RunLogArgs, - - #[command(flatten)] - pub loki: RunLokiArgs, - #[command(flatten)] pub feature: RunFeatureArgs, } @@ -478,61 +469,6 @@ pub struct RunP2PArgs { pub disable_reuseport: bool, } -/// Logging flags. -#[derive(clap::Args, Clone, Debug)] -pub struct RunLogArgs { - #[arg( - long = "log-format", - env = "CHARON_LOG_FORMAT", - default_value = "console", - help = "Log format; console, logfmt or json" - )] - pub format: String, - - #[arg( - long = "log-level", - env = "CHARON_LOG_LEVEL", - default_value = "info", - help = "Log level; debug, info, warn or error" - )] - pub level: String, - - #[arg( - long = "log-color", - env = "CHARON_LOG_COLOR", - default_value = "auto", - help = "Log color; auto, force, disable." - )] - pub color: ConsoleColor, - - #[arg( - long = "log-output-path", - env = "CHARON_LOG_OUTPUT_PATH", - help = "Path in which to write on-disk logs." - )] - pub log_output_path: Option, -} - -/// Loki flags. -#[derive(clap::Args, Clone, Debug)] -pub struct RunLokiArgs { - #[arg( - long = "loki-addresses", - env = "CHARON_LOKI_ADDRESSES", - value_delimiter = ',', - help = "Enables sending of logfmt structured logs to these Loki log aggregation server addresses. This is in addition to normal stderr logs." - )] - pub loki_addresses: Vec, - - #[arg( - long = "loki-service", - env = "CHARON_LOKI_SERVICE", - default_value = "pluto", - help = "Service label sent with logs to Loki." - )] - pub loki_service: String, -} - /// Feature set flags. #[derive(clap::Args, Clone, Debug)] pub struct RunFeatureArgs { @@ -614,15 +550,13 @@ pub struct FeatureSetConfig { } /// Configuration for the `run` command — the settings produced from the parsed -/// flags, consumed by [`run`] (tracing/Loki) and bridged into +/// flags, consumed by [`run`] and bridged into /// [`pluto_app::node::AppConfig`] by [`build_app_config`]; `p2p_fuzz` is the /// single test-only field, set only via the hidden `unsafe run` command. #[derive(Debug)] pub struct RunConfig { /// P2P configuration built from [`RunP2PArgs`]. pub p2p: pluto_p2p::config::P2PConfig, - /// Tracing configuration built from [`RunLogArgs`]/[`RunLokiArgs`]. - pub log: pluto_tracing::TracingConfig, /// Feature set configuration. pub feature_set: FeatureSetConfig, /// Path to the cluster lock file. @@ -717,8 +651,6 @@ impl TryFrom for RunConfig { debug_monitoring, no_verify, p2p, - log, - loki, feature, } = args; @@ -791,12 +723,8 @@ impl TryFrom for RunConfig { disable_reuse_port: p2p.disable_reuseport, }; - let log_config = - build_console_tracing_config(log.level, &log.color, build_loki_config(&loki)); - Ok(Self { p2p: p2p_config, - log: log_config, feature_set: FeatureSetConfig { min_status: feature.feature_set, enabled: feature.feature_set_enable, @@ -895,78 +823,10 @@ fn validate_vc_tls(cert: &str, key: &str) -> Result<()> { Ok(()) } -/// Builds the optional Loki tracing configuration from the loki flags. -/// -/// Only a single Loki endpoint is supported today, so any extra -/// `--loki-addresses` entries are ignored with a warning. The warning goes to -/// stderr because no tracing subscriber is installed yet. -fn build_loki_config(loki: &RunLokiArgs) -> Option { - match loki.loki_addresses.as_slice() { - [] => None, - [loki_url, rest @ ..] => { - if !rest.is_empty() { - eprintln!( - "warning: {extra} additional --loki-addresses ignored; only the first is used", - extra = rest.len(), - ); - } - - Some(pluto_tracing::LokiConfig { - loki_url: loki_url.clone(), - labels: HashMap::from([("service".to_string(), loki.loki_service.clone())]), - extra_fields: HashMap::new(), - }) - } - } -} - /// Runs the `run` command from an already-built configuration. -/// -/// Initializes tracing and owns the Loki lifecycle for the command's lifetime: -/// when `--loki-addresses` is set, the background task is spawned here and -/// drained on exit so buffered logs are delivered. pub async fn run(config: RunConfig, ct: CancellationToken) -> Result<()> { - let loki_shutdown = match pluto_tracing::init(&config.log) { - Ok(Some(loki)) => Some((loki.controller, tokio::spawn(loki.task))), - Ok(None) => None, - // In tests the global subscriber is shared across runs in the same - // process, so reinitializing fails; treat that as "no Loki worker" - // rather than failing the command. - #[cfg(test)] - Err(pluto_tracing::init::Error::Init(_)) => None, - Err(err) => return Err(err.into()), - }; - info!("{LICENSE}"); - let result = run_workflow(config, ct).await; - - if let Err(err) = &result { - // Surface the failure through the subscriber so it reaches Loki before - // the worker is drained; `main` only `eprintln!`s the returned error - // and that path bypasses the tracing subscriber. - error!(error = %err, "run exited with error"); - } - - // Drain the Loki worker under a single budget so a hung endpoint cannot - // wedge process exit; hard-abort after the budget elapses. - if let Some((controller, handle)) = loki_shutdown { - let abort_handle = handle.abort_handle(); - let _ = tokio::time::timeout(LOKI_FLUSH_TIMEOUT, async { - controller.shutdown().await; - let _ = handle.await; - }) - .await; - abort_handle.abort(); - } - - result -} - -/// The long-running validator workflow: bridges the parsed [`RunConfig`] into -/// [`pluto_app::node::AppConfig`] and drives the node until `ct` fires (signal -/// handling lives in `main`). -async fn run_workflow(config: RunConfig, ct: CancellationToken) -> Result<()> { let app_config = build_app_config(config)?; pluto_app::node::App::new(app_config).run(ct).await?; Ok(()) @@ -998,7 +858,6 @@ fn build_app_config(config: RunConfig) -> Result { // bridge behavior fails to compile instead of being silently dropped. let RunConfig { p2p, - log: _, feature_set: _, lock_file, manifest_file: _, @@ -1096,7 +955,6 @@ fn check_unsupported_flags(config: &RunConfig) -> Result<()> { } /// Warns about observability/availability-only flags the run workflow ignores. -/// Runs after tracing init (see [`run`]) so the warnings reach the subscriber. fn warn_ignored_flags(config: &RunConfig) { if !config.debug_addr.is_empty() { warn!( @@ -1174,12 +1032,16 @@ fn parse_socket_addr(flag: &str, addr: &str) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::cli::{Cli, Commands, UnsafeCommands}; + use crate::{ + cli::{Cli, Commands, UnsafeCommands}, + commands::common::{LogFormat, LogLevel}, + }; use clap::{CommandFactory, Parser}; use std::{collections::BTreeSet, time::Duration as StdDuration}; - /// Every flag the safe `run` command must expose. - const EXPECTED_RUN_FLAGS: [&str; 54] = [ + /// Every flag the safe `run` command must expose itself. The log and Loki + /// flags are global and live on the root command. + const EXPECTED_RUN_FLAGS: [&str; 48] = [ // priv key "private-key-file", "private-key-file-lock", @@ -1230,14 +1092,6 @@ mod tests { "p2p-tcp-address", "p2p-udp-address", "p2p-disable-reuseport", - // log - "log-format", - "log-level", - "log-color", - "log-output-path", - // loki - "loki-addresses", - "loki-service", // feature "feature-set-enable", "feature-set-disable", @@ -1280,6 +1134,23 @@ mod tests { (*args).try_into() } + /// Parses safe `run` args and returns the tracing config the global log + /// flags produce. + fn parse_tracing(extra: &[&str]) -> pluto_tracing::TracingConfig { + let mut argv = vec![ + "pluto", + "run", + "--beacon-node-endpoints", + "http://beacon.node", + ]; + argv.extend_from_slice(extra); + + Cli::try_parse_from(argv) + .expect("run args should parse") + .tracing + .tracing_config() + } + /// Returns the `Display` string of the error from a failing `parse_run`. fn run_err(extra: &[&str]) -> String { parse_run(extra) @@ -1381,6 +1252,13 @@ mod tests { "http://beacon.node", ]) .expect("run command should parse"); + + // Log and Loki flags are global, so they land on the root. + assert_eq!(cli.tracing.log_level, LogLevel::Info); + assert_eq!(cli.tracing.log_format, LogFormat::Console); + assert_eq!(cli.tracing.loki_service, "pluto"); + assert!(cli.tracing.loki_addresses.is_empty()); + let Commands::Run(args) = cli.command else { panic!("expected run command"); }; @@ -1440,13 +1318,6 @@ mod tests { assert!(args.p2p.tcp_addrs.is_empty()); assert!(args.p2p.external_ip.is_none()); - // Log. - assert_eq!(args.log.level, "info"); - assert_eq!(args.log.format, "console"); - - // Loki. - assert_eq!(args.loki.loki_service, "pluto"); - // Feature. assert_eq!(args.feature.feature_set, "stable"); assert!(args.feature.feature_set_enable.is_empty()); @@ -1582,19 +1453,16 @@ mod tests { #[test] fn run_loki_config_built_from_addresses() { - // `--loki-addresses` must produce a Loki layer in the tracing config so - // `run` spawns/drains the Loki worker (regression for the lifecycle bug - // where the worker was initialized in `main` and then dropped). - let config = parse_run(&[ + // The Loki flags are global, but they must still reach the subscriber + // config when supplied on `run`. + let tracing_config = parse_tracing(&[ "--loki-addresses", "http://loki.test/push", "--loki-service", "svc", - ]) - .expect("config should build"); + ]); - let loki = config - .log + let loki = tracing_config .loki .as_ref() .expect("loki layer should be configured"); @@ -1602,13 +1470,7 @@ mod tests { assert_eq!(loki.labels.get("service").map(String::as_str), Some("svc")); // No `--loki-addresses` → no Loki layer (nothing to spawn). - assert!( - parse_run(&[]) - .expect("config should build") - .log - .loki - .is_none() - ); + assert!(parse_tracing(&[]).loki.is_none()); } #[test] @@ -1749,10 +1611,6 @@ mod tests { ); // p2p_fuzz is never set on the safe `run` path. assert!(!config.p2p_fuzz); - // `--log-color=force` forces ANSI on the console layer. - let console = config.log.console.as_ref().expect("console config"); - assert!(console.with_ansi); - assert_eq!(config.log.override_env_filter.as_deref(), Some("debug")); } /// Builds the app config from safe `run` flags. diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index c1ce261a..4bf2787b 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -116,14 +116,6 @@ pub enum CliError { #[error("Eth2util deposit error: {0}")] Eth2utilDepositError(#[from] pluto_eth2util::deposit::DepositError), - /// Tracing initialization error. - #[error("Tracing initialization error: {0}")] - TracingInitError(#[from] pluto_tracing::init::Error), - - /// Command parsing error. - #[error("Command parsing error: {0}")] - CommandParsingError(#[from] clap::Error), - /// Create DKG error. #[error("Create DKG error: {0}")] CreateDKGError(#[from] crate::commands::create_dkg::CreateDkgError), diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 79bb40a7..a7bb6326 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -9,6 +9,7 @@ use clap::FromArgMatches; use cli::{AlphaCommands, Cli, Commands, CreateCommands, TestCommands, UnsafeCommands}; use std::process::ExitCode; use tokio_util::sync::CancellationToken; +use tracing::error; mod ascii; mod cli; @@ -18,19 +19,43 @@ mod error; #[tokio::main] async fn main() -> ExitCode { - match run().await { + let matches = cli::build_command().get_matches(); + + let cli = match Cli::from_arg_matches(&matches) { + Ok(cli) => cli, + Err(err) => { + eprintln!("{err}"); + return ExitCode::FAILURE; + } + }; + + let loki = match pluto_tracing::init(&cli.tracing.tracing_config()) { + Ok(loki) => loki, + Err(err) => { + eprintln!("{err}"); + return ExitCode::FAILURE; + } + }; + cli.tracing.warn_unused(); + + let result = run(cli.command).await; + + let exit = match &result { Ok(()) => ExitCode::SUCCESS, Err(err) => { - eprintln!("Error: {}", err); + error!(error = %err, "command exited with error"); ExitCode::FAILURE } + }; + + if let Some(loki) = loki { + loki.shutdown().await; } -} -async fn run() -> std::result::Result<(), CliError> { - let matches = cli::build_command().get_matches(); - let cli = Cli::from_arg_matches(&matches)?; + exit +} +async fn run(command: Commands) -> std::result::Result<(), CliError> { // Top level cancellation token for graceful shutdown on Ctrl+C / SIGTERM. let ct = CancellationToken::new(); tokio::spawn({ @@ -61,23 +86,18 @@ async fn run() -> std::result::Result<(), CliError> { }); let mut stdout = std::io::stdout(); - match cli.command { - Commands::Create(args) => { - pluto_tracing::init(&pluto_tracing::TracingConfig::default()) - .expect("Failed to initialize tracing"); - match args.command { - CreateCommands::Dkg(args) => commands::create_dkg::run(*args).await, - CreateCommands::Enr(args) => commands::create_enr::run(args), - CreateCommands::Cluster(args) => { - commands::create_cluster::run(&mut stdout, *args).await - } + match command { + Commands::Create(args) => match args.command { + CreateCommands::Dkg(args) => commands::create_dkg::run(*args).await, + CreateCommands::Enr(args) => commands::create_enr::run(args), + CreateCommands::Cluster(args) => { + commands::create_cluster::run(&mut stdout, *args).await } - } + }, Commands::Enr(args) => commands::enr::run(args), Commands::Version(args) => commands::version::run(args), Commands::Dkg(args) => { let config: pluto_dkg::dkg::Config = (*args).try_into()?; - pluto_tracing::init(&config.log).expect("Failed to initialize tracing"); commands::dkg::run(config, ct).await } Commands::Relay(args) => { @@ -86,7 +106,6 @@ async fn run() -> std::result::Result<(), CliError> { } Commands::Run(args) => { let config: commands::run::RunConfig = (*args).try_into()?; - // Tracing/Loki init is owned by `commands::run::run`. commands::run::run(config, ct).await } Commands::Unsafe(args) => match args.command { @@ -96,32 +115,26 @@ async fn run() -> std::result::Result<(), CliError> { } }, Commands::Alpha(args) => match args.command { - AlphaCommands::Test(args) => { - pluto_tracing::init(&pluto_tracing::TracingConfig::default()) - .expect("Failed to initialize tracing"); - match args.command { - TestCommands::Peers(args) => commands::test::peers::run(args, &mut stdout, ct) + AlphaCommands::Test(args) => match args.command { + TestCommands::Peers(args) => commands::test::peers::run(args, &mut stdout, ct) + .await + .map(|_| ()), + TestCommands::Beacon(args) => commands::test::beacon::run(args, &mut stdout, ct) + .await + .map(|_| ()), + TestCommands::Validator(args) => { + commands::test::validator::run(args, &mut stdout, ct) .await - .map(|_| ()), - TestCommands::Beacon(args) => { - commands::test::beacon::run(args, &mut stdout, ct) - .await - .map(|_| ()) - } - TestCommands::Validator(args) => { - commands::test::validator::run(args, &mut stdout, ct) - .await - .map(|_| ()) - } - TestCommands::Mev(args) => commands::test::mev::run(args, &mut stdout, ct) - .await - .map(|_| ()), - TestCommands::Infra(args) => commands::test::infra::run(args, &mut stdout, ct) - .await - .map(|_| ()), - TestCommands::All(args) => commands::test::all::run(*args, &mut stdout).await, + .map(|_| ()) } - } + TestCommands::Mev(args) => commands::test::mev::run(args, &mut stdout, ct) + .await + .map(|_| ()), + TestCommands::Infra(args) => commands::test::infra::run(args, &mut stdout, ct) + .await + .map(|_| ()), + TestCommands::All(args) => commands::test::all::run(*args, &mut stdout).await, + }, }, } } diff --git a/crates/dkg/Cargo.toml b/crates/dkg/Cargo.toml index 0cca0559..4b7545b1 100644 --- a/crates/dkg/Cargo.toml +++ b/crates/dkg/Cargo.toml @@ -33,7 +33,6 @@ pluto-parsigex.workspace = true pluto-peerinfo.workspace = true pluto-frost.workspace = true async-trait.workspace = true -pluto-tracing.workspace = true hex.workspace = true rand.workspace = true serde.workspace = true diff --git a/crates/dkg/src/dkg.rs b/crates/dkg/src/dkg.rs index d5bc9d91..25d0383d 100644 --- a/crates/dkg/src/dkg.rs +++ b/crates/dkg/src/dkg.rs @@ -36,7 +36,6 @@ use pluto_eth2util::keymanager::{self, KeymanagerError}; use pluto_p2p::{ bootnode::BootnodeError, config::P2PConfig, k1::key_path, p2p::P2PError, peer::Peer, }; -use pluto_tracing::TracingConfig; use url::Url; const DEFAULT_DATA_DIR: &str = ".charon"; @@ -275,10 +274,6 @@ pub struct Config { #[builder(default = default_p2p_config())] pub p2p: P2PConfig, - /// Shared tracing configuration for the DKG entrypoint. - #[builder(default = default_tracing_config())] - pub log: pluto_tracing::TracingConfig, - /// Keymanager configuration. #[builder(default)] pub keymanager: KeymanagerConfig, @@ -369,13 +364,6 @@ fn default_p2p_config() -> P2PConfig { } } -fn default_tracing_config() -> TracingConfig { - TracingConfig::builder() - .with_default_console() - .override_env_filter("info") - .build() -} - /// Runs the DKG entrypoint. pub async fn run(conf: Config, ct: CancellationToken) -> Result<(), DkgError> { if ct.is_cancelled() { @@ -1091,8 +1079,6 @@ mod tests { assert!(!config.no_verify); assert_eq!(config.data_dir, path::PathBuf::from(DEFAULT_DATA_DIR)); assert_eq!(config.p2p.relays, pluto_p2p::config::default_relays()); - assert_eq!(config.log.override_env_filter.as_deref(), Some("info")); - assert!(config.log.console.is_some()); assert_eq!(config.publish.address, DEFAULT_PUBLISH_ADDRESS); assert_eq!(config.publish.timeout, DEFAULT_PUBLISH_TIMEOUT); assert!(!config.publish.enabled); diff --git a/crates/peerinfo/examples/peerinfo.rs b/crates/peerinfo/examples/peerinfo.rs index a96e1d68..02e9cae8 100644 --- a/crates/peerinfo/examples/peerinfo.rs +++ b/crates/peerinfo/examples/peerinfo.rs @@ -234,8 +234,8 @@ async fn main() -> anyhow::Result<()> { // Initialize tracing with optional Loki support let tracing_config = build_tracing_config(&args); - if let Some(loki) = pluto_tracing::init(&tracing_config)? { - tokio::spawn(loki.task); + let loki = pluto_tracing::init(&tracing_config)?; + if loki.is_some() { tracing::info!("Loki logging enabled"); } @@ -401,5 +401,9 @@ async fn main() -> anyhow::Result<()> { } } + if let Some(loki) = loki { + loki.shutdown().await; + } + Ok(()) } diff --git a/crates/relay-server/Cargo.toml b/crates/relay-server/Cargo.toml index 8e04354e..1818ecb3 100644 --- a/crates/relay-server/Cargo.toml +++ b/crates/relay-server/Cargo.toml @@ -19,7 +19,6 @@ vise.workspace = true tokio.workspace = true tokio-util.workspace = true rand.workspace = true -pluto-tracing.workspace = true tracing.workspace = true pluto-p2p.workspace = true pluto-core.workspace = true @@ -27,6 +26,7 @@ pluto-core.workspace = true [dev-dependencies] reqwest = { workspace = true } serde_json = { workspace = true } +pluto-tracing.workspace = true [lints] workspace = true diff --git a/crates/relay-server/src/config.rs b/crates/relay-server/src/config.rs index 245eb9fe..14a6f1ff 100644 --- a/crates/relay-server/src/config.rs +++ b/crates/relay-server/src/config.rs @@ -3,7 +3,6 @@ use std::{num::NonZeroU32, path::PathBuf, time::Duration}; use bon::Builder; use libp2p::relay; use pluto_p2p::config::P2PConfig; -use pluto_tracing::TracingConfig; /// One hour in seconds. pub const ONE_HOUR_SECONDS: u64 = 60 * 60; @@ -36,9 +35,6 @@ pub struct Config { pub debug_addr: Option, /// The P2P configuration. pub p2p_config: P2PConfig, - /// The logging configuration. - #[builder(default)] - pub log_config: TracingConfig, /// Whether to automatically generate a P2P key. #[builder(default = false)] pub auto_p2p_key: bool, diff --git a/crates/tracing/examples/basic.rs b/crates/tracing/examples/basic.rs index 29cb35b7..70955775 100644 --- a/crates/tracing/examples/basic.rs +++ b/crates/tracing/examples/basic.rs @@ -22,7 +22,6 @@ async fn main() { // Initialize tracing with default console config let config = TracingConfig::builder() .with_default_console() - .with_metrics(true) .loki(LokiConfig { loki_url: "http://localhost:3100".to_string(), labels: HashMap::new(), @@ -35,8 +34,6 @@ async fn main() { .expect("Failed to initialize tracing") .expect("Loki background task should be Some"); - tokio::spawn(loki.task); - let bind_address = SocketAddr::from(([0, 0, 0, 0], 9464)); let exporter = MetricsExporter::default() @@ -71,7 +68,9 @@ async fn main() { info!("Processing completed"); // Wait for 10 seconds to see the logs in Loki - std::thread::sleep(std::time::Duration::from_secs(10)); + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + + loki.shutdown().await; } #[instrument] diff --git a/crates/tracing/src/config.rs b/crates/tracing/src/config.rs index 3fde25b2..2223a77e 100644 --- a/crates/tracing/src/config.rs +++ b/crates/tracing/src/config.rs @@ -7,13 +7,10 @@ pub struct TracingConfig { /// logging is enabled. pub loki: Option, - /// Console configuration. Enables console logging if provided. If not - no - /// console logging is enabled. + /// Console layer options. Defaults are used when absent; console logging is + /// always enabled. pub console: Option, - /// Enables metrics logging. If not - no metrics logging is enabled. - pub metrics: bool, - /// Overrides the environment filter. If not - the environment filter is /// used. pub override_env_filter: Option, @@ -178,18 +175,6 @@ impl TracingConfigBuilder { self } - /// Enables metrics logging. - pub fn with_metrics(mut self, enabled: bool) -> Self { - self.tracing_config.metrics = enabled; - self - } - - /// Sets whether metrics logging is enabled. - pub fn metrics(mut self, enabled: bool) -> Self { - self.tracing_config.metrics = enabled; - self - } - /// Sets the environment filter override. pub fn override_env_filter(mut self, filter: impl Into) -> Self { self.tracing_config.override_env_filter = Some(filter.into()); diff --git a/crates/tracing/src/init.rs b/crates/tracing/src/init.rs index 8b116969..0b6469b2 100644 --- a/crates/tracing/src/init.rs +++ b/crates/tracing/src/init.rs @@ -1,8 +1,8 @@ -use std::str::FromStr; +use std::{str::FromStr, time::Duration}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use percent_encoding::percent_decode_str; -use tracing_loki::{BackgroundTask, BackgroundTaskController, url::Url}; +use tracing_loki::{BackgroundTaskController, url::Url}; use tracing_subscriber::{ EnvFilter, Registry, layer::SubscriberExt as _, util::SubscriberInitExt as _, }; @@ -27,23 +27,48 @@ pub enum Error { type Result = std::result::Result; -/// Loki background task plus the controller used to signal graceful shutdown. +/// Grace period given to the Loki background task to flush buffered events once +/// shutdown has been signalled. +const FLUSH_TIMEOUT: Duration = Duration::from_secs(3); + +/// The running Loki background task, returned by [`init`] when Loki is +/// configured. /// -/// For long-lived services, hold onto `controller` and call -/// `controller.shutdown().await` followed by awaiting the spawned `task` -/// before exit so buffered events are drained. Short-lived programs (e.g. -/// examples, one-shot CLI subcommands) may drop the controller; any logs -/// not yet posted to Loki at process exit will be lost. -#[must_use = "the background `task` must be spawned for events to reach Loki"] -pub struct LokiInit { - /// Handle used to tell the background task to drain its queue and exit. - pub controller: BackgroundTaskController, - /// Future that ships buffered events to Loki; must be spawned to run. - pub task: BackgroundTask, +/// Dropping this rather than calling [`LokiWorker::shutdown`] loses every event +/// not yet posted to Loki. +pub struct LokiWorker { + controller: BackgroundTaskController, + handle: tokio::task::JoinHandle<()>, +} + +impl LokiWorker { + /// Drains buffered events and stops the worker. + /// + /// Signalling and draining share a single grace period so an unreachable + /// Loki endpoint cannot wedge process exit; the worker is hard-aborted once + /// the budget elapses. + pub async fn shutdown(self) { + let abort_handle = self.handle.abort_handle(); + + let _ = tokio::time::timeout(FLUSH_TIMEOUT, async { + self.controller.shutdown().await; + let _ = self.handle.await; + }) + .await; + + abort_handle.abort(); + } } /// Initializes the tracing subscriber. -pub fn init(config: &TracingConfig) -> Result> { +/// +/// When `config` enables Loki, the background task that ships events is spawned +/// here and returned so the caller can drain it before exit. +/// +/// # Panics +/// +/// Panics when Loki is configured and this is called outside a Tokio runtime. +pub fn init(config: &TracingConfig) -> Result> { let env_filter = if let Some(override_env_filter) = config.override_env_filter.as_ref() { EnvFilter::from_str(override_env_filter).unwrap_or_else(|_| default_env_filter()) } else { @@ -53,6 +78,8 @@ pub fn init(config: &TracingConfig) -> Result> { let console_config = config.console.clone().unwrap_or_default(); let fmt_layer = tracing_subscriber::fmt::layer() + // Logs belong on stderr so a command's stdout stays pipeable. + .with_writer(std::io::stderr) .with_target(console_config.with_target) .with_level(console_config.with_level) .with_thread_ids(console_config.with_thread_ids) @@ -91,7 +118,10 @@ pub fn init(config: &TracingConfig) -> Result> { let registry = registry.with(loki_layer); registry.try_init()?; - Ok(Some(LokiInit { controller, task })) + Ok(Some(LokiWorker { + controller, + handle: tokio::spawn(task), + })) } else { registry.try_init()?; Ok(None) diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index 7a4d407d..c467d9d6 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -18,4 +18,4 @@ pub mod metrics; pub use config::{ConsoleConfig, LokiConfig, TracingConfig, TracingConfigBuilder}; -pub use init::{LokiInit, init}; +pub use init::{LokiWorker, init};