diff --git a/Cargo.lock b/Cargo.lock index c8d8902..8e487d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,11 +26,61 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-channel" @@ -44,6 +94,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -164,9 +225,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ "hybrid-array", ] @@ -192,9 +253,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" @@ -236,9 +297,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -247,6 +308,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -333,9 +440,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "hybrid-array", ] @@ -358,13 +465,13 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6" +checksum = "285743a676ccb6b3e116bc14cc69319b957867930ae9c4822f8e0f54509d7243" dependencies = [ - "block-buffer 0.11.0", + "block-buffer 0.12.0", "const-oid", - "crypto-common 0.2.0", + "crypto-common 0.2.1", ] [[package]] @@ -999,6 +1106,12 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1016,9 +1129,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1050,9 +1163,9 @@ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1060,6 +1173,15 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -1175,6 +1297,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl" version = "0.10.75" @@ -1219,23 +1347,140 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +dependencies = [ + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand", + "thiserror", + "tokio", + "tokio-stream", +] + [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1295,6 +1540,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.44" @@ -1359,6 +1627,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1372,9 +1649,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -1418,9 +1695,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -1471,6 +1748,25 @@ dependencies = [ "url", ] +[[package]] +name = "samod-cli" +version = "0.1.0" +dependencies = [ + "axum", + "clap", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "rand", + "rayon", + "samod", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + [[package]] name = "samod-core" version = "0.8.0" @@ -1512,6 +1808,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "3.7.0" @@ -1646,7 +1948,7 @@ checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.11.0", + "digest 0.11.1", ] [[package]] @@ -1718,11 +2020,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1770,9 +2078,9 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", "getrandom 0.4.1", @@ -1844,6 +2152,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1984,6 +2293,38 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +[[package]] +name = "tonic" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -2074,6 +2415,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.22" @@ -2084,12 +2435,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -2181,6 +2535,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.21.0" @@ -2252,9 +2612,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2265,9 +2625,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -2279,9 +2639,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2289,9 +2649,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -2302,9 +2662,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -2345,9 +2705,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -2696,18 +3056,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a16a15a..ee0ca2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] edition = "2024" resolver = "3" -members = [ "samod","samod-core", "samod-test-harness"] +members = [ "samod", "samod-cli","samod-core", "samod-test-harness"] [workspace.dependencies] automerge = "0.7.1" +rand = "0.9.2" diff --git a/samod-cli/Cargo.toml b/samod-cli/Cargo.toml new file mode 100644 index 0000000..9bbdce2 --- /dev/null +++ b/samod-cli/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "samod-cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.60", features = ["derive"] } +tokio = { version = "1.49.0", features = ["full"] } +samod = { path = "../samod", version="0.8", features=["tokio", "axum", "tungstenite", "threadpool"] } +url = "2.5.8" +axum = { version = "0.8.8", features = ["ws"] } +tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } +tracing = "0.1.44" +uuid = { version = "1.21.0", features = ["v4"] } +rand = { workspace = true } +opentelemetry = { version = "0.31", features = ["metrics"] } +opentelemetry_sdk = { version = "0.31", features = ["metrics", "rt-tokio"] } +opentelemetry-otlp = { version = "0.31", features = ["metrics", "http-proto"] } +rayon = "1.11.0" diff --git a/samod-cli/src/listener_arg.rs b/samod-cli/src/listener_arg.rs new file mode 100644 index 0000000..e6d525f --- /dev/null +++ b/samod-cli/src/listener_arg.rs @@ -0,0 +1,40 @@ +use std::net::{IpAddr, SocketAddr}; + +#[derive(Debug, Clone)] +pub(crate) enum ListenerArg { + Tcp(SocketAddr), + WebSocket(SocketAddr), +} + +impl std::str::FromStr for ListenerArg { + type Err = String; + + fn from_str(s: &str) -> Result { + let url: url::Url = s.parse().map_err(|e: url::ParseError| e.to_string())?; + + let host = url.host().ok_or("URL must contain a host")?; + let ip: IpAddr = match host { + url::Host::Ipv4(addr) => addr.into(), + url::Host::Ipv6(addr) => addr.into(), + url::Host::Domain("localhost") => IpAddr::from([127, 0, 0, 1]), + url::Host::Domain(other) => { + return Err(format!( + "expected an IP address or 'localhost', not '{other}'" + )); + } + }; + + let port = url + .port_or_known_default() + .ok_or("URL must contain a port")?; + let addr = SocketAddr::new(ip, port); + + match url.scheme() { + "tcp" => Ok(ListenerArg::Tcp(addr)), + "ws" | "wss" => Ok(ListenerArg::WebSocket(addr)), + other => Err(format!( + "unsupported scheme: '{other}', expected 'tcp', 'ws', or 'wss'" + )), + } + } +} diff --git a/samod-cli/src/main.rs b/samod-cli/src/main.rs new file mode 100644 index 0000000..3d4572d --- /dev/null +++ b/samod-cli/src/main.rs @@ -0,0 +1,285 @@ +use std::{net::SocketAddr, path::PathBuf, sync::Arc}; + +use clap::Parser; + +mod listener_arg; +use listener_arg::ListenerArg; +mod peer_arg; +use peer_arg::PeerArg; +mod otel_observer; +use otel_observer::OtelObserver; +use samod::{ + AcceptorHandle, ConcurrencyConfig, DocumentId, PeerId, Repo, + storage::{InMemoryStorage, TokioFilesystemStorage}, + websocket::TungsteniteDialer, +}; +use tokio::net::TcpListener; + +#[derive(clap::ValueEnum, Clone, Default)] +enum LogFormat { + #[default] + Text, + Json, +} + +#[derive(clap::Parser)] +pub(crate) struct Args { + #[arg(long, default_value = "text", help = "Log output format")] + log_format: LogFormat, + #[command(subcommand)] + command: Command, +} + +#[derive(clap::Subcommand)] +pub(crate) enum Command { + Serve(ServeCommand), +} + +#[derive(clap::Parser)] +pub(crate) struct ServeCommand { + #[arg(short, long, help = "URLS to listen on")] + listeners: Vec, + #[arg(short, long, help = "Peer URLs to connect to")] + peers: Vec, + #[arg( + short, + long, + help = "Path to the directory where samod should store its data" + )] + storage_dir: Option, + #[arg( + long, + help = "Peer ID prefixes to announce documents to (e.g. storage-server for the public sync server" + )] + relay_peer_id_prefixes: Vec, + #[arg( + long, + help = "Peer ID prefix to use for this server (e.g. storage-server for the public sync server)" + )] + peer_id_prefix: Option, + #[arg( + long, + help = "OTLP HTTP endpoint to export metrics to (e.g. http://localhost:4318 for otel-tui, or http://localhost:9090/api/v1/otlp for Prometheus)" + )] + otel_endpoint: Option, +} + +#[tokio::main] +async fn main() { + let Args { + log_format, + command, + } = Args::parse(); + let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); + match log_format { + LogFormat::Text => tracing_subscriber::fmt().with_env_filter(env_filter).init(), + LogFormat::Json => tracing_subscriber::fmt() + .json() + .with_env_filter(env_filter) + .init(), + } + + match command { + Command::Serve(serve_command) => serve(serve_command).await, + } +} + +fn init_meter_provider(endpoint: &str) -> opentelemetry_sdk::metrics::SdkMeterProvider { + use opentelemetry_otlp::WithExportConfig; + use opentelemetry_sdk::Resource; + use opentelemetry_sdk::metrics::SdkMeterProvider; + + let endpoint = endpoint.strip_suffix('/').unwrap_or(endpoint); + let exporter = opentelemetry_otlp::MetricExporter::builder() + .with_http() + .with_endpoint(format!("{endpoint}/v1/metrics")) + .build() + .expect("failed to build OTLP metric exporter"); + + let provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter) + .with_resource(Resource::builder().with_service_name("samod-cli").build()) + .build(); + + opentelemetry::global::set_meter_provider(provider.clone()); + provider +} + +async fn serve( + ServeCommand { + listeners, + peers, + storage_dir, + relay_peer_id_prefixes, + peer_id_prefix, + otel_endpoint, + }: ServeCommand, +) { + // Set up OpenTelemetry metrics if an endpoint is configured + let _meter_provider = otel_endpoint.as_deref().map(|endpoint| { + let provider = init_meter_provider(endpoint); + tracing::info!("OpenTelemetry metrics exporting to {}", endpoint); + provider + }); + + let observer = _meter_provider.as_ref().map(|_| { + let meter = opentelemetry::global::meter("samod-cli"); + OtelObserver::new(&meter) + }); + + let announce_policy = move |_doc_id: DocumentId, peer_id: PeerId| { + relay_peer_id_prefixes + .iter() + .any(|prefix| peer_id.to_string().starts_with(prefix)) + }; + let peer_id = if let Some(prefix) = peer_id_prefix { + PeerId::from_string(format!("{}-{}", prefix, uuid::Uuid::new_v4())) + } else { + PeerId::new_with_rng(&mut rand::rng()) + }; + let threadpool = rayon::ThreadPoolBuilder::new() + .build() + .expect("failed to build threadpool"); + let repo = match storage_dir { + Some(dir) => { + tracing::info!("using file system storage at {}", dir.display()); + let storage = TokioFilesystemStorage::new(dir); + let mut builder = samod::Repo::build_tokio() + .with_storage(storage) + .with_announce_policy(announce_policy) + .with_peer_id(peer_id) + .with_concurrency(ConcurrencyConfig::Threadpool(threadpool)); + if let Some(obs) = observer { + builder = builder.with_observer(obs); + } + builder.load().await + } + None => { + tracing::info!("using ephemeral in-memory storage"); + let storage = InMemoryStorage::new(); + let mut builder = samod::Repo::build_tokio() + .with_storage(storage) + .with_announce_policy(announce_policy) + .with_peer_id(peer_id) + .with_concurrency(ConcurrencyConfig::Threadpool(threadpool)); + if let Some(obs) = observer { + builder = builder.with_observer(obs); + } + builder.load().await + } + }; + + for listener in listeners { + match listener { + ListenerArg::WebSocket(addr) => { + tracing::info!("starting websocket listener on {}", addr); + listen_websocket(repo.clone(), addr).await; + } + ListenerArg::Tcp(addr) => { + tracing::info!("starting tcp listener on {addr}"); + listen_tcp(repo.clone(), addr).await; + } + } + } + + for peer in peers { + match peer { + PeerArg::Tcp { host, port } => { + tracing::info!("creating outbound connection to {}:{}", host, port); + repo.dial( + samod::BackoffConfig::default(), + Arc::new(samod::tokio_io::TcpDialer::new_host_port( + host.clone(), + port, + )), + ) + .inspect_err(|e| { + tracing::warn!("error dialing tcp peer {}:{}: {e}", host, port); + }) + .ok(); + } + PeerArg::WebSocket(url) => { + tracing::info!("creating outbound connection to {}", url); + repo.dial( + samod::BackoffConfig::default(), + Arc::new(TungsteniteDialer::new(url.clone())), + ) + .inspect_err(|e| { + tracing::warn!("error dialing websocket peer {}: {e}", url); + }) + .ok(); + } + } + } + + // Now wait for termination + tokio::signal::ctrl_c() + .await + .expect("failed to listen for ctrl-c signal"); + + // Flush metrics on shutdown + if let Some(provider) = _meter_provider + && let Err(e) = provider.shutdown() + { + tracing::warn!("error shutting down meter provider: {e}"); + } +} + +async fn listen_tcp(repo: Repo, addr: SocketAddr) { + let url = url::Url::parse(&format!("tcp://{}:{}", addr.ip(), addr.port())).unwrap(); + let Ok(listener) = TcpListener::bind(addr).await.inspect_err(|e| { + tracing::error!("unable to listen on {url}: {e}"); + }) else { + return; + }; + let Ok(acceptor) = repo.make_acceptor(url.clone()).inspect_err(|e| { + tracing::warn!("error creating acceptor for {url}: {e}"); + }) else { + return; + }; + tokio::spawn(async move { + loop { + let Ok((io, addr)) = listener.accept().await.inspect_err(|e| { + tracing::warn!("error accepting tcp connection on {url}: {e}"); + }) else { + continue; + }; + tracing::info!("accepted tcp connection from {}", addr); + if let Err(e) = acceptor.accept_tokio_io(io) { + tracing::error!(?e, "failed to accept tcp connection from {}", addr); + } + } + }); +} + +async fn listen_websocket(repo: Repo, addr: SocketAddr) { + let listener = TcpListener::bind(addr) + .await + .expect("unable to bind socket"); + let Ok(acceptor) = repo + .make_acceptor(url::Url::parse(&format!("ws://{}", addr)).unwrap()) + .inspect_err(|e| { + tracing::warn!("error creating acceptor for {}: {e}", addr); + }) + else { + return; + }; + + let app = axum::Router::new() + .route("/", axum::routing::get(websocket_handler)) + .with_state(acceptor.clone()); + let server = axum::serve(listener, app).into_future(); + tokio::spawn(server); +} + +async fn websocket_handler( + ws: axum::extract::ws::WebSocketUpgrade, + axum::extract::State(acceptor): axum::extract::State, +) -> axum::response::Response { + ws.on_upgrade(|socket| async move { + if let Err(e) = acceptor.accept_axum(socket) { + tracing::error!(?e, "failed to accept axum websocket"); + } + }) +} diff --git a/samod-cli/src/otel_observer.rs b/samod-cli/src/otel_observer.rs new file mode 100644 index 0000000..c2b304f --- /dev/null +++ b/samod-cli/src/otel_observer.rs @@ -0,0 +1,160 @@ +use std::sync::atomic::{AtomicI64, Ordering}; + +use opentelemetry::KeyValue; +use opentelemetry::metrics::{Counter, Gauge, Histogram, Meter}; +use samod::{RepoEvent, RepoObserver, StorageOperation}; + +pub struct OtelObserver { + documents_active: Gauge, + documents_active_count: AtomicI64, + peers_connected: Gauge, + peers_connected_count: AtomicI64, + connections_total: Counter, + sync_processing_millis: Histogram, + sync_queue_millis: Histogram, + sync_message_bytes: Histogram, + storage_operation_millis: Histogram, + hub_event_processing_millis: Histogram, + hub_connections: Gauge, + hub_documents: Gauge, + document_pending_sync_messages: Gauge, +} + +impl OtelObserver { + pub fn new(meter: &Meter) -> Self { + Self { + documents_active: meter + .f64_gauge("samod_documents_active") + .with_description("Number of active document actors") + .build(), + peers_connected: meter + .f64_gauge("samod_peers_connected") + .with_description("Number of connected peers") + .build(), + connections_total: meter + .u64_counter("samod_connections_total") + .with_description("Total connection lifecycle events") + .build(), + sync_processing_millis: meter + .f64_histogram("samod_sync_processing_millis") + .with_description("Time spent processing sync messages") + .with_unit("ms") + .build(), + sync_queue_millis: meter + .f64_histogram("samod_sync_queue_millis") + .with_description("Time sync messages spent waiting in queue") + .with_unit("ms") + .build(), + sync_message_bytes: meter + .f64_histogram("samod_sync_message_bytes") + .with_description("Size of sync messages") + .with_unit("By") + .build(), + storage_operation_millis: meter + .f64_histogram("samod_storage_operation_millis") + .with_description("Time spent on storage operations") + .with_unit("ms") + .build(), + hub_event_processing_millis: meter + .f64_histogram("samod_hub_event_processing_millis") + .with_description("Time spent processing hub events") + .with_unit("ms") + .build(), + hub_connections: meter + .f64_gauge("samod_hub_connections") + .with_description("Number of active hub connections") + .build(), + hub_documents: meter + .f64_gauge("samod_hub_documents") + .with_description("Number of active hub documents") + .build(), + document_pending_sync_messages: meter + .f64_gauge("samod_document_pending_sync_messages") + .with_description("Pending sync messages during Loading phase") + .build(), + documents_active_count: AtomicI64::new(0), + peers_connected_count: AtomicI64::new(0), + } + } +} + +impl RepoObserver for OtelObserver { + fn observe(&self, event: &RepoEvent) { + match event { + RepoEvent::DocumentOpened { .. } => { + let count = self.documents_active_count.fetch_add(1, Ordering::Relaxed) + 1; + self.documents_active.record(count as f64, &[]); + } + RepoEvent::DocumentClosed { .. } => { + let count = self.documents_active_count.fetch_sub(1, Ordering::Relaxed) - 1; + self.documents_active.record(count as f64, &[]); + } + RepoEvent::ConnectionEstablished { connection_id: _ } => { + self.connections_total + .add(1, &[KeyValue::new("event", "connected")]); + let count = self.peers_connected_count.fetch_add(1, Ordering::Relaxed) + 1; + self.peers_connected.record(count as f64, &[]); + } + RepoEvent::ConnectionLost { connection_id: _ } => { + self.connections_total + .add(1, &[KeyValue::new("event", "disconnected")]); + let count = self.peers_connected_count.fetch_sub(1, Ordering::Relaxed) - 1; + self.peers_connected.record(count as f64, &[]); + } + RepoEvent::SyncMessageReceived { + bytes, + duration, + queue_duration, + .. + } => { + let attrs = [KeyValue::new("direction", "received")]; + self.sync_processing_millis + .record(duration.as_secs_f64() * 1000.0, &attrs); + self.sync_queue_millis + .record(queue_duration.as_secs_f64() * 1000.0, &attrs); + self.sync_message_bytes.record(*bytes as f64, &attrs); + } + RepoEvent::SyncMessageGenerated { + bytes, duration, .. + } => { + let attrs = [KeyValue::new("direction", "generated")]; + self.sync_processing_millis + .record(duration.as_millis() as f64, &attrs); + self.sync_message_bytes.record(*bytes as f64, &attrs); + } + RepoEvent::StorageOperationCompleted { + operation, + duration, + .. + } => { + let op = match operation { + StorageOperation::Load => "load", + StorageOperation::LoadRange => "load_range", + StorageOperation::Put => "put", + StorageOperation::Delete => "delete", + }; + self.storage_operation_millis.record( + duration.as_millis() as f64, + &[KeyValue::new("operation", op)], + ); + } + RepoEvent::HubEventProcessed { + duration, + event_type, + connections, + documents, + } => { + let attrs = [KeyValue::new("event_type", *event_type)]; + self.hub_event_processing_millis + .record(duration.as_millis() as f64, &attrs); + self.hub_connections.record(*connections as f64, &[]); + self.hub_documents.record(*documents as f64, &[]); + } + RepoEvent::DocumentPendingSyncMessages { count, .. } => { + self.document_pending_sync_messages + .record(*count as f64, &[]); + } + _ => {} + } + } +} diff --git a/samod-cli/src/peer_arg.rs b/samod-cli/src/peer_arg.rs new file mode 100644 index 0000000..5eaebc0 --- /dev/null +++ b/samod-cli/src/peer_arg.rs @@ -0,0 +1,25 @@ +#[derive(Debug, Clone)] +pub(crate) enum PeerArg { + Tcp { host: String, port: u16 }, + WebSocket(url::Url), +} + +impl std::str::FromStr for PeerArg { + type Err = String; + + fn from_str(s: &str) -> Result { + let url: url::Url = s.parse().map_err(|e: url::ParseError| e.to_string())?; + + match url.scheme() { + "tcp" => { + let host = url.host_str().ok_or("URL must contain a host")?.to_string(); + let port = url.port().ok_or("URL must contain a port")?; + Ok(PeerArg::Tcp { host, port }) + } + "ws" | "wss" => Ok(PeerArg::WebSocket(url)), + other => Err(format!( + "unsupported scheme: '{other}', expected 'tcp', 'ws', or 'wss'" + )), + } + } +} diff --git a/samod-core/Cargo.toml b/samod-core/Cargo.toml index 403c275..cf50e74 100644 --- a/samod-core/Cargo.toml +++ b/samod-core/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" base64 = "0.21" tracing = "0.1.41" uuid = { version = "1.0", features = ["v4", "serde"] } -rand = "0.9" +rand = { workspace = true } automerge = { workspace = true } serde = { version = "1.0", features = ["derive"] } hex = "0.4"