From cef837caf6aeb3763023351b0e3fda75f9a438e0 Mon Sep 17 00:00:00 2001 From: Evgenyi Safronov Date: Mon, 12 Jan 2026 13:53:59 +0300 Subject: [PATCH] feat: prometheus metrics export --- Cargo.lock | 393 ++++++++++++++++++++++++++++++++--------- dwd/Cargo.toml | 2 + dwd/src/api/metrics.rs | 381 +++++++++++++++++++++++++++++++++++++++ dwd/src/api/mod.rs | 9 + dwd/src/api/server.rs | 33 ++++ dwd/src/cfg.rs | 4 + dwd/src/cmd.rs | 6 + dwd/src/engine.rs | 37 ++++ dwd/src/histogram.rs | 12 ++ dwd/src/lib.rs | 5 +- dwd/src/stat/percpu.rs | 51 +++++- 11 files changed, 845 insertions(+), 88 deletions(-) create mode 100644 dwd/src/api/metrics.rs create mode 100644 dwd/src/api/mod.rs create mode 100644 dwd/src/api/server.rs diff --git a/Cargo.lock b/Cargo.lock index 50e8672..753ce42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "aho-corasick" version = "1.1.3" @@ -100,18 +85,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] -name = "backtrace" -version = "0.3.74" +name = "axum" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", + "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]] @@ -340,11 +362,18 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + [[package]] name = "dwd" version = "0.5.3" dependencies = [ "anyhow", + "axum", "bytes", "clap", "crossterm", @@ -365,6 +394,7 @@ dependencies = [ "pcap-parser", "pin-project-lite", "pnet", + "prometheus-client", "rand", "ratatui", "serde", @@ -429,6 +459,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +[[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.31" @@ -530,12 +569,6 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "glob" version = "0.3.1" @@ -599,6 +632,12 @@ version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.6.0" @@ -611,6 +650,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -618,6 +658,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -712,9 +768,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.167" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libloading" @@ -723,7 +779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -757,6 +813,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.4" @@ -764,19 +826,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] -name = "minimal-lexical" -version = "0.2.1" +name = "mime" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "miniz_oxide" -version = "0.8.0" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" -dependencies = [ - "adler2", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" @@ -897,15 +956,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "object" -version = "0.36.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -938,7 +988,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -958,6 +1008,12 @@ dependencies = [ "rusticata-macros", ] +[[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.15" @@ -1099,6 +1155,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus-client" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4500adecd7af8e0e9f4dbce15cfee07ce913fbf6ad605cc468b83f2d531ee94" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.37" @@ -1202,12 +1281,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc-hash" version = "2.1.0" @@ -1256,18 +1329,28 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1286,6 +1369,29 @@ dependencies = [ "serde", ] +[[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" @@ -1361,12 +1467,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1414,6 +1520,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 = "1.0.69" @@ -1465,37 +1577,65 @@ dependencies = [ [[package]] name = "tokio" -version = "1.42.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +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.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1652,13 +1792,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1667,7 +1813,25 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -1676,14 +1840,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1692,48 +1873,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/dwd/Cargo.toml b/dwd/Cargo.toml index a69cedd..622b21c 100644 --- a/dwd/Cargo.toml +++ b/dwd/Cargo.toml @@ -46,6 +46,8 @@ pnet = "0.35" libc = "0.2" jemallocator = "0.5" rand = "0.9" +axum = "0.8" +prometheus-client = "0.24" [target.'cfg(target_os = "linux")'.dependencies] netlink-packet-core = { version = "0.7" } diff --git a/dwd/src/api/metrics.rs b/dwd/src/api/metrics.rs new file mode 100644 index 0000000..d142e2e --- /dev/null +++ b/dwd/src/api/metrics.rs @@ -0,0 +1,381 @@ +//! Prometheus metrics collector and HTTP handler. + +use std::{fmt::Write, sync::Arc}; + +use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Router}; +use prometheus_client::{ + encoding::text::encode, + metrics::{counter::Counter, gauge::Gauge}, + registry::Registry, +}; + +use crate::{ + histogram::LogHistogram, + stat::{CommonStat, HttpStat, RxStat, SocketStat, TxStat}, +}; + +/// Prometheus histogram buckets for latency (in seconds). +/// Range: 5us to 10s with logarithmic distribution. +const LATENCY_BUCKETS: [f64; 20] = [ + 0.000_005, // 5us + 0.000_010, // 10us + 0.000_025, // 25us + 0.000_050, // 50us + 0.000_100, // 100us + 0.000_250, // 250us + 0.000_500, // 500us + 0.001, // 1ms + 0.002_5, // 2.5ms + 0.005, // 5ms + 0.010, // 10ms + 0.025, // 25ms + 0.050, // 50ms + 0.100, // 100ms + 0.250, // 250ms + 0.500, // 500ms + 1.0, // 1s + 2.5, // 2.5s + 5.0, // 5s + 10.0, // 10s +]; + +/// Collector that gathers metrics from stat sources and exports them to +/// Prometheus. +pub struct MetricsCollector { + registry: Registry, + generator_rps: Gauge, + requests_total: Counter, + responses_total: Counter, + timeouts_total: Counter, + bytes_tx_total: Counter, + bytes_rx_total: Counter, + http_2xx_total: Counter, + http_3xx_total: Counter, + http_4xx_total: Counter, + http_5xx_total: Counter, + sockets_created_total: Counter, + socket_errors_total: Counter, + retransmits_total: Counter, +} + +impl MetricsCollector { + /// Creates a new metrics collector and registers all metrics. + pub fn new() -> Self { + let mut registry = Registry::default(); + + let generator_rps = Gauge::default(); + registry.register( + "dwd_generator_rps", + "Target RPS from the generator", + generator_rps.clone(), + ); + + let requests_total = Counter::default(); + registry.register( + "dwd_requests_total", + "Total number of requests sent", + requests_total.clone(), + ); + + let responses_total = Counter::default(); + registry.register( + "dwd_responses_total", + "Total number of responses received", + responses_total.clone(), + ); + + let timeouts_total = Counter::default(); + registry.register( + "dwd_timeouts_total", + "Total number of request timeouts", + timeouts_total.clone(), + ); + + let bytes_tx_total = Counter::default(); + registry.register("dwd_bytes_tx_total", "Total bytes transmitted", bytes_tx_total.clone()); + + let bytes_rx_total = Counter::default(); + registry.register("dwd_bytes_rx_total", "Total bytes received", bytes_rx_total.clone()); + + let http_2xx_total = Counter::default(); + registry.register("dwd_http_2xx_total", "Total HTTP 2xx responses", http_2xx_total.clone()); + + let http_3xx_total = Counter::default(); + registry.register("dwd_http_3xx_total", "Total HTTP 3xx responses", http_3xx_total.clone()); + + let http_4xx_total = Counter::default(); + registry.register("dwd_http_4xx_total", "Total HTTP 4xx responses", http_4xx_total.clone()); + + let http_5xx_total = Counter::default(); + registry.register("dwd_http_5xx_total", "Total HTTP 5xx responses", http_5xx_total.clone()); + + let sockets_created_total = Counter::default(); + registry.register( + "dwd_sockets_created_total", + "Total number of sockets created", + sockets_created_total.clone(), + ); + + let socket_errors_total = Counter::default(); + registry.register( + "dwd_socket_errors_total", + "Total number of socket errors", + socket_errors_total.clone(), + ); + + let retransmits_total = Counter::default(); + registry.register( + "dwd_retransmits_total", + "Total number of TCP retransmits", + retransmits_total.clone(), + ); + + Self { + registry, + generator_rps, + requests_total, + responses_total, + timeouts_total, + bytes_tx_total, + bytes_rx_total, + http_2xx_total, + http_3xx_total, + http_4xx_total, + http_5xx_total, + sockets_created_total, + socket_errors_total, + retransmits_total, + } + } + + /// Updates common stats (generator RPS). + pub fn update_common(&self, stat: &S) { + self.generator_rps.set(stat.generator() as i64); + } + + /// Updates TX stats. + pub fn update_tx(&self, stat: &S) { + let requests = stat.num_requests(); + let bytes_tx = stat.bytes_tx(); + + // Calculate delta from current counter value. + let current_requests = self.requests_total.get(); + if requests > current_requests { + self.requests_total.inc_by(requests - current_requests); + } + + let current_bytes_tx = self.bytes_tx_total.get(); + if bytes_tx > current_bytes_tx { + self.bytes_tx_total.inc_by(bytes_tx - current_bytes_tx); + } + } + + /// Updates RX stats. + pub fn update_rx(&self, stat: &S) { + let responses = stat.num_responses(); + let timeouts = stat.num_timeouts(); + let bytes_rx = stat.bytes_rx(); + + let current_responses = self.responses_total.get(); + if responses > current_responses { + self.responses_total.inc_by(responses - current_responses); + } + + let current_timeouts = self.timeouts_total.get(); + if timeouts > current_timeouts { + self.timeouts_total.inc_by(timeouts - current_timeouts); + } + + let current_bytes_rx = self.bytes_rx_total.get(); + if bytes_rx > current_bytes_rx { + self.bytes_rx_total.inc_by(bytes_rx - current_bytes_rx); + } + } + + /// Updates HTTP stats. + pub fn update_http(&self, stat: &S) { + let num_2xx = stat.num_2xx(); + let num_3xx = stat.num_3xx(); + let num_4xx = stat.num_4xx(); + let num_5xx = stat.num_5xx(); + + let current_2xx = self.http_2xx_total.get(); + if num_2xx > current_2xx { + self.http_2xx_total.inc_by(num_2xx - current_2xx); + } + + let current_3xx = self.http_3xx_total.get(); + if num_3xx > current_3xx { + self.http_3xx_total.inc_by(num_3xx - current_3xx); + } + + let current_4xx = self.http_4xx_total.get(); + if num_4xx > current_4xx { + self.http_4xx_total.inc_by(num_4xx - current_4xx); + } + + let current_5xx = self.http_5xx_total.get(); + if num_5xx > current_5xx { + self.http_5xx_total.inc_by(num_5xx - current_5xx); + } + } + + /// Updates socket stats. + pub fn update_socket(&self, stat: &S) { + let created = stat.num_sock_created(); + let errors = stat.num_sock_errors(); + let retransmits = stat.num_retransmits(); + + let current_created = self.sockets_created_total.get(); + if created > current_created { + self.sockets_created_total.inc_by(created - current_created); + } + + let current_errors = self.socket_errors_total.get(); + if errors > current_errors { + self.socket_errors_total.inc_by(errors - current_errors); + } + + let current_retransmits = self.retransmits_total.get(); + if retransmits > current_retransmits { + self.retransmits_total.inc_by(retransmits - current_retransmits); + } + } + + /// Encodes all metrics to Prometheus text format. + pub fn encode(&self) -> String { + let mut buffer = String::new(); + encode(&mut buffer, &self.registry).expect("encoding should not fail"); + buffer + } +} + +impl Default for MetricsCollector { + fn default() -> Self { + Self::new() + } +} + +/// Encodes a LogHistogram to Prometheus histogram format. +/// +/// The histogram is encoded manually because prometheus-client's Histogram +/// uses observe() which accumulates values, but we need to export absolute +/// cumulative bucket counts from the log-histogram. +fn encode_histogram(hist: &LogHistogram) -> String { + let snapshot = hist.snapshot(); + let factor = LogHistogram::factor(); + + // Calculate cumulative counts for prometheus buckets. + // For each prometheus bucket with upper bound B (in seconds), + // we sum all log-bucket counts where the upper bound <= B. + let mut bucket_counts = vec![0u64; LATENCY_BUCKETS.len()]; + let mut total_count = 0u64; + let mut total_sum = 0.0f64; + + for (idx, &count) in snapshot.iter().enumerate() { + if count == 0 { + continue; + } + + total_count += count; + + // Upper bound of this log-bucket in microseconds. + let upper_us = factor.powi(idx as i32); + // Lower bound for sum calculation. + let lower_us = if idx == 0 { 0.0 } else { factor.powi(idx as i32 - 1) }; + // Midpoint in seconds for sum calculation. + let midpoint_sec = (lower_us + upper_us) / 2.0 / 1_000_000.0; + total_sum += midpoint_sec * count as f64; + + // Upper bound in seconds. + let upper_sec = upper_us / 1_000_000.0; + + // Add count to all prometheus buckets whose upper bound >= upper_sec. + for (bucket_idx, &bucket_bound) in LATENCY_BUCKETS.iter().enumerate() { + if bucket_bound >= upper_sec { + bucket_counts[bucket_idx] += count; + } + } + } + + let mut output = String::new(); + writeln!( + output, + "# HELP dwd_latency_seconds Response latency histogram in seconds" + ) + .unwrap(); + writeln!(output, "# TYPE dwd_latency_seconds histogram").unwrap(); + + // Buckets must be cumulative. + let mut cumulative = 0u64; + for (idx, &bound) in LATENCY_BUCKETS.iter().enumerate() { + cumulative += bucket_counts[idx]; + writeln!( + output, + "dwd_latency_seconds_bucket{{le=\"{:.6}\"}} {}", + bound, cumulative + ) + .unwrap(); + } + writeln!(output, "dwd_latency_seconds_bucket{{le=\"+Inf\"}} {}", total_count).unwrap(); + writeln!(output, "dwd_latency_seconds_sum {:.6}", total_sum).unwrap(); + writeln!(output, "dwd_latency_seconds_count {}", total_count).unwrap(); + + output +} + +/// Trait for stat sources that can be collected. +pub trait StatSource: Send + Sync { + /// Updates the metrics collector with current stats. + fn collect(&self, collector: &MetricsCollector); + + /// Returns the latency histogram if available. + fn histogram(&self) -> Option { + None + } +} + +/// Shared state for the metrics handler. +pub struct MetricsState { + collector: MetricsCollector, + stat_source: Arc, +} + +impl MetricsState { + /// Creates a new metrics state. + pub fn new(stat_source: Arc) -> Self { + Self { + collector: MetricsCollector::new(), + stat_source, + } + } +} + +/// Creates a router for metrics endpoints. +pub fn router(state: Arc) -> Router { + Router::new() + .route("/api/v1/metrics", get(metrics_handler)) + .with_state(state) +} + +async fn metrics_handler(State(state): State>) -> impl IntoResponse { + // Update metrics from stat source. + state.stat_source.collect(&state.collector); + + // Encode to prometheus format. + let mut body = state.collector.encode(); + + // Add histogram if available (encoded separately due to its special nature). + if let Some(hist) = state.stat_source.histogram() { + body.push_str(&encode_histogram(&hist)); + } + + ( + StatusCode::OK, + [( + axum::http::header::CONTENT_TYPE, + "text/plain; version=0.0.4; charset=utf-8", + )], + body, + ) +} diff --git a/dwd/src/api/mod.rs b/dwd/src/api/mod.rs new file mode 100644 index 0000000..f3e821a --- /dev/null +++ b/dwd/src/api/mod.rs @@ -0,0 +1,9 @@ +//! HTTP API for exposing generator state and metrics. + +mod metrics; +mod server; + +pub use self::{ + metrics::{MetricsCollector, MetricsState, StatSource}, + server::Server, +}; diff --git a/dwd/src/api/server.rs b/dwd/src/api/server.rs new file mode 100644 index 0000000..7106b45 --- /dev/null +++ b/dwd/src/api/server.rs @@ -0,0 +1,33 @@ +//! HTTP API server. + +use core::net::SocketAddr; +use std::sync::Arc; + +use axum::Router; +use tokio::net::TcpListener; + +use super::metrics::{self, MetricsState}; + +/// HTTP API server. +pub struct Server { + addr: SocketAddr, + metrics_state: Arc, +} + +impl Server { + /// Creates a new API server. + pub fn new(addr: SocketAddr, metrics_state: Arc) -> Self { + Self { addr, metrics_state } + } + + /// Runs the API server. + pub async fn run(self) -> Result<(), std::io::Error> { + let app = Router::new().merge(metrics::router(self.metrics_state)); + + let listener = TcpListener::bind(self.addr).await?; + let addr = listener.local_addr()?; + log::info!("API server listening on {addr}"); + + axum::serve(listener, app).await + } +} diff --git a/dwd/src/cfg.rs b/dwd/src/cfg.rs index f9d5226..c0fef48 100644 --- a/dwd/src/cfg.rs +++ b/dwd/src/cfg.rs @@ -34,6 +34,8 @@ use crate::{ pub struct Config { pub mode: ModeConfig, pub generator_fn: BoxedGeneratorNew, + /// Address to expose API on. + pub api_addr: Option, } impl TryFrom for Config { @@ -41,6 +43,7 @@ impl TryFrom for Config { fn try_from(v: Cmd) -> Result { let mode = v.mode.try_into()?; + let api_addr = v.api_addr; let generator_fn = { let path = v.generator.clone(); @@ -60,6 +63,7 @@ impl TryFrom for Config { let m = Self { mode, generator_fn: BoxedGeneratorNew(generator_fn), + api_addr, }; Ok(m) diff --git a/dwd/src/cmd.rs b/dwd/src/cmd.rs index 18ae332..79df58c 100644 --- a/dwd/src/cmd.rs +++ b/dwd/src/cmd.rs @@ -23,6 +23,12 @@ pub struct Cmd { /// Be verbose in terms of logging. #[clap(short, action = ArgAction::Count, global = true)] pub verbose: u8, + /// Address to expose API on. + /// + /// When specified, starts an HTTP server that exposes API that can be used + /// to observe the generator state and metrics. + #[clap(long, global = true, value_name = "IP:PORT")] + pub api_addr: Option, } #[derive(Debug, Clone, Parser)] diff --git a/dwd/src/engine.rs b/dwd/src/engine.rs index 54b6620..9ce5425 100644 --- a/dwd/src/engine.rs +++ b/dwd/src/engine.rs @@ -14,6 +14,7 @@ use tokio::sync::mpsc::{self, Receiver}; #[cfg(feature = "dpdk")] use crate::worker::dpdk::DpdkEngine; use crate::{ + api::{MetricsState, Server, StatSource}, cfg::{Config, ModeConfig}, engine::{ http::{Engine as HttpEngine, EngineRaw as HttpEngineRaw}, @@ -40,6 +41,7 @@ trait Engine: Send { fn generator(&self) -> SharedGenerator; fn limits(&self) -> Vec>; fn ui(&self) -> (Ui, Receiver); + fn stat_source(&self) -> Arc; fn run(self: Box, is_running: Arc) -> Result<(), Error>; } @@ -67,6 +69,10 @@ impl Engine for HttpEngine { (ui, rx) } + fn stat_source(&self) -> Arc { + self.stat() + } + fn run(self: Box, is_running: Arc) -> Result<(), Error> { Self::run(*self, future::pending(), is_running) } @@ -96,6 +102,10 @@ impl Engine for HttpEngineRaw { (ui, rx) } + fn stat_source(&self) -> Arc { + self.stat() + } + fn run(self: Box, is_running: Arc) -> Result<(), Error> { Self::run(*self, future::pending(), is_running) } @@ -122,6 +132,10 @@ impl Engine for UdpEngine { (ui, rx) } + fn stat_source(&self) -> Arc { + self.stat() + } + fn run(self: Box, is_running: Arc) -> Result<(), Error> { Self::run(*self, future::pending(), is_running) } @@ -149,6 +163,10 @@ impl Engine for DpdkEngine { (ui, rx) } + fn stat_source(&self) -> Arc { + self.stat() + } + fn run(self: Box, is_running: Arc) -> Result<(), Error> { Self::run(*self, is_running) } @@ -179,6 +197,20 @@ impl Runtime { let limits = engine.limits(); let generator = engine.generator(); let (ui, rx) = engine.ui(); + let stat_source = engine.stat_source(); + + // Start API server if configured. + let api_handle = if let Some(addr) = self.cfg.api_addr { + let metrics_state = Arc::new(MetricsState::new(stat_source)); + let server = Server::new(addr, metrics_state); + Some(tokio::spawn(async move { + if let Err(e) = server.run().await { + log::error!("API server error: {e}"); + } + })) + } else { + None + }; let engine = { let is_running = self.is_running.clone(); @@ -195,6 +227,11 @@ impl Runtime { ui.join().expect("no self join").unwrap(); engine.join().expect("no self join")?; + // Abort API server when shutting down. + if let Some(handle) = api_handle { + handle.abort(); + } + Ok(()) } diff --git a/dwd/src/histogram.rs b/dwd/src/histogram.rs index 1f8b3d7..235330b 100644 --- a/dwd/src/histogram.rs +++ b/dwd/src/histogram.rs @@ -50,6 +50,18 @@ impl LogHistogram { Self { snapshot } } + /// Returns the raw snapshot of bucket counts. + #[inline] + pub fn snapshot(&self) -> &[u64] { + &self.snapshot + } + + /// Returns the logarithmic factor used for bucket boundaries. + #[inline] + pub const fn factor() -> f64 { + FACTOR + } + /// Calculates the quantile. /// /// Suppose we have the following histogram, in linear coordinates: diff --git a/dwd/src/lib.rs b/dwd/src/lib.rs index 4a45b8b..d53317a 100644 --- a/dwd/src/lib.rs +++ b/dwd/src/lib.rs @@ -1,15 +1,16 @@ use core::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; +pub mod api; pub mod cfg; pub mod cmd; pub mod engine; mod generator; -mod histogram; +pub mod histogram; pub mod logging; mod shaper; mod sockstat; -mod stat; +pub mod stat; mod ui; mod worker; diff --git a/dwd/src/stat/percpu.rs b/dwd/src/stat/percpu.rs index 89b33d0..15ef791 100644 --- a/dwd/src/stat/percpu.rs +++ b/dwd/src/stat/percpu.rs @@ -6,6 +6,7 @@ use std::{sync::Arc, time::Instant}; use super::{CommonStat, HttpStat, RxStat, SocketStat, TxStat}; use crate::{ + api::{MetricsCollector, StatSource}, histogram::{LogHistogram, PerCpuLogHistogram}, stat::BurstTxStat, }; @@ -17,9 +18,7 @@ pub struct SharedGenerator { impl SharedGenerator { pub fn new() -> Self { - Self { - counter: Arc::new(AtomicU64::new(0)), - } + Self { counter: Arc::new(AtomicU64::new(0)) } } pub fn load(&self) -> u64 { @@ -46,7 +45,10 @@ where B: Default, { pub fn new(stats: Vec>>) -> Self { - Self { generator: SharedGenerator::new(), stats } + Self { + generator: SharedGenerator::new(), + stats, + } } pub fn generator(&self) -> SharedGenerator { @@ -161,6 +163,47 @@ impl BurstTxStat for Stat { } } +// StatSource implementations for specific Stat type combinations. + +/// HTTP engine stat: has TX, RX, Socket, and HTTP stats with histogram. +impl StatSource for Stat +where + B: Send + Sync, +{ + fn collect(&self, collector: &MetricsCollector) { + collector.update_common(self); + collector.update_tx(self); + collector.update_rx(self); + collector.update_http(self); + collector.update_socket(self); + } + + fn histogram(&self) -> Option { + Some(self.hist()) + } +} + +/// UDP engine stat: has TX and Socket stats, no histogram. +impl StatSource for Stat +where + H: Send + Sync, + B: Send + Sync, +{ + fn collect(&self, collector: &MetricsCollector) { + collector.update_common(self); + collector.update_tx(self); + collector.update_socket(self); + } +} + +/// DPDK engine stat: has TX stats only, no histogram. +impl StatSource for Stat { + fn collect(&self, collector: &MetricsCollector) { + collector.update_common(self); + collector.update_tx(self); + } +} + #[derive(Debug, Default)] pub struct PerCpuStat { tx: T,