diff --git a/Cargo.lock b/Cargo.lock index c8712f1..03a5fb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,76 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "bytes" version = "1.11.1" @@ -42,6 +106,21 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.32" @@ -130,12 +209,126 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.14.0" @@ -143,7 +336,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -152,18 +347,33 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "laminar" version = "0.1.0" dependencies = [ "anyhow", + "axum", "futures", "serde", + "serde_json", "serde_yaml", "thiserror", "tokio", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -172,6 +382,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -193,12 +409,24 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.2.0" @@ -248,12 +476,28 @@ dependencies = [ "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-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -272,6 +516,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -281,6 +531,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.23" @@ -293,6 +549,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -323,6 +585,42 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -388,6 +686,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "thiserror" version = "2.0.18" @@ -445,12 +749,41 @@ dependencies = [ "syn", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -488,6 +821,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.23" @@ -495,11 +838,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "nu-ansi-term", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -508,12 +854,29 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -526,6 +889,103 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -540,3 +1000,103 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index a377715..c0a9206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,13 +6,16 @@ license = "MIT" [dependencies] anyhow = "1.0.102" +axum = "0.8.9" futures = "0.3.32" serde = {version ="1.0.228",features = ["derive"]} +serde_json = "1.0.150" serde_yaml = {version = "0.9.34"} thiserror = "2.0.18" tokio = {version = "1.52.3", features = ["full","net"]} tracing = "0.1.44" -tracing-subscriber = "0.3.23" +tracing-subscriber = {version="0.3.23", features = ["json"]} +uuid = {version="1.23.2",features=["v4"]} diff --git a/ROADMAP.md b/ROADMAP.md index 32ee9f1..8a4442e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -89,7 +89,6 @@ - [x] Track active connections per backend - [x] Increment on connect - [x] Decrement on disconnect -- [ ] Track total requests --- @@ -149,11 +148,12 @@ ## Metrics -- [ ] Active connection metrics +- [x] Active connection metrics - [ ] Request counters -- [ ] Failure counters -- [ ] Backend health metrics +- [x] Failure counters +- [x] Backend health metrics - [ ] Throughput metrics +- [x] Track total requests --- @@ -167,8 +167,8 @@ ## Logging Improvements -- [ ] Structured JSON logs -- [ ] Request correlation IDs +- [x] Structured JSON logs +- [x] Request correlation IDs - [x] Retry logging - [x] Timeout logging - [x] Backend transition logging @@ -198,7 +198,7 @@ - [ ] Add runtime status endpoint - [ ] Add backend health endpoint - [ ] Add backend enable/disable API -- [ ] Add metrics endpoint +- [x] Add metrics endpoint --- diff --git a/deny.toml b/deny.toml index d04e8f6..d7091a9 100644 --- a/deny.toml +++ b/deny.toml @@ -94,6 +94,7 @@ allow = [ "MIT", "Apache-2.0", "Unicode-3.0", + "BSD-3-Clause" ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the diff --git a/src/admin/http.rs b/src/admin/http.rs new file mode 100644 index 0000000..f082df7 --- /dev/null +++ b/src/admin/http.rs @@ -0,0 +1,65 @@ +use std::sync::atomic::Ordering; + +use axum::{Json, Router, extract::State, routing::get}; + +use laminar::state::app::SharedAppState; +use serde::Serialize; +use tokio::net::TcpListener; + +#[derive(Serialize)] +struct BackendMetrics { + id: String, + healthy: bool, + active_connections: usize, + total_requests: usize, + failed_requests: usize, +} + +#[derive(Serialize)] +struct UpstreamMetrics { + id: String, + algorithm: String, + backends: Vec, +} + +#[derive(Serialize)] +struct MetricsResponse { + upstreams: Vec, +} + +async fn metrics_handler(State(state): State) -> Json { + let state = state.read().await; + + let upstreams = state + .upstreams + .iter() + .map(|upstream| { + let backends = upstream + .backends + .iter() + .map(|backend| BackendMetrics { + id: backend.config.id.clone(), + healthy: backend.healthy.load(Ordering::Relaxed), + active_connections: backend.active_connections.load(Ordering::Relaxed), + total_requests: backend.total_requests.load(Ordering::Relaxed), + failed_requests: backend.failed_requests.load(Ordering::Relaxed), + }) + .collect(); + + UpstreamMetrics { + id: upstream.id.clone(), + algorithm: format!("{:?}", upstream.algorithm), + backends, + } + }) + .collect(); + + Json(MetricsResponse { upstreams }) +} + +pub async fn start_admin_server(address: &str, state: SharedAppState) -> anyhow::Result<()> { + let app = Router::new().route("/metrics", get(metrics_handler)).with_state(state); + let listener = TcpListener::bind(address).await?; + axum::serve(listener, app).await?; + Ok(()) +} diff --git a/src/admin/mod.rs b/src/admin/mod.rs new file mode 100644 index 0000000..3883215 --- /dev/null +++ b/src/admin/mod.rs @@ -0,0 +1 @@ +pub mod http; diff --git a/src/common/mod.rs b/src/common/mod.rs index b91f65f..de8498c 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,47 +1 @@ -// // server state will have .. ip and port ( we can get that form the siurces.yml , check if it is active (up) and the number of connections ) - -// pub mod types; -// use std::{ fs::File, net::SocketAddr }; -// use std::result::Result; -// use anyhow::{ self, Ok }; -// use futures::future::join_all; -// use tokio::net::TcpStream; - -// pub async fn check_servers_health() -> Result, anyhow::Error> { -// let servers = parse_yaml()?; - -// // try to connect each one and filter out the dead ones -// let futures = servers.into_iter().map(|mut s| async { -// let addr: SocketAddr = s.ip.parse().expect("the address is not valid"); -// // let res = TcpStream::connect(addr).await; - -// // match res { -// // Ok(_) => { -// // s.can_connect = true; -// // } -// // Err(_) => { -// // s.can_connect = false; -// // } -// // } - -// // shortcut -// s.can_connect = TcpStream::connect(addr).await.is_ok(); - -// s -// }); - -// // run the futures as the async blocks are lazy -// let res = join_all(futures).await; -// Ok(res) -// } - -// pub async fn active_servers() -> Result, anyhow::Error> { -// let servers = check_servers_health().await?; - -// Ok( -// servers -// .into_iter() -// .filter(|item| item.can_connect) -// .collect() -// ) -// } +pub mod shutdown; diff --git a/src/common/shutdown.rs b/src/common/shutdown.rs new file mode 100644 index 0000000..33ece88 --- /dev/null +++ b/src/common/shutdown.rs @@ -0,0 +1,6 @@ +use tracing::info; + +pub async fn shutdown_signal() { + tokio::signal::ctrl_c().await.expect("failed to listen for shutdown signal"); + info!("shutdown signal received"); +} diff --git a/src/config/types.rs b/src/config/types.rs index 795ef80..f7cde8d 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -54,7 +54,7 @@ pub struct UpstreamConfig { pub servers: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub enum LoadBalancingAlgorithm { RoundRobin, diff --git a/src/main.rs b/src/main.rs index 21d2a2f..de47d9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![warn(clippy::all)] #![warn(clippy::pedantic)] - +#![allow(dead_code)] +mod admin; use anyhow::{Result, bail}; use laminar::{ config::{loader::load_config, validator::validate_config}, @@ -11,10 +12,10 @@ use laminar::{ use std::sync::Arc; use tokio::sync::RwLock; use tracing::info; - +mod common; #[tokio::main] async fn main() -> Result<()> { - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt().json().with_current_span(true).with_span_list(true).init(); let path = std::env::args().nth(1).unwrap_or_else(|| "laminar_config.yaml".to_string()); @@ -36,11 +37,15 @@ async fn main() -> Result<()> { } let shared_state: SharedAppState = Arc::new(RwLock::new(state)); - // for upstream in &state.upstreams { - // info!("upstream '{}' initialized with {} backends", upstream.id, upstream.backends.len()); - // } let health_state = shared_state.clone(); + let admin_state = shared_state.clone(); + + tokio::spawn(async move { + if let Err(error) = admin::http::start_admin_server("127.0.0.1:9090", admin_state).await { + tracing::error!("admin server failed: {:?}", error); + } + }); tokio::spawn(async move { start_health_checker(health_state, health_interval).await; }); diff --git a/src/proxy/tcp.rs b/src/proxy/tcp.rs index 2f810c1..eca16f4 100644 --- a/src/proxy/tcp.rs +++ b/src/proxy/tcp.rs @@ -1,3 +1,4 @@ +use crate::common::shutdown::shutdown_signal; use crate::state::app::SharedAppState; use crate::state::backend::ConnectionGuard; use std::{collections::HashSet, time::Duration}; @@ -7,6 +8,7 @@ use tokio::{ time::timeout, }; use tracing::{error, info}; +use uuid::Uuid; pub async fn start_tcp_proxy(address: &str, state: SharedAppState) -> anyhow::Result<()> { let listener = TcpListener::bind(address).await?; @@ -14,19 +16,35 @@ pub async fn start_tcp_proxy(address: &str, state: SharedAppState) -> anyhow::Re info!("tcp proxy listening on {}", address); loop { - let (client_stream, client_address) = listener.accept().await?; + tokio::select! { + result = listener.accept() => { + let (client_stream, client_address) = result?; + info!( + client = %client_address, + "new client connected" + ); + let state = state.clone(); + tokio::spawn(async move { + if let Err(error) = handle_connection(client_stream,state).await + { + error!("connection handling failed {:?}",error); + } + }); + } - info!("new client connected {}", client_address); - let state = state.clone(); - tokio::spawn(async move { - if let Err(error) = handle_connection(client_stream, state).await { - error!("connection handling failed {:?}", error) + _ = shutdown_signal() => { + info!("tcp proxy shutting down"); + break; } - }); + } } + info!("tcp proxy stopped accepting new connections"); + + Ok(()) } pub async fn handle_connection(mut stream: TcpStream, state: SharedAppState) -> anyhow::Result<()> { + let request_id = Uuid::new_v4(); let (retry_attempt, connect_timeout, idle_timeout) = { let state = state.read().await; (state.retry_attempts, state.connect_timeout, state.idle_timeout) @@ -49,7 +67,10 @@ pub async fn handle_connection(mut stream: TcpStream, state: SharedAppState) -> let backend_arc = match upstream.next_backend() { Some(backend) => backend, None => { - error!("no healthy backend available"); + error!( + request_id = %request_id, + "no healthy backend available" + ); return Ok(()); } }; @@ -61,15 +82,34 @@ pub async fn handle_connection(mut stream: TcpStream, state: SharedAppState) -> continue; } - info!("forwarding traffic to {}", backend_address); + info!( + request_id = %request_id, + backend_id = %guard.backend_id(), + backend = %backend_address, + "forwarding traffic" + ); match proxy_connection(&mut stream, &backend_address, connect_timeout, idle_timeout).await { Ok(_) => { + info!( + request_id = %request_id, + backend_id = %guard.backend_id(), + "request completed" + ); + guard.backend().increment_total_requests(); return Ok(()); } Err(error) => { + guard.backend().increment_failed_requests(); guard.mark_backend_unhealthy(); - error!(backend_id = %guard.backend_id(),backend = %backend_address,attempt = attempted_backends.len() + 1,"backend request failed: {:?}",error); + error!( + request_id = %request_id, + backend_id = %guard.backend_id(), + backend = %backend_address, + attempt = attempted_backends.len() + 1, + error = %error, + "backend request failed" + ); attempted_backends.insert(guard.backend_id().to_string()); continue; } diff --git a/src/state/backend.rs b/src/state/backend.rs index cd2c0d0..a06c846 100644 --- a/src/state/backend.rs +++ b/src/state/backend.rs @@ -27,6 +27,9 @@ pub struct BackendState { // This becomes important for least-connections balancing. pub active_connections: AtomicUsize, pub failed_health_checks: usize, + + pub total_requests: AtomicUsize, + pub failed_requests: AtomicUsize, } impl ConnectionGuard { @@ -43,6 +46,10 @@ impl ConnectionGuard { pub fn address(&self) -> String { format!("{}:{}", self.backend.config.host, self.backend.config.port) } + + pub fn backend(&self) -> &BackendState { + &self.backend + } pub fn mark_backend_unhealthy(&self) { self.backend.healthy.store(false, Ordering::Relaxed); } @@ -63,9 +70,19 @@ impl BackendState { healthy: AtomicBool::new(true), active_connections: AtomicUsize::new(0), failed_health_checks: 0, + total_requests: AtomicUsize::new(0), + failed_requests: AtomicUsize::new(0), } } + pub fn increment_total_requests(&self) { + self.total_requests.fetch_add(1, Ordering::Relaxed); + } + + pub fn increment_failed_requests(&self) { + self.failed_requests.fetch_add(1, Ordering::Relaxed); + } + pub fn is_healthy(&self) -> bool { self.healthy.load(Ordering::Relaxed) } diff --git a/tests/connect_timeout.rs b/tests/connect_timeout.rs index 40e453d..852c55f 100644 --- a/tests/connect_timeout.rs +++ b/tests/connect_timeout.rs @@ -24,6 +24,8 @@ async fn marks_backend_unhealthy_on_connect_timeout() { healthy: AtomicBool::new(true), active_connections: AtomicUsize::new(0), failed_health_checks: 0, + failed_requests: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), }); let guard = ConnectionGuard::new(backend.clone()); diff --git a/tests/connection_guard.rs b/tests/connection_guard.rs index 1b6d9aa..1d71504 100644 --- a/tests/connection_guard.rs +++ b/tests/connection_guard.rs @@ -20,6 +20,8 @@ fn connection_guard_tracks_active_connections() { healthy: AtomicBool::new(true), active_connections: AtomicUsize::new(0), failed_health_checks: 0, + failed_requests: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), }); assert_eq!(backend.active_connections.load(Ordering::Relaxed), 0); diff --git a/tests/health_checker.rs b/tests/health_checker.rs index 8b27675..ff7fbef 100644 --- a/tests/health_checker.rs +++ b/tests/health_checker.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use tokio::net::TcpListener; @@ -17,10 +17,10 @@ fn create_backend(port: u16) -> BackendState { }, healthy: AtomicBool::new(false), - - active_connections: 0.into(), - + active_connections: (0).into(), failed_health_checks: 0, + failed_requests: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), } } diff --git a/tests/health_selection.rs b/tests/health_selection.rs index 74f16aa..671a6e4 100644 --- a/tests/health_selection.rs +++ b/tests/health_selection.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, AtomicUsize}; use laminar::{ config::types::BackendServerConfig, @@ -15,10 +15,10 @@ fn create_backend(id: &str, port: u16, healthy: bool) -> BackendState { }, healthy: AtomicBool::new(healthy), - active_connections: (0).into(), - failed_health_checks: 0, + failed_requests: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), } } diff --git a/tests/least_connections.rs b/tests/least_connections.rs index eef6df0..7286e64 100644 --- a/tests/least_connections.rs +++ b/tests/least_connections.rs @@ -24,6 +24,8 @@ fn create_backend( healthy: AtomicBool::new(healthy), active_connections: AtomicUsize::new(active_connections), failed_health_checks: 0, + failed_requests: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), }) } diff --git a/tests/request_metrics.rs b/tests/request_metrics.rs new file mode 100644 index 0000000..e01b73a --- /dev/null +++ b/tests/request_metrics.rs @@ -0,0 +1,29 @@ +use laminar::{config::types::BackendServerConfig, state::backend::BackendState}; +use std::sync::{ + Arc, + atomic::{AtomicBool, AtomicUsize, Ordering}, +}; + +#[test] +fn request_metrics_increment_correctly() { + let backend = Arc::new(BackendState { + config: BackendServerConfig { + id: "server-1".into(), + host: "127.0.0.1".into(), + port: 9001, + weight: 1, + }, + healthy: AtomicBool::new(true), + active_connections: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), + failed_requests: AtomicUsize::new(0), + failed_health_checks: 0, + }); + + backend.increment_total_requests(); + backend.increment_total_requests(); + backend.increment_failed_requests(); + + assert_eq!(backend.total_requests.load(Ordering::Relaxed), 2); + assert_eq!(backend.failed_requests.load(Ordering::Relaxed), 1); +} diff --git a/tests/retry_stabilization.rs b/tests/retry_stabilization.rs index 0aeb42b..ba5e253 100644 --- a/tests/retry_stabilization.rs +++ b/tests/retry_stabilization.rs @@ -20,6 +20,8 @@ fn unhealthy_backend_is_not_selected_again() { healthy: AtomicBool::new(false), active_connections: AtomicUsize::new(0), failed_health_checks: 0, + failed_requests: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), }); let backend_2 = Arc::new(BackendState { @@ -33,6 +35,8 @@ fn unhealthy_backend_is_not_selected_again() { healthy: AtomicBool::new(true), active_connections: AtomicUsize::new(0), failed_health_checks: 0, + failed_requests: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), }); let backends = vec![backend_1, backend_2.clone()]; diff --git a/tests/round_robin.rs b/tests/round_robin.rs index 75af3cf..9340cfb 100644 --- a/tests/round_robin.rs +++ b/tests/round_robin.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, AtomicUsize}; use laminar::{ config::types::BackendServerConfig, @@ -15,10 +15,10 @@ fn create_backend(id: &str, port: u16) -> BackendState { }, healthy: AtomicBool::new(true), - active_connections: (0).into(), - failed_health_checks: 0, + failed_requests: AtomicUsize::new(0), + total_requests: AtomicUsize::new(0), } }