diff --git a/Cargo.lock b/Cargo.lock index 47aab53..b61d639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,30 +2,190 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "arc-cell" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec9da9adf9420d86def101bd5b4a227b0512d456b6a128b0d677fdf68e5f7b8" + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cc" +version = "1.2.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "darling" version = "0.21.3" @@ -61,12 +221,54 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -82,6 +284,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -100,6 +308,69 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[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.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "http" version = "1.3.1" @@ -156,6 +427,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -168,6 +440,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.18" @@ -195,12 +484,57 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -213,6 +547,16 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -222,12 +566,75 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "metrics" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b166dea96003ee2531cf14833efedced545751d800f03535801d833313f8c15" +dependencies = [ + "base64", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "indexmap", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe8db7a05415d0f919ffb905afa37784f71901c9a773188876984b4f769ab986" +dependencies = [ + "aho-corasick", + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.5", + "indexmap", + "metrics", + "ordered-float", + "quanta", + "radix_trie", + "rand", + "rand_xoshiro", + "sketches-ddsketch", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.1.0" @@ -239,6 +646,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -248,6 +674,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -273,6 +708,118 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "orx-concurrent-iter" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78e4381d6f1393a99e5a2bebe63e7a4c58c2526cdf25e01830baa11410c7ece1" +dependencies = [ + "orx-iterable", + "orx-pseudo-default", +] + +[[package]] +name = "orx-concurrent-option" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842a5c05d6f02368d1cdfebec87ae6e2277ca0cf544ab3778e6f2e6c5c947da3" + +[[package]] +name = "orx-concurrent-vec" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad98d4d130277af640a2222188f0f706f25515be29b9b74519fad790b6a58e6e" +dependencies = [ + "orx-concurrent-option", + "orx-fixed-vec", + "orx-pinned-concurrent-col", + "orx-pinned-vec", + "orx-pseudo-default", + "orx-split-vec", +] + +[[package]] +name = "orx-fixed-vec" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96586d7477170175263986dfdfd36cc9a4b08bcc1743084f06115d6610181bb4" +dependencies = [ + "orx-concurrent-iter", + "orx-iterable", + "orx-pinned-vec", + "orx-pseudo-default", +] + +[[package]] +name = "orx-iterable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa2cb3f82a187c68835faac9cf03faaee70b93f4da3b85515ac1b4c6f8a432d" +dependencies = [ + "orx-self-or", +] + +[[package]] +name = "orx-pinned-concurrent-col" +version = "2.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b22c9e39d97d1c1d63dcbd9be68ac414cc61c9c6b8e799a125d383a18f3f10" +dependencies = [ + "orx-fixed-vec", + "orx-pinned-vec", + "orx-pseudo-default", + "orx-split-vec", +] + +[[package]] +name = "orx-pinned-vec" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38ff024d902a587fefdc502598107bbbf3d2a5350d63247bef34bd6a5518de9" +dependencies = [ + "orx-iterable", + "orx-pseudo-default", +] + +[[package]] +name = "orx-pseudo-default" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34eaace9ae01f7025804fbca40ec45b87c19ba0328d97195e01c6135897762a8" + +[[package]] +name = "orx-self-or" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8e35dfe18921e475b9861266fd58a5ecfd681161f242d24a9e2d1e07fbc28" + +[[package]] +name = "orx-split-vec" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794100ff181e3903c90398e91147bbba7734ab0a1c6e13c4c569f4a15bccd6f7" +dependencies = [ + "orx-concurrent-iter", + "orx-iterable", + "orx-pinned-vec", + "orx-pseudo-default", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -303,10 +850,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] [[package]] name = "proc-macro2" @@ -336,9 +908,15 @@ dependencies = [ name = "prometric" version = "0.1.4" dependencies = [ + "arc-cell", "hyper", "hyper-util", + "metrics-exporter-prometheus", + "metrics-util", + "orx-concurrent-vec", + "parking_lot", "prometheus", + "quanta", "sysinfo", "tokio", ] @@ -379,6 +957,21 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.42" @@ -388,6 +981,69 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -397,12 +1053,164 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.15.1" @@ -425,6 +1233,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.110" @@ -496,6 +1310,7 @@ version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", @@ -515,6 +1330,29 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower-service" version = "0.3.3" @@ -552,6 +1390,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -567,6 +1417,70 @@ 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.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -697,13 +1611,22 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "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", + "windows-targets 0.53.5", ] [[package]] @@ -715,6 +1638,22 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "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" @@ -722,14 +1661,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link 0.2.1", - "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.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]] @@ -741,50 +1680,130 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows_aarch64_gnullvm" +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" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/README.md b/README.md index bc78205..7c1a600 100644 --- a/README.md +++ b/README.md @@ -18,51 +18,32 @@ instead of [metrics](https://docs.rs/metrics/latest/metrics), and supports dynam ### Basic Usage -```rust -use prometric_derive::metrics; -use prometric::{Counter, Gauge, Histogram}; +See [`basic_usage`](./prometric-derive/examples/basic_usage.rs) example for usage. Here's a reduced example usage: +``` rust // The `scope` attribute is used to set the prefix for the metric names in this struct. #[metrics(scope = "app")] struct AppMetrics { /// The total number of HTTP requests. #[metric(rename = "http_requests_total", labels = ["method", "path"])] http_requests: Counter, - - // For histograms, the `buckets` attribute is optional. It will default to [prometheus::DEFAULT_BUCKETS] if not provided. - // `buckets` can also be an expression that evaluates into a `Vec`. - /// The duration of HTTP requests. - #[metric(labels = ["method", "path"], buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0])] - http_requests_duration: Histogram, - - /// This doc comment will be overwritten by the `help` attribute. - #[metric(rename = "current_active_users", labels = ["service"], help = "The current number of active users.")] - current_users: Gauge, - - /// The balance of the account, in dollars. Uses a floating point number. - #[metric(rename = "account_balance", labels = ["account_id"])] - account_balance: Gauge, - - /// The total number of errors. - #[metric] - errors: Counter, } -// Build the metrics struct with static labels, which will initialize and register the metrics with the default registry. -// A custom registry can be used by passing it to the builder using `with_registry`. -let metrics = AppMetrics::builder().with_label("host", "localhost").with_label("port", "8080").build(); +// Build the metrics struct with static labels, which will initialize and register the metrics +// with the default registry. A custom registry can be used by passing it to the builder +// using `with_registry`. +let metrics = + AppMetrics::builder().with_label("host", "localhost").with_label("port", "8080").build(); -// Metric fields each get an accessor method generated, which can be used to interact with the metric. -// The arguments to the accessor method are the labels for the metric. +// Metric fields each get an accessor method generated, which can be used to interact with the +// metric. The arguments to the accessor method are the labels for the metric. metrics.http_requests("GET", "/").inc(); -metrics.http_requests_duration("GET", "/").observe(1.0); -metrics.current_users("service-1").set(10); -metrics.account_balance("1234567890").set(-12.2); -metrics.errors().inc(); ``` #### Sample Output +TODO: document how to obtain sample output + ```text # HELP app_account_balance The balance of the account, in dollars. Uses a floating point number. # TYPE app_account_balance gauge @@ -102,47 +83,13 @@ app_http_requests_total{host="localhost",method="POST",path="/",port="8080"} 2 You can also generate a static `LazyLock` instance by using the `static` attribute. When enabled, the builder methods and `Default` implementation are made private, ensuring the only way to access the metrics is through the static instance: -```rust -use prometric_derive::metrics; -use prometric::{Counter, Gauge}; - -#[metrics(scope = "app", static)] -struct AppMetrics { - /// The total number of requests. - #[metric(labels = ["method"])] - requests: Counter, - - /// The current number of active connections. - #[metric] - active_connections: Gauge, -} - -// Use the static directly (the name is APP_METRICS in SCREAMING_SNAKE_CASE) -APP_METRICS.requests("GET").inc(); -APP_METRICS.active_connections().set(10); - -// The following would not compile: -// let metrics = AppMetrics::builder(); // Error: builder() is private -// let metrics = AppMetrics::default(); // Error: Default is not implemented -``` +See [`static_metrics`](./prometric-derive/examples/static_metrics.rs) example for usage. ### Exporting Metrics -An HTTP exporter is provided by [`prometric::exporter::ExporterBuilder`]. Usage: +An HTTP exporter is provided by [`prometric::exporter::ExporterBuilder`]. -```rust -use prometric::exporter::ExporterBuilder; - -ExporterBuilder::new() - // Specify the address to listen on - .with_address("127.0.0.1:9090") - // Set the global namespace for the metrics (usually the name of the application) - .with_namespace("exporter") - // Install the exporter. This will start an HTTP server and serve metrics on the specified - // address. - .install() - .expect("Failed to install exporter"); -``` +See [`exporter`](./prometric-derive/examples/exporter.rs) example for usage. ### Process Metrics diff --git a/justfile b/justfile new file mode 100644 index 0000000..214d6ec --- /dev/null +++ b/justfile @@ -0,0 +1,16 @@ +default: check doc fmt clippy + +check: + cargo check --workspace --all-features --all-targets + +doc: + cargo doc --workspace --all-features --no-deps --document-private-items + +clippy: + cargo +nightly clippy --all --all-features -- -D warnings + +fmt: + cargo +nightly fmt --all -- --check + +test: + cargo nextest run --workspace --all-features --retries 3 diff --git a/prometric-derive/examples/basic_usage.rs b/prometric-derive/examples/basic_usage.rs new file mode 100644 index 0000000..bc01e69 --- /dev/null +++ b/prometric-derive/examples/basic_usage.rs @@ -0,0 +1,51 @@ +use prometric::{Counter, Gauge, Histogram, Summary}; +use prometric_derive::metrics; + +// The `scope` attribute is used to set the prefix for the metric names in this struct. +#[metrics(scope = "app")] +struct AppMetrics { + /// The total number of HTTP requests. + #[metric(rename = "http_requests_total", labels = ["method", "path"])] + http_requests: Counter, + + // For histograms, the `buckets` attribute is optional. It will default to + // [prometheus::DEFAULT_BUCKETS] if not provided. `buckets` can also be an expression that + // evaluates into a `Vec`. + /// The duration of HTTP requests. + #[metric(labels = ["method", "path"], buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0])] + http_requests_duration: Histogram, + + /// The size fo HTTP requests. + #[metric(labels = ["method", "path"], quantiles = [0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0])] + http_request_sizes: Summary, + + /// This doc comment will be overwritten by the `help` attribute. + #[metric(rename = "current_active_users", labels = ["service"], help = "The current number of active users.")] + current_users: Gauge, + + /// The balance of the account, in dollars. Uses a floating point number. + #[metric(rename = "account_balance", labels = ["account_id"])] + account_balance: Gauge, + + /// The total number of errors. + #[metric] + errors: Counter, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + // Build the metrics struct with static labels, which will initialize and register the metrics + // with the default registry. A custom registry can be used by passing it to the builder + // using `with_registry`. + let metrics = + AppMetrics::builder().with_label("host", "localhost").with_label("port", "8080").build(); + + // Metric fields each get an accessor method generated, which can be used to interact with the + // metric. The arguments to the accessor method are the labels for the metric. + metrics.http_requests("GET", "/").inc(); + metrics.http_requests_duration("GET", "/").observe(1.0); + metrics.http_request_sizes("GET", "/").observe(12345); + metrics.current_users("service-1").set(10); + metrics.account_balance("1234567890").set(-12.2); + metrics.errors().inc(); +} diff --git a/prometric-derive/examples/exporter.rs b/prometric-derive/examples/exporter.rs index 410352e..17d37b8 100644 --- a/prometric-derive/examples/exporter.rs +++ b/prometric-derive/examples/exporter.rs @@ -3,9 +3,11 @@ use prometric_derive::metrics; #[metrics(scope = "example")] struct ExampleMetrics { + /// A simple counter #[metric] counter: Counter, + /// A simple gauge #[metric] gauge: Gauge, } diff --git a/prometric-derive/examples/static_metrics.rs b/prometric-derive/examples/static_metrics.rs new file mode 100644 index 0000000..7c6ac5e --- /dev/null +++ b/prometric-derive/examples/static_metrics.rs @@ -0,0 +1,24 @@ +use prometric::{Counter, Gauge}; +use prometric_derive::metrics; + +#[metrics(scope = "app", static)] +struct AppMetrics { + /// The total number of requests. + #[metric(labels = ["method"])] + requests: Counter, + + /// The current number of active connections. + #[metric] + active_connections: Gauge, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + // Use the static directly (the name is APP_METRICS in SCREAMING_SNAKE_CASE) + APP_METRICS.requests("GET").inc(); + APP_METRICS.active_connections().set(10); + + // The following would not compile: + // let metrics = AppMetrics::builder(); // Error: builder() is private + // let metrics = AppMetrics::default(); // Error: Default is not implemented +} diff --git a/prometric-derive/src/expand.rs b/prometric-derive/src/expand.rs index 1154777..8855461 100644 --- a/prometric-derive/src/expand.rs +++ b/prometric-derive/src/expand.rs @@ -2,8 +2,8 @@ use darling::{FromField, FromMeta}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ - Field, GenericArgument, Ident, ItemStruct, LitFloat, LitStr, PathArguments, PathSegment, - Result, Type, + Field, GenericArgument, Ident, ItemStruct, LitFloat, LitStr, PathArguments, Result, Type, + TypePath, }; use crate::utils::{snake_to_pascal, to_screaming_snake}; @@ -25,10 +25,25 @@ pub(super) struct MetricsAttr { _static: bool, } +/// A wrapper over [`prometric`] metric types, containing their type path and generic +/// arguments, if any. +/// +/// ```ignore +/// # use syn::parse_str; +/// +/// let counter_ty = +/// MetricType::from_path(parse_str("::prometric::Counter").unwrap()).unwrap(); +/// assert!(matches!(counter_ty, MetricType::Counter("::prometric::Counter", u64))); +/// +/// let guauge_ty = +/// MetricType::from_path(parse_str("Gauge").unwrap()).unwrap(); +/// assert!(matches!(gauge_ty, MetricType::Gauge("Gauge", ::prometric::GaugeDefault))); +/// ``` enum MetricType { - Counter(Ident, Type), - Gauge(Ident, Type), - Histogram(Ident), + Counter(TypePath, Type), + Gauge(TypePath, Type), + Histogram(TypePath), + Summary(TypePath), } impl std::fmt::Display for MetricType { @@ -37,52 +52,81 @@ impl std::fmt::Display for MetricType { Self::Counter(_, _) => write!(f, "Counter"), Self::Gauge(_, _) => write!(f, "Gauge"), Self::Histogram(_) => write!(f, "Histogram"), + Self::Summary(_) => write!(f, "Summary"), } } } impl MetricType { - /// Parse the metric type (and generic argument) from a path segment. - fn from_segment(segment: &PathSegment) -> Result { - let ident = segment.ident.clone(); - - // Parse the potential generic argument. - let maybe_generic = match &segment.arguments { - PathArguments::None => None, + /// Extract the generic argument (if specified) from the given PathArguments. + /// + /// Will return error if the path arguments are of the [`PathArgumnets::Parenthesized`] kind, + /// or if there's more than 1 argument, or if the argument is not a type argument + fn generic_argument(args: &PathArguments) -> Result> { + match &args { + PathArguments::None => Ok(None), PathArguments::AngleBracketed(generic) => { if generic.args.len() != 1 { return Err(syn::Error::new_spanned( - segment, + generic, "Expected a single generic argument", )); } let arg = &generic.args[0]; if let GenericArgument::Type(ty) = arg { - Some(ty.clone()) + Ok(Some(ty.clone())) } else { - return Err(syn::Error::new_spanned(arg, "Expected a type argument")); + Err(syn::Error::new_spanned(arg, "Expected a type argument")) } } PathArguments::Parenthesized(_) => { - return Err(syn::Error::new_spanned(segment, "Expected a generic type argument")); + Err(syn::Error::new_spanned(args, "Expected a generic type argument")) } + } + } + + /// Parse the metric type (and generic argument) from a path segment. + fn from_path(mut path: TypePath) -> Result { + let last_segment = path.path.segments.last_mut().unwrap(); + let ident = last_segment.ident.clone(); + + let maybe_generic = Self::generic_argument(&last_segment.arguments)?; + + // Specifically override the generic argument of `dest` + // This effectively replaces the same type extracted in the `maybe_generic` block + let override_generic_arg = |ty, dest: &mut PathArguments| { + let args = syn::parse_quote! {<#ty>}; + *dest = PathArguments::AngleBracketed(args); }; + // Here we convert the parsed metric type (by identifier) into a variant of this enum. + // Additionally, we encode the generic argument as part of the stored qualified type path, + // using the prometric type alias if the default generic argument was used, for consistency. + // + // For example: `prometric::Counter` is parsed the same as + // `prometric::Counter<::prometric::CounterDefault>` and will result in a + // `MetricType::Counte` with `prometric::Counter<::prometric::CounterDefault>` for the path, + // and `::prometric::CounterDefault` for the generic argument match ident.to_string().as_str() { "Counter" => { - // NOTE: Use the prometric type alias here so it remains consistent. let generic = - maybe_generic.unwrap_or(syn::parse_str("prometric::CounterDefault").unwrap()); - Ok(Self::Counter(ident.clone(), generic)) + maybe_generic.unwrap_or(syn::parse_str("::prometric::CounterDefault").unwrap()); + // Ensure the stored `path` has the generic argument + override_generic_arg(generic.clone(), &mut last_segment.arguments); + + Ok(Self::Counter(path, generic)) } "Gauge" => { - // NOTE: Use the prometric type alias here so it remains consistent. let generic = - maybe_generic.unwrap_or(syn::parse_str("prometric::GaugeDefault").unwrap()); - Ok(Self::Gauge(ident.clone(), generic)) + maybe_generic.unwrap_or(syn::parse_str("::prometric::GaugeDefault").unwrap()); + // Ensure the stored `path` has the generic argument + override_generic_arg(generic.clone(), &mut last_segment.arguments); + + Ok(Self::Gauge(path, generic)) } - "Histogram" => Ok(Self::Histogram(ident.clone())), + "Histogram" => Ok(Self::Histogram(path)), + "Summary" => Ok(Self::Summary(path)), other => Err(syn::Error::new_spanned( ident, format!("Unsupported metric type '{other}'. Use Counter, Gauge, or Histogram"), @@ -90,11 +134,80 @@ impl MetricType { } } - fn full_type(&self) -> TokenStream { + fn full_type(&self) -> &TypePath { + match self { + Self::Counter(path, _) | + Self::Gauge(path, _) | + Self::Histogram(path) | + Self::Summary(path) => path, + } + } + + fn partitions_for( + &self, + maybe_buckets: Option, + maybe_quantiles: Option, + ) -> Result { + match self { + MetricType::Counter(_, _) | MetricType::Gauge(_, _) => Ok(Partitions::NotApplicable), + MetricType::Histogram(_) => { + if maybe_quantiles.is_some() { + Err(syn::Error::new_spanned( + maybe_quantiles, + "Invalid configuration for Histogram: `quantiles` is not a valid option, use `buckets` or switch to Summary.", + )) + } else { + Ok(maybe_buckets.map(Partitions::Buckets).unwrap_or(Partitions::None)) + } + } + MetricType::Summary(_) => { + if maybe_buckets.is_some() { + Err(syn::Error::new_spanned( + maybe_buckets, + "Invalid configuration for Summary: `buckets` is not a valid option, use `quantiles` or switch to Histogram.", + )) + } else { + Ok(maybe_quantiles.map(Partitions::Quantiles).unwrap_or(Partitions::None)) + } + } + } + } +} + +/// Represents which partition for a given metric type was parsed +/// +/// This is realistically only useful for Histogram (`Buckets`) and Summaries (`Quantiles`). +/// +/// This enum also encodes if no partitioning was specified (`None`), or if one was specified but +/// the metric type doesn't make use of it (ie: Gauge, Counter) (as `NotApplicable`). +/// +/// Currently there's no difference between `None` and `NotApplicable`, but the latter might become +/// a hard error in the future, like when specifing `bucket` with a Counter metric. +enum Partitions { + /// No partitions specified + None, + /// Partitions not applicable to given metric type + /// + /// Examples: Gauge, Counter + NotApplicable, + /// Buckets of a histogram + Buckets(syn::Expr), + /// Quantiles of a summary + Quantiles(syn::Expr), +} + +impl Partitions { + fn buckets(&self) -> Option<&syn::Expr> { + match self { + Self::Buckets(buckets) => Some(buckets), + _ => None, + } + } + + fn quantiles(&self) -> Option<&syn::Expr> { match self { - Self::Counter(ident, ty) => quote! { #ident<#ty> }, - Self::Gauge(ident, ty) => quote! { #ident<#ty> }, - Self::Histogram(ident) => quote! { #ident }, + Self::Quantiles(quantiles) => Some(quantiles), + _ => None, } } } @@ -107,46 +220,59 @@ struct MetricBuilder { ty: MetricType, /// The label keys to define for the metric. labels: Option>, - /// The buckets to use for the histogram. - buckets: Option, /// The full name of the metric. /// = scope + separator + identifier || rename. full_name: String, /// The doc string of the metric. help: String, + /// The buckets of a histogram or the quantiles of a summary. + partitions: Partitions, } impl MetricBuilder { fn try_from(field: &Field, scope: &str) -> Result { let metric_field = MetricField::from_field(field)?; + if metric_field.buckets.is_some() && metric_field.quantiles.is_some() { + return Err(syn::Error::new_spanned( + field, + "The attributes `buckets` and `quantiles` are mutually exclusive", + )); + } - let help = metric_field - .help - .or_else(|| { - field - .attrs - .iter() - .find(|attr| attr.path().is_ident("doc")) - .map(|attr| { - let syn::Meta::NameValue(value) = &attr.meta else { - return Err(syn::Error::new_spanned(attr, "Expected a doc attribute")); - }; - - if let syn::Expr::Lit(lit) = &value.value { - if let syn::Lit::Str(lit) = &lit.lit { - Ok(lit.value().trim().to_string()) - } else { - Err(syn::Error::new_spanned(attr, "Expected a string literal")) - } + // prometheus::Opts requires a non-empty help string + // Here we retrieve it from the `help` argument of the `metric`, + // falling back to the documentation of the field otherwise + let help = metric_field.help.or_else(|| { + field + .attrs + .iter() + .find(|attr| attr.path().is_ident("doc")) + .map(|attr| { + let syn::Meta::NameValue(value) = &attr.meta else { + return Err(syn::Error::new_spanned(attr, "Expected a doc attribute")); + }; + + if let syn::Expr::Lit(lit) = &value.value { + if let syn::Lit::Str(lit) = &lit.lit { + Ok(lit.value().trim().to_string()) } else { Err(syn::Error::new_spanned(attr, "Expected a string literal")) } - }) - .transpose() - .ok() - .flatten() - }) - .unwrap_or_default(); + } else { + Err(syn::Error::new_spanned(attr, "Expected a string literal")) + } + }) + .transpose() + .ok() + .flatten() + }); + + let Some(help) = help else { + return Err(syn::Error::new_spanned( + field, + "Unable to determine `help` label for metric. Provide an explicit `help` argument to `metric` or document the field", + )); + }; let metric_name = metric_field .rename @@ -156,13 +282,13 @@ impl MetricBuilder { let full_name = format!("{scope}{DEFAULT_SEPARATOR}{metric_name}"); - let Type::Path(type_path) = &metric_field.ty else { + let Type::Path(type_path) = metric_field.ty else { return Err(syn::Error::new_spanned(field, "Expected a path type")); }; - let last_segment = type_path.path.segments.last().unwrap(); + let ty = MetricType::from_path(type_path)?; - let ty = MetricType::from_segment(last_segment)?; + let partitions = ty.partitions_for(metric_field.buckets, metric_field.quantiles)?; Ok(Self { identifier: metric_field @@ -172,7 +298,7 @@ impl MetricBuilder { labels: metric_field .labels .map(|labels| labels.iter().map(|label| label.value()).collect()), - buckets: metric_field.buckets, + partitions, full_name, help, }) @@ -189,21 +315,33 @@ impl MetricBuilder { let ty = self.ty.full_type(); let name = &self.full_name; let labels = self.labels(); - let buckets = &self.buckets; + let partitions = &self.partitions; - if let MetricType::Histogram(_) = &self.ty { - let buckets = if let Some(buckets_expr) = buckets { - quote! { Some(#buckets_expr.into()) } - } else { - quote! { None } - }; + match self.ty { + MetricType::Counter(_, _) | MetricType::Gauge(_, _) => quote! { + #ident: <#ty>::new(self.registry, #name, #help, &[#(#labels),*], self.labels.clone()) + }, + MetricType::Histogram(_) => { + let buckets = if let Some(buckets_expr) = partitions.buckets() { + quote! { Some(#buckets_expr.into()) } + } else { + quote! { None } + }; - quote! { - #ident: <#ty>::new(self.registry, #name, #help, &[#(#labels),*], self.labels.clone(), #buckets) + quote! { + #ident: <#ty>::new(self.registry, #name, #help, &[#(#labels),*], self.labels.clone(), #buckets) + } } - } else { - quote! { - #ident: <#ty>::new(self.registry, #name, #help, &[#(#labels),*], self.labels.clone()) + MetricType::Summary(_) => { + let quantiles = if let Some(quantiles_expr) = partitions.quantiles() { + quote! { Some(#quantiles_expr.into()) } + } else { + quote! { None } + }; + + quote! { + #ident: <#ty>::new(self.registry, #name, #help, &[#(#labels),*], self.labels.clone(), #quantiles) + } } } } @@ -212,7 +350,7 @@ impl MetricBuilder { let help = &self.help; let mut doc_builder = format!( "{help}\n\ - * Metric type: [prometric::{}]", + * Metric type: [`::prometric::{}`]", self.ty, ); @@ -220,11 +358,22 @@ impl MetricBuilder { doc_builder.push_str(&format!("\n* Labels: {}\n", labels.join(", "))); } - if let MetricType::Histogram(_) = &self.ty { - if let Some(buckets_expr) = &self.buckets { - doc_builder.push_str(&format!("\n* Buckets: {}", quote! { #buckets_expr })); - } else { - doc_builder.push_str("\n* Buckets: [prometheus::DEFAULT_BUCKETS]"); + match self.ty { + MetricType::Counter(_, _) | MetricType::Gauge(_, _) => {} + MetricType::Histogram(_) => { + if let Some(buckets_expr) = self.partitions.buckets() { + doc_builder.push_str(&format!("\n* Buckets: {}", quote! { #buckets_expr })); + } else { + doc_builder.push_str("\n* Buckets: [`::prometheus::DEFAULT_BUCKETS`]"); + } + } + MetricType::Summary(_) => { + if let Some(quantiles_expr) = self.partitions.quantiles() { + doc_builder.push_str(&format!("\n* Quantiles: {}", quote! { #quantiles_expr })); + } else { + doc_builder + .push_str("\n* Buckets: [`::prometric::summary::DEFAULT_QUANTILES`]"); + } } } @@ -302,7 +451,7 @@ impl MetricBuilder { #vis fn inc_by(&self, value: V) where - V: prometric::IntoAtomic<#counter_ty>, + V: ::prometric::IntoAtomic<#counter_ty>, { #labels_array self.inner.inc_by(labels, value.into_atomic()); @@ -326,7 +475,7 @@ impl MetricBuilder { #vis fn add(&self, value: V) where - V: prometric::IntoAtomic<#gauge_ty>, + V: ::prometric::IntoAtomic<#gauge_ty>, { #labels_array self.inner.add(labels, value.into_atomic()); @@ -334,7 +483,7 @@ impl MetricBuilder { #vis fn sub(&self, value: V) where - V: prometric::IntoAtomic<#gauge_ty>, + V: ::prometric::IntoAtomic<#gauge_ty>, { #labels_array self.inner.sub(labels, value.into_atomic()); @@ -342,7 +491,7 @@ impl MetricBuilder { #vis fn set(&self, value: V) where - V: prometric::IntoAtomic<#gauge_ty>, + V: ::prometric::IntoAtomic<#gauge_ty>, { #labels_array self.inner.set(labels, value.into_atomic()); @@ -351,7 +500,16 @@ impl MetricBuilder { MetricType::Histogram(_) => quote! { #vis fn observe(&self, value: V) where - V: prometric::IntoAtomic, + V: ::prometric::IntoAtomic, + { + #labels_array + self.inner.observe(labels, value.into_atomic()); + } + }, + MetricType::Summary(_) => quote! { + #vis fn observe(&self, value: V) + where + V: ::prometric::IntoAtomic, { #labels_array self.inner.observe(labels, value.into_atomic()); @@ -381,11 +539,17 @@ struct MetricField { labels: Option>, /// The help string to use for the metric. Takes precedence over the doc attribute. help: Option, - /// The buckets to use for the histogram. - buckets: Option, /// The sample rate to use for the histogram. /// TODO: Implement this. sample: Option, + /// The buckets to use for the histogram. + /// + /// Mutually exclusive with `quantiles` + buckets: Option, + /// The quantiles to use for the summary. + /// + /// Mutually exclusive with `buckets` + quantiles: Option, } pub fn expand(metrics_attr: MetricsAttr, input: &mut ItemStruct) -> Result { @@ -417,8 +581,8 @@ pub fn expand(metrics_attr: MetricsAttr, input: &mut ItemStruct) -> Result { - registry: &'a prometheus::Registry, - labels: std::collections::HashMap, + registry: &'a ::prometheus::Registry, + labels: ::std::collections::HashMap, } impl<'a> #builder_name<'a> { @@ -450,7 +614,7 @@ pub fn expand(metrics_attr: MetricsAttr, input: &mut ItemStruct) -> Result = std::sync::LazyLock::new(|| #ident::builder().build()); + #vis static #static_name: ::std::sync::LazyLock<#ident> = ::std::sync::LazyLock::new(|| #ident::builder().build()); }) } else { None @@ -490,8 +654,8 @@ pub fn expand(metrics_attr: MetricsAttr, input: &mut ItemStruct) -> Result() -> #builder_name<'a> { #builder_name { - registry: prometheus::default_registry(), - labels: std::collections::HashMap::new(), + registry: ::prometheus::default_registry(), + labels: ::std::collections::HashMap::new(), } } diff --git a/prometric-derive/tests/macro.rs b/prometric-derive/tests/macro.rs index 7a8472e..9b91060 100644 --- a/prometric-derive/tests/macro.rs +++ b/prometric-derive/tests/macro.rs @@ -1,7 +1,6 @@ use std::time::Duration; use prometheus::Encoder as _; -use prometric::{Counter, Gauge, Histogram}; /// This is a struct that contains the metrics for the application. /// @@ -218,3 +217,62 @@ fn bucket_defaults_work() { assert!(output.contains("test_hist")); } + +#[test] +fn quantiles_defaults_work() { + #[prometric_derive::metrics(scope = "test")] + struct QuantileMetrics { + /// Test Summary metric with quantile expression. + #[metric] + summary: prometric::Summary, + } + + let registry = prometheus::default_registry(); + let app_metrics = QuantileMetrics::builder().with_registry(registry).build(); + + let duration = Duration::from_secs(1); + app_metrics.summary().observe(duration.as_secs_f64()); + + let encoder = prometheus::TextEncoder::new(); + let metric_families = registry.gather(); // Wait, need to expose registry + + let mut buffer = vec![]; + encoder.encode(&metric_families, &mut buffer).unwrap(); + let output = String::from_utf8(buffer).unwrap(); + + assert!(output.contains("test_summary")); +} + +#[test] +fn quantiles_with_batching_work() { + #[prometric_derive::metrics(scope = "test")] + struct QuantileMetrics { + /// Test Summary metric with quantile expression. + #[metric] + summary: prometric::Summary, + } + + let registry = prometheus::default_registry(); + let app_metrics = QuantileMetrics::builder().with_registry(registry).build(); + + let duration = Duration::from_secs(1); + for i in 0..10000 { + let start = std::time::Instant::now(); + println!("{i} "); + app_metrics.summary().observe(duration.as_secs_f64() * i as f64); + if i % 100 == 0 { + println!("Time taken: {:?}", start.elapsed()); + } + } + + let encoder = prometheus::TextEncoder::new(); + let metric_families = registry.gather(); // Wait, need to expose registry + + let mut buffer = vec![]; + encoder.encode(&metric_families, &mut buffer).unwrap(); + let output = String::from_utf8(buffer).unwrap(); + + println!("{}", output); + + assert!(output.contains("test_summary")); +} diff --git a/prometric/Cargo.toml b/prometric/Cargo.toml index a9be72c..1a63c07 100644 --- a/prometric/Cargo.toml +++ b/prometric/Cargo.toml @@ -21,9 +21,21 @@ tokio = { version = "1.40.0", optional = true, features = ["net", "rt"] } # Process sysinfo = { version = "0.37.2", optional = true } +# Summary +arc-cell = {version = "0.3.3", optional = true } +metrics-util = { version = "0.20.0", optional = true } +metrics-exporter-prometheus = { version = "0.17.2", optional = true } +orx-concurrent-vec = { version = "3.10.0", optional = true } +parking_lot = { version = "0.12", optional = true } +quanta = { version = "0.12.6", optional = true } + [features] -default = ["exporter"] +default = ["exporter", "summary"] # Expose HTTP exporter functionality with the `hyper` crate. Enabled by default. exporter = ["dep:hyper", "dep:hyper-util", "dep:tokio"] # Expose process metrics collection functionality with the `sysinfo` crate. process = ["dep:sysinfo"] +# Expose a Summary functionality. Enabled by default +summary = ["dep:metrics-util", "dep:metrics-exporter-prometheus", "dep:parking_lot", "dep:quanta", "dep:orx-concurrent-vec", "dep:arc-cell"] + +[dev-dependencies] diff --git a/prometric/src/counter.rs b/prometric/src/counter.rs new file mode 100644 index 0000000..beb31b2 --- /dev/null +++ b/prometric/src/counter.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; + +use crate::private::Sealed; + +/// The default number type for counters. +pub type CounterDefault = u64; + +/// A marker trait for numbers that can be used as counter values. +/// Supported types: `u64`, `f64` +pub trait CounterNumber: Sized + 'static + Sealed { + /// The atomic type associated with this number type. + type Atomic: prometheus::core::Atomic; +} + +impl CounterNumber for u64 { + type Atomic = prometheus::core::AtomicU64; +} + +impl CounterNumber for f64 { + type Atomic = prometheus::core::AtomicF64; +} + +/// A counter metric with a generic number type. Default is `u64`, which provides better performance +/// for natural numbers. +#[derive(Debug)] +pub struct Counter { + inner: prometheus::core::GenericCounterVec, +} + +impl Clone for Counter { + fn clone(&self) -> Self { + Self { inner: self.inner.clone() } + } +} + +impl Counter { + /// Create a new counter metric with the given registry, name, help, labels, and const labels. + pub fn new( + registry: &prometheus::Registry, + name: &str, + help: &str, + labels: &[&str], + const_labels: HashMap, + ) -> Self { + let opts = prometheus::Opts::new(name, help).const_labels(const_labels); + let metric = prometheus::core::GenericCounterVec::::new(opts, labels).unwrap(); + + let boxed = Box::new(metric.clone()); + if let Err(e) = registry.register(boxed.clone()) { + let id = format!("{}, Labels: {}", name, labels.join(", "),); + // If the metric is already registered, overwrite it. + if matches!(e, prometheus::Error::AlreadyReg) { + registry + .unregister(boxed.clone()) + .unwrap_or_else(|_| panic!("Failed to unregister metric {id}")); + + registry + .register(boxed) + .unwrap_or_else(|_| panic!("Failed to overwrite metric {id}")); + } else { + panic!("Failed to register metric {id}"); + } + } + + Self { inner: metric } + } + + pub fn inc(&self, labels: &[&str]) { + self.inner.with_label_values(labels).inc(); + } + + pub fn inc_by(&self, labels: &[&str], value: ::T) { + self.inner.with_label_values(labels).inc_by(value); + } + + pub fn reset(&self, labels: &[&str]) { + self.inner.with_label_values(labels).reset(); + } +} diff --git a/prometric/src/gauge.rs b/prometric/src/gauge.rs new file mode 100644 index 0000000..0245de6 --- /dev/null +++ b/prometric/src/gauge.rs @@ -0,0 +1,91 @@ +use std::collections::HashMap; + +use crate::private::Sealed; + +/// The default number type for gauges. +pub type GaugeDefault = u64; + +/// A marker trait for numbers that can be used as gauge values. +/// Supported types: `i64`, `f64`, `u64` +pub trait GaugeNumber: Sized + 'static + Sealed { + /// The atomic type associated with this number type. + type Atomic: prometheus::core::Atomic; +} + +impl GaugeNumber for i64 { + type Atomic = prometheus::core::AtomicI64; +} + +impl GaugeNumber for f64 { + type Atomic = prometheus::core::AtomicF64; +} + +impl GaugeNumber for u64 { + type Atomic = prometheus::core::AtomicU64; +} + +/// A gauge metric with a generic number type. Default is `i64`, which provides better performance +/// for integers. +#[derive(Debug)] +pub struct Gauge { + inner: prometheus::core::GenericGaugeVec, +} + +impl Clone for Gauge { + fn clone(&self) -> Self { + Self { inner: self.inner.clone() } + } +} + +impl Gauge { + /// Create a new gauge metric with the given registry, name, help, labels, and const labels. + pub fn new( + registry: &prometheus::Registry, + name: &str, + help: &str, + labels: &[&str], + const_labels: HashMap, + ) -> Self { + let opts = prometheus::Opts::new(name, help).const_labels(const_labels); + let metric = prometheus::core::GenericGaugeVec::::new(opts, labels).unwrap(); + + let boxed = Box::new(metric.clone()); + if let Err(e) = registry.register(boxed.clone()) { + let id = format!("{}, Labels: {}", name, labels.join(", "),); + // If the metric is already registered, overwrite it. + if matches!(e, prometheus::Error::AlreadyReg) { + registry + .unregister(boxed.clone()) + .unwrap_or_else(|_| panic!("Failed to unregister metric {id}")); + + registry + .register(boxed) + .unwrap_or_else(|_| panic!("Failed to overwrite metric {id}")); + } else { + panic!("Failed to register metric {id}"); + } + } + + Self { inner: metric } + } + + pub fn inc(&self, labels: &[&str]) { + self.inner.with_label_values(labels).inc(); + } + + pub fn dec(&self, labels: &[&str]) { + self.inner.with_label_values(labels).dec(); + } + + pub fn add(&self, labels: &[&str], value: ::T) { + self.inner.with_label_values(labels).add(value); + } + + pub fn sub(&self, labels: &[&str], value: ::T) { + self.inner.with_label_values(labels).sub(value); + } + + pub fn set(&self, labels: &[&str], value: ::T) { + self.inner.with_label_values(labels).set(value); + } +} diff --git a/prometric/src/histogram.rs b/prometric/src/histogram.rs new file mode 100644 index 0000000..bd8dc8e --- /dev/null +++ b/prometric/src/histogram.rs @@ -0,0 +1,52 @@ +use std::collections::HashMap; + +/// A histogram metric. +#[derive(Debug)] +pub struct Histogram { + inner: prometheus::HistogramVec, +} + +impl Clone for Histogram { + fn clone(&self) -> Self { + Self { inner: self.inner.clone() } + } +} + +impl Histogram { + pub fn new( + registry: &prometheus::Registry, + name: &str, + help: &str, + labels: &[&str], + const_labels: HashMap, + buckets: Option>, + ) -> Self { + let buckets = buckets.unwrap_or(prometheus::DEFAULT_BUCKETS.to_vec()); + let opts = + prometheus::HistogramOpts::new(name, help).const_labels(const_labels).buckets(buckets); + let metric = prometheus::HistogramVec::new(opts, labels).unwrap(); + + let boxed = Box::new(metric.clone()); + if let Err(e) = registry.register(boxed.clone()) { + let id = format!("{}, Labels: {}", name, labels.join(", "),); + // If the metric is already registered, overwrite it. + if matches!(e, prometheus::Error::AlreadyReg) { + registry + .unregister(boxed.clone()) + .unwrap_or_else(|_| panic!("Failed to unregister metric {id}")); + + registry + .register(boxed) + .unwrap_or_else(|_| panic!("Failed to overwrite metric {id}")); + } else { + panic!("Failed to register metric {id}"); + } + } + + Self { inner: metric } + } + + pub fn observe(&self, labels: &[&str], value: f64) { + self.inner.with_label_values(labels).observe(value); + } +} diff --git a/prometric/src/lib.rs b/prometric/src/lib.rs index dfd9030..4a9d757 100644 --- a/prometric/src/lib.rs +++ b/prometric/src/lib.rs @@ -2,11 +2,10 @@ //! Prometheus core types. These types are primarily used for *defining* metrics, and not for //! *using* them. The actual usage of metrics is done through the generated structs from the //! `prometric-derive` crate. -//! - [`Counter`]: A counter metric. -//! - [`Gauge`]: A gauge metric. -//! - [`Histogram`]: A histogram metric. - -use std::collections::HashMap; +//! - [`counter::Counter`]: A counter metric. +//! - [`gauge::Gauge`]: A gauge metric. +//! - [`histogram::Histogram`]: A histogram metric. +//! - [`summary::Summary`]: A summary metric. Requires the `summary` feature to be enabled. #[cfg(feature = "exporter")] pub mod exporter; @@ -14,6 +13,20 @@ pub mod exporter; #[cfg(feature = "process")] pub mod process; +pub mod counter; +pub use counter::*; + +pub mod gauge; +pub use gauge::*; + +pub mod histogram; +pub use histogram::*; + +#[cfg(feature = "summary")] +pub mod summary; +#[cfg(feature = "summary")] +pub use summary::*; + /// Sealed trait to prevent outside code from implementing the metric types. mod private { pub trait Sealed {} @@ -67,218 +80,3 @@ impl_into_atomic!(i32 => f64); impl_into_atomic!(u32 => f64); impl_into_atomic!(usize => f64); impl_into_atomic!(f32 => f64); - -/// The default number type for counters. -pub type CounterDefault = u64; - -/// The default number type for gauges. -pub type GaugeDefault = u64; - -/// A marker trait for numbers that can be used as counter values. -/// Supported types: `u64`, `f64` -pub trait CounterNumber: Sized + 'static + private::Sealed { - /// The atomic type associated with this number type. - type Atomic: prometheus::core::Atomic; -} - -impl CounterNumber for u64 { - type Atomic = prometheus::core::AtomicU64; -} - -impl CounterNumber for f64 { - type Atomic = prometheus::core::AtomicF64; -} - -/// A marker trait for numbers that can be used as gauge values. -/// Supported types: `i64`, `f64`, `u64` -pub trait GaugeNumber: Sized + 'static + private::Sealed { - /// The atomic type associated with this number type. - type Atomic: prometheus::core::Atomic; -} - -impl GaugeNumber for i64 { - type Atomic = prometheus::core::AtomicI64; -} - -impl GaugeNumber for f64 { - type Atomic = prometheus::core::AtomicF64; -} - -impl GaugeNumber for u64 { - type Atomic = prometheus::core::AtomicU64; -} - -/// A counter metric with a generic number type. Default is `u64`, which provides better performance -/// for natural numbers. -#[derive(Debug)] -pub struct Counter { - inner: prometheus::core::GenericCounterVec, -} - -impl Clone for Counter { - fn clone(&self) -> Self { - Self { inner: self.inner.clone() } - } -} - -impl Counter { - /// Create a new counter metric with the given registry, name, help, labels, and const labels. - pub fn new( - registry: &prometheus::Registry, - name: &str, - help: &str, - labels: &[&str], - const_labels: HashMap, - ) -> Self { - let opts = prometheus::Opts::new(name, help).const_labels(const_labels); - let metric = prometheus::core::GenericCounterVec::::new(opts, labels).unwrap(); - - let boxed = Box::new(metric.clone()); - if let Err(e) = registry.register(boxed.clone()) { - let id = format!("{}, Labels: {}", name, labels.join(", "),); - // If the metric is already registered, overwrite it. - if matches!(e, prometheus::Error::AlreadyReg) { - registry - .unregister(boxed.clone()) - .unwrap_or_else(|_| panic!("Failed to unregister metric {id}")); - - registry - .register(boxed) - .unwrap_or_else(|_| panic!("Failed to overwrite metric {id}")); - } else { - panic!("Failed to register metric {id}"); - } - } - - Self { inner: metric } - } - - pub fn inc(&self, labels: &[&str]) { - self.inner.with_label_values(labels).inc(); - } - - pub fn inc_by(&self, labels: &[&str], value: ::T) { - self.inner.with_label_values(labels).inc_by(value); - } - - pub fn reset(&self, labels: &[&str]) { - self.inner.with_label_values(labels).reset(); - } -} - -/// A gauge metric with a generic number type. Default is `i64`, which provides better performance -/// for integers. -#[derive(Debug)] -pub struct Gauge { - inner: prometheus::core::GenericGaugeVec, -} - -impl Clone for Gauge { - fn clone(&self) -> Self { - Self { inner: self.inner.clone() } - } -} - -impl Gauge { - /// Create a new gauge metric with the given registry, name, help, labels, and const labels. - pub fn new( - registry: &prometheus::Registry, - name: &str, - help: &str, - labels: &[&str], - const_labels: HashMap, - ) -> Self { - let opts = prometheus::Opts::new(name, help).const_labels(const_labels); - let metric = prometheus::core::GenericGaugeVec::::new(opts, labels).unwrap(); - - let boxed = Box::new(metric.clone()); - if let Err(e) = registry.register(boxed.clone()) { - let id = format!("{}, Labels: {}", name, labels.join(", "),); - // If the metric is already registered, overwrite it. - if matches!(e, prometheus::Error::AlreadyReg) { - registry - .unregister(boxed.clone()) - .unwrap_or_else(|_| panic!("Failed to unregister metric {id}")); - - registry - .register(boxed) - .unwrap_or_else(|_| panic!("Failed to overwrite metric {id}")); - } else { - panic!("Failed to register metric {id}"); - } - } - - Self { inner: metric } - } - - pub fn inc(&self, labels: &[&str]) { - self.inner.with_label_values(labels).inc(); - } - - pub fn dec(&self, labels: &[&str]) { - self.inner.with_label_values(labels).dec(); - } - - pub fn add(&self, labels: &[&str], value: ::T) { - self.inner.with_label_values(labels).add(value); - } - - pub fn sub(&self, labels: &[&str], value: ::T) { - self.inner.with_label_values(labels).sub(value); - } - - pub fn set(&self, labels: &[&str], value: ::T) { - self.inner.with_label_values(labels).set(value); - } -} - -/// A histogram metric. -#[derive(Debug)] -pub struct Histogram { - inner: prometheus::HistogramVec, -} - -impl Clone for Histogram { - fn clone(&self) -> Self { - Self { inner: self.inner.clone() } - } -} - -impl Histogram { - pub fn new( - registry: &prometheus::Registry, - name: &str, - help: &str, - labels: &[&str], - const_labels: HashMap, - buckets: Option>, - ) -> Self { - let buckets = buckets.unwrap_or(prometheus::DEFAULT_BUCKETS.to_vec()); - let opts = - prometheus::HistogramOpts::new(name, help).const_labels(const_labels).buckets(buckets); - let metric = prometheus::HistogramVec::new(opts, labels).unwrap(); - - let boxed = Box::new(metric.clone()); - if let Err(e) = registry.register(boxed.clone()) { - let id = format!("{}, Labels: {}", name, labels.join(", "),); - // If the metric is already registered, overwrite it. - if matches!(e, prometheus::Error::AlreadyReg) { - registry - .unregister(boxed.clone()) - .unwrap_or_else(|_| panic!("Failed to unregister metric {id}")); - - registry - .register(boxed) - .unwrap_or_else(|_| panic!("Failed to overwrite metric {id}")); - } else { - panic!("Failed to register metric {id}"); - } - } - - Self { inner: metric } - } - - pub fn observe(&self, labels: &[&str], value: f64) { - self.inner.with_label_values(labels).observe(value); - } -} diff --git a/prometric/src/summary/batching.rs b/prometric/src/summary/batching.rs new file mode 100644 index 0000000..602db36 --- /dev/null +++ b/prometric/src/summary/batching.rs @@ -0,0 +1,244 @@ +//! Summary with concurrent measurements (via batching) + +use std::sync::Arc; + +use arc_cell::ArcCell; +use parking_lot::RwLock; + +use crate::summary::traits::{NonConcurrentSummaryProvider, SummaryProvider}; + +pub const DEFAULT_BATCH_SIZE: usize = 128; + +/// The configuration for the [`BatchedSummary`] +#[derive(Clone)] +pub struct BatchOpts { + /// The number of measurements to batch before committing to the inner Summary + pub batch_size: usize, + pub inner: O, +} + +impl BatchOpts { + pub fn from_inner(inner: O) -> Self { + Self { batch_size: DEFAULT_BATCH_SIZE, inner } + } + + pub fn with_batch_size(self, batch_size: usize) -> Self { + Self { batch_size, ..self } + } +} + +// TODO: switch to FixedVec +// NOTE: ConcurrentVec doesn't currently implement `Clone` over _all_ possible `P`, but only on the +// default one +type Batch = orx_concurrent_vec::ConcurrentVec< + T, + orx_concurrent_vec::SplitVec< + orx_concurrent_vec::ConcurrentElement, + orx_concurrent_vec::Doubling, + >, +>; + +/// Wraps over the given [`NonConcurrentSummaryProvider`] `P` to batch measurements according to +/// configured batch size +/// +/// This is useful to transform a [`NonConcurrentSummaryProvider`] into a [`SummaryProvider`], with +/// a simple batching logic for improved lock accesses +#[derive(Debug)] +pub struct BatchedSummary

{ + batch_size: usize, + // We use ArcCell to allow more measurements to be recorded while the batch is being committed + measurements: ArcCell>, + inner: RwLock

, +} + +impl Clone for BatchedSummary

{ + fn clone(&self) -> Self { + // [ `ArcCell::clone` ] just makes a clone to the inner Arc + let measurements = Batch::clone(&self.measurements.get()); + + Self { + measurements: ArcCell::new(Arc::new(measurements)), + batch_size: self.batch_size, + inner: RwLock::new(self.inner.read().clone()), + } + } +} + +impl BatchedSummary

{ + // These exists for utility, to avoid having to use the provider trait + pub fn new(opts: &BatchOpts) -> Self { + ::new_provider(opts) + } + + // These exists for utility, to avoid having to use the provider trait + pub fn snapshot(&self) -> P::Summary { + SummaryProvider::snapshot(self) + } + + fn new_batch(batch_size: usize) -> Arc> { + // We will always have at most `batch_size` measurements before committing, so let's + // preallocate enough capacity + + // NOTE: We should also overallocate to have some overhead if + // some measurements are added before the commit operation takes ownership of the + // current batch + + // NOTE: `SplitVec` can't be initialized with a requested total capacity directly + let mut batch = Batch::new(); + batch.reserve_maximum_capacity(batch_size); + + Arc::new(batch) + } + + /// Wait for the given Arc to have a single owner and obtain the inner value + fn wait_for_arc(mut arc: Arc) -> T { + loop { + match Arc::try_unwrap(arc) { + Ok(inner) => return inner, + Err(this) => { + arc = this; + } + } + + std::hint::spin_loop(); + } + } + + /// Commits the current measurements batch to the underlying summary + /// + /// Will clear current the measurements batch + pub fn commit(&self) { + // If [`Batch`] had something like `.take()` the [`ArcCell`] would be unnecessary + // NOTE: we take the previous batch so new measurements can be added without changing + // the set that we are currently committing + let measurements = self.measurements.set(Self::new_batch(self.batch_size)); + let measurements = Self::wait_for_arc(measurements); + + let mut inner = self.inner.write(); + + for measure in measurements.into_iter() { + inner.observe(measure); + } + } + + /// Retrieve the inner summary + /// + /// Will commit the current batch before returning the summary + pub fn into_inner(self) -> P { + self.commit(); + self.inner.into_inner() + } +} + +impl SummaryProvider for BatchedSummary

{ + type Opts = BatchOpts; + type Summary = P::Summary; + + fn new_provider(opts: &Self::Opts) -> Self { + let inner = RwLock::new(P::new_provider(&opts.inner)); + Self { + inner, + measurements: ArcCell::new(Self::new_batch(opts.batch_size)), + batch_size: opts.batch_size, + } + } + + fn observe(&self, val: f64) { + let measurements = self.measurements.get(); + measurements.push(val); + + if measurements.len() >= self.batch_size { + // forcefully drop the guard before committing + // to avoid deadlocks + std::mem::drop(measurements); + + // Commit the current batch + self.commit() + } + } + + fn snapshot(&self) -> Self::Summary { + // Forcefully commit the current batch before snapshotting + self.commit(); + self.inner.read().snapshot() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::{ + simple::{SimpleSummary, SimpleSummaryOpts}, + traits::Summary, + }; + + use super::*; + + #[test] + fn concurrent_observe() { + // TODO: Consider converting into quickcheck test + // parametrized by: batch size, number of measurements and concurrent tasks + let batch_size = DEFAULT_BATCH_SIZE; + + let opts = SimpleSummaryOpts::default(); + let opts = BatchOpts::from_inner(opts).with_batch_size(batch_size); + + let summary = BatchedSummary::::new(&opts); + let summary = Arc::new(summary); + + let tasks = 8; + let measurements = 50_000; + + let mut handles = Vec::with_capacity(tasks); + for _ in 0..tasks { + let summary = summary.clone(); + let task = std::thread::spawn(move || { + for i in 0..measurements { + summary.observe(i as f64) + } + }); + handles.push(task); + } + + for h in handles { + h.join().expect("no task panics"); + } + + let result = summary.snapshot(); + assert_eq!( + result.sample_count(), + tasks as u64 * measurements, + "Should have all measurements present in the collection" + ); + } + + #[test] + fn single_threaded_observe() { + // TODO: Consider converting into quickcheck test + // parametrized by: batch size, number of measurements + let batch_size = DEFAULT_BATCH_SIZE; + + let opts = SimpleSummaryOpts::default(); + let opts = BatchOpts::from_inner(opts).with_batch_size(batch_size); + + let summary = BatchedSummary::::new(&opts); + + let measurements = 50_000; + + for i in 0..measurements { + let start = std::time::Instant::now(); + summary.observe(i as f64); + if i % 100 == 0 { + println!("Time taken: {:?}", start.elapsed()); + } + } + + let result = summary.snapshot(); + assert_eq!( + result.sample_count(), + measurements, + "Should have all measurements present in the collection" + ); + } +} diff --git a/prometric/src/summary/generic.rs b/prometric/src/summary/generic.rs new file mode 100644 index 0000000..851527b --- /dev/null +++ b/prometric/src/summary/generic.rs @@ -0,0 +1,314 @@ +//! Enables a [`Summary`] to be represented as a prometheus Summary metric + +use std::{collections::HashMap, marker::PhantomData, ops::Deref, sync::Arc}; + +use prometheus::{ + Opts, + core::{Desc, Describer, Metric, MetricVecBuilder}, + proto as pp, +}; + +use crate::summary::traits::{Summary, SummaryMetric}; + +use super::traits::NonConcurrentSummaryProvider; + +// from metrics_exporter_prometheus::PrometheusBuilder::new +pub const DEFAULT_QUANTILES: &[f64] = &[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0]; + +/// Configuration options for [`GenericSummary`] +#[derive(Clone)] +pub struct SummaryOpts { + pub common_opts: Opts, + /// Used to initialize the specific [`SummaryProvider`] + pub summary_opts: O, + + /// Which quantiles to export + pub quantiles: Vec, +} + +// needed for MetricVecBuilder::P +impl Describer for SummaryOpts { + fn describe(&self) -> prometheus::Result { + self.common_opts.describe() + } +} + +impl SummaryOpts { + pub fn new, S2: Into>(name: S1, help: S2, summary: O) -> Self { + Self { + common_opts: Opts::new(name, help), + summary_opts: summary, + quantiles: Vec::from(DEFAULT_QUANTILES), + } + } + + /// See [`Opts::const_labels`] + pub fn const_labels(mut self, const_labels: HashMap) -> Self { + self.common_opts = self.common_opts.const_labels(const_labels); + self + } + + /// See [`Opts::variable_labels`] + pub fn variable_labels(mut self, variable_labels: Vec) -> Self { + self.common_opts = self.common_opts.variable_labels(variable_labels); + self + } + + /// Configure the quantiles to use when creating a prometheus protobuf summary + pub fn quantiles>>(self, quantiles: B) -> Self { + Self { quantiles: quantiles.into(), ..self } + } +} + +/// Uses the configured [`SummaryProvider`] `P` to collect observations and compute quantiles +/// +/// Main purpose is to wrap over the summary to convert it into a [`prometheus::proto::Summary`] +#[derive(Debug)] +pub struct GenericSummary

{ + label_pairs: Vec, + quantiles: Vec, + provider: P, +} + +impl GenericSummary

{ + pub fn new>( + opts: &SummaryOpts, + label_values: &[V], + ) -> prometheus::Result { + let desc = opts.common_opts.describe()?; + let label_pairs = make_label_pairs(&desc, label_values)?; + + Ok(Self { + label_pairs, + quantiles: opts.quantiles.clone(), + provider: P::new_provider(&opts.summary_opts), + }) + } + + /// Make a snapshot of the current summary state exposed as a Protobuf struct + pub fn proto(&self) -> pp::Summary { + let snapshot = self.provider.snapshot(); + let mut summary = pp::Summary::default(); + + summary.set_sample_sum(snapshot.sample_sum()); + summary.set_sample_count(snapshot.sample_count()); + + let mut quantiles = Vec::with_capacity(self.quantiles.len()); + for quantile in self.quantiles.iter().cloned() { + let mut q = pp::Quantile::default(); + q.set_quantile(quantile); + + // TODO: signal that this value was not computable if == None + let Some(val) = snapshot.quantile(quantile) else { continue }; + q.set_value(val); + + quantiles.push(q); + } + + summary.set_quantile(quantiles); + + summary + } +} + +impl

Deref for GenericSummary

{ + type Target = P; + + fn deref(&self) -> &Self::Target { + &self.provider + } +} + +/// NewType over [`GenericSummaryImpl`] +/// +/// Uses `Arc` to ensure clones refer to the same data. +/// This is becuase [ `prometheus::core::MetricVec` ] will clone the metric each time it is to be +/// accessed, even when inserting new data +#[derive(Clone)] +pub struct GenericSummaryMetric

(Arc>); + +impl

Deref for GenericSummaryMetric

{ + type Target = GenericSummary

; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl

From> for GenericSummaryMetric

{ + fn from(value: GenericSummary

) -> Self { + Self(Arc::new(value)) + } +} + +impl Metric for GenericSummaryMetric

{ + fn metric(&self) -> pp::Metric { + let mut m = pp::Metric::from_label(self.label_pairs.clone()); + m.set_summary(self.proto()); + m + } +} + +/// Similarly to [`::prometheus::HistogramVec`], but for Summaries. +pub struct SummaryVecBuilder { + _p: PhantomData, +} + +impl Clone for SummaryVecBuilder { + fn clone(&self) -> Self { + Self { _p: self._p } + } +} + +impl

SummaryVecBuilder

{ + pub fn new() -> Self { + Self { _p: PhantomData } + } +} + +impl MetricVecBuilder for SummaryVecBuilder { + // NOTE: [`prometheus::core::MetricVec`] clones this `M` whenever it is to be returned, + // given a set of label. + // Therefore, we want this clone to refer to the same instance of the data. + type M = GenericSummaryMetric; + type P = SummaryOpts; + + fn build>(&self, opts: &Self::P, vals: &[V]) -> prometheus::Result { + GenericSummary::::new(opts, vals).map(Into::into) + } +} + +// from prometheus::value::make_label_pairs +fn make_label_pairs>( + desc: &Desc, + label_values: &[V], +) -> prometheus::Result> { + if desc.variable_labels.len() != label_values.len() { + return Err(prometheus::Error::InconsistentCardinality { + expect: desc.variable_labels.len(), + got: label_values.len(), + }); + } + + let total_len = desc.variable_labels.len() + desc.const_label_pairs.len(); + if total_len == 0 { + return Ok(vec![]); + } + + if desc.variable_labels.is_empty() { + return Ok(desc.const_label_pairs.clone()); + } + + let mut label_pairs = Vec::with_capacity(total_len); + for (i, n) in desc.variable_labels.iter().enumerate() { + let mut label_pair = pp::LabelPair::default(); + label_pair.set_name(n.clone()); + label_pair.set_value(label_values[i].as_ref().to_owned()); + label_pairs.push(label_pair); + } + + for label_pair in &desc.const_label_pairs { + label_pairs.push(label_pair.clone()); + } + label_pairs.sort(); + Ok(label_pairs) +} + +#[cfg(test)] +mod tests { + use crate::{ + batching::{BatchOpts, BatchedSummary}, + rolling::{RollingSummary, RollingSummaryOpts}, + simple::{SimpleSummary, SimpleSummaryOpts}, + traits::SummaryProvider, + }; + + use super::*; + + const MEASUREMENTS: usize = 50_000; + const PRINT_EVERY: usize = 100; + + impl

GenericSummary

{ + pub fn inner_mut(&mut self) -> &mut P { + &mut self.provider + } + } + + fn measure_mut(mut summary: GenericSummary) { + for i in 0..MEASUREMENTS { + let start = std::time::Instant::now(); + summary.inner_mut().observe(i as f64); + if i % PRINT_EVERY == 0 { + println!("Time taken: {:?}", start.elapsed()); + } + } + + let result = summary.snapshot(); + assert_eq!( + result.sample_count(), + MEASUREMENTS as u64, + "Should have all measurements present in the collection" + ); + } + + fn measure(summary: GenericSummary) { + for i in 0..MEASUREMENTS { + let start = std::time::Instant::now(); + summary.observe(i as f64); + if i % PRINT_EVERY == 0 { + println!("Time taken: {:?}", start.elapsed()); + } + } + + let result = summary.snapshot(); + assert_eq!( + result.sample_count(), + MEASUREMENTS as u64, + "Should have all measurements present in the collection" + ); + } + + #[test] + fn with_simple_summary() { + let opts = SimpleSummaryOpts::default(); + let opts = + SummaryOpts::new("test_summary", "simple", opts).quantiles(DEFAULT_QUANTILES.to_vec()); + let summary = GenericSummary::::new::<&str>(&opts, &[]).unwrap(); + + measure_mut(summary); + } + + #[test] + fn with_batched_simple_summary() { + let opts = SimpleSummaryOpts::default(); + let opts = BatchOpts::from_inner(opts); + let opts = SummaryOpts::new("test_summary", "batched_simple", opts) + .quantiles(DEFAULT_QUANTILES.to_vec()); + let summary = + GenericSummary::>::new::<&str>(&opts, &[]).unwrap(); + + measure(summary); + } + + #[test] + fn with_rolling_summary() { + let opts = RollingSummaryOpts::default().with_quantiles(DEFAULT_QUANTILES); + let opts = + SummaryOpts::new("test_summary", "rolling", opts).quantiles(DEFAULT_QUANTILES.to_vec()); + let summary = GenericSummary::::new::<&str>(&opts, &[]).unwrap(); + + measure_mut(summary); + } + + #[test] + fn with_batched_rolling_summary() { + let opts = RollingSummaryOpts::default().with_quantiles(DEFAULT_QUANTILES); + let opts = BatchOpts::from_inner(opts); + let opts = SummaryOpts::new("test_summary", "batched_rolling", opts) + .quantiles(DEFAULT_QUANTILES.to_vec()); + let summary = + GenericSummary::>::new::<&str>(&opts, &[]).unwrap(); + + measure(summary); + } +} diff --git a/prometric/src/summary/mod.rs b/prometric/src/summary/mod.rs new file mode 100644 index 0000000..08315f1 --- /dev/null +++ b/prometric/src/summary/mod.rs @@ -0,0 +1,167 @@ +use std::collections::HashMap; + +use prometheus::core::MetricVec; + +pub mod traits; +use traits::{NonConcurrentSummaryProvider, SummaryMetric, SummaryProvider}; + +mod generic; +use generic::SummaryVecBuilder; +pub use generic::{DEFAULT_QUANTILES, SummaryOpts}; + +pub mod simple; + +pub mod rolling; +use rolling::{RollingSummary, RollingSummaryOpts}; + +pub mod batching; +use batching::{BatchOpts, BatchedSummary}; + +pub type DefaultSummaryProvider = BatchedSummary; + +type SummaryVec = MetricVec>; + +/// A Summary metric. +#[derive(Clone, Debug)] +pub struct Summary { + inner: SummaryVec, +} + +impl Summary { + // NOTE: Unlike other items like `HistogramVec`, this can't exist on `MetricVec` directly + // as we are not allowed to have inherent impls on foreign types + fn new_summary_vec( + opts: SummaryOpts, + label_names: &[&str], + ) -> prometheus::Result> { + let variable_names = label_names.iter().map(|s| (*s).to_owned()).collect(); + let opts = opts.variable_labels(variable_names); + let metric_vec = MetricVec::create( + prometheus::proto::MetricType::SUMMARY, + SummaryVecBuilder::::new(), + opts, + )?; + + Ok(metric_vec as SummaryVec) + } +} + +impl Summary { + pub fn new( + registry: &prometheus::Registry, + name: &str, + help: &str, + labels: &[&str], + const_labels: HashMap, + quantiles: Option>, + ) -> Self { + let quantiles = quantiles.unwrap_or(generic::DEFAULT_QUANTILES.to_vec()); + + let opts = RollingSummaryOpts::default().with_quantiles(&quantiles); + let opts = BatchOpts::from_inner(opts); + let opts = + SummaryOpts::new(name, help, opts).const_labels(const_labels).quantiles(quantiles); + + let metric = Self::new_summary_vec(opts, labels).unwrap(); + + let boxed = Box::new(metric.clone()); + if let Err(e) = registry.register(boxed.clone()) { + let id = format!("{}, Labels: {}", name, labels.join(", "),); + // If the metric is already registered, overwrite it. + if matches!(e, prometheus::Error::AlreadyReg) { + registry + .unregister(boxed.clone()) + .unwrap_or_else(|_| panic!("Failed to unregister metric {id}")); + + registry + .register(boxed) + .unwrap_or_else(|_| panic!("Failed to overwrite metric {id}")); + } else { + panic!("Failed to register metric {id}"); + } + } + + Self { inner: metric } + } +} + +impl Summary +where + S: SummaryProvider

::Summary> + SummaryMetric, +{ + pub fn observe(&self, labels: &[&str], value: f64) { + self.inner.with_label_values(labels).observe(value); + } + + pub fn snapshot(&self, labels: &[&str]) -> ::Summary { + NonConcurrentSummaryProvider::snapshot(&**self.inner.with_label_values(labels)) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::traits::Summary as _; + + use super::*; + + const MEASUREMENTS: usize = 50_000; + const PRINT_EVERY: usize = 100; + + #[test] + fn smoke() { + let registry = prometheus::default_registry(); + let summary = + Summary::new(registry, "smoke", "Smoke test summary", &[], Default::default(), None); + + for i in 0..MEASUREMENTS { + let start = std::time::Instant::now(); + summary.observe(&[], i as f64); + if i % PRINT_EVERY == 0 { + println!("Time taken: {:?}", start.elapsed()); + } + } + + let result = summary.snapshot(&[]); + assert_eq!( + result.sample_count(), + MEASUREMENTS as u64, + "Should have all measurements present in the collection" + ); + } + + #[test] + fn concurrent_smoke() { + let registry = prometheus::default_registry(); + let summary = + Summary::new(registry, "smoke", "Smoke test summary", &[], Default::default(), None); + let summary = Arc::new(summary); + + let tasks = 8; + + let mut handles = Vec::with_capacity(tasks); + for _ in 0..tasks { + let summary = summary.clone(); + + let task = std::thread::spawn(move || { + for i in 0..MEASUREMENTS { + let start = std::time::Instant::now(); + summary.observe(&[], i as f64); + if i % PRINT_EVERY == 0 { + println!("Time taken: {:?}", start.elapsed()); + } + } + }); + handles.push(task); + } + handles.into_iter().for_each(|h| h.join().unwrap()); + + let result = summary.snapshot(&[]); + assert_eq!( + result.sample_count(), + (MEASUREMENTS * tasks) as u64, + "Should have all measurements present in the collection" + ); + } +} diff --git a/prometric/src/summary/rolling.rs b/prometric/src/summary/rolling.rs new file mode 100644 index 0000000..f35d02f --- /dev/null +++ b/prometric/src/summary/rolling.rs @@ -0,0 +1,126 @@ +//! Rolling Summary implementation +//! +//! Uses [`metrics_exporter_prometheus::Distribution`] for the underlying representation + +use std::{num::NonZeroU32, time::Duration}; + +use metrics_util::Quantile; +use quanta::Instant; + +use crate::summary::{ + DEFAULT_QUANTILES, + simple::SimpleSummary, + traits::{NonConcurrentSummaryProvider, Summary}, +}; + +// from metrics_exporter_prometheus::Distribution +pub const DEFAULT_SUMMARY_BUCKET_DURATION: Duration = Duration::from_secs(20); +pub const DEFAULT_SUMMARY_BUCKET_COUNT: NonZeroU32 = NonZeroU32::new(3).unwrap(); + +/// A Rolling summary implementation, backed by [`metrics_exporter_prometheus::Distribution`] +/// +/// This is a summry which includes a "rolling" algorithm, to exclude measurements past the +/// configured `duration` (in [`RollingSummaryOpts`]). As the RollingSummary stores measurements in +/// buckets, the measurement expiry is on a per-bucket basis, meaning that old values might still be +/// used if the bucket they belong in hasn't expired yet. +/// +/// Quantiles are computed using [`SimpleSummary`], which will contain the non-expired measurements +pub type RollingSummary = metrics_exporter_prometheus::Distribution; + +/// A [`crate::summary::traits::Summary`] snapshot implementation for [`RollingSummary`] +/// +/// Will return the total count and total sum, but use the resulting [`SimpleSummary`] from +/// [`RollingSummary`] for the quantile computation, which only uses non-expired values +/// +/// # References +/// [`RollingSummary`] is usually rendered with the total sum and count, but using the active values +/// for quantile computation, as seen in [`metrics_exporter_prometheus`](https://github.com/metrics-rs/metrics/blob/main/metrics-exporter-prometheus/src/recorder.rs#L183). +pub struct RollingSummarySnapshot { + count: usize, + inner: SimpleSummary, +} + +impl Summary for RollingSummarySnapshot { + fn sample_sum(&self) -> f64 { + self.inner.sample_sum() + } + + fn sample_count(&self) -> u64 { + self.count as u64 + } + + fn quantile(&self, val: f64) -> Option { + self.inner.quantile(val) + } +} + +/// Configuration for the Summary +/// +/// See [`RollingSummary::new`] for documentation on the various options +#[derive(Clone)] +pub struct RollingSummaryOpts { + pub quantiles: Vec, + pub duration: Duration, + pub max_buckets_count: NonZeroU32, +} + +impl RollingSummaryOpts { + pub fn with_quantiles(self, quantiles: &[f64]) -> Self { + Self { + quantiles: quantiles.iter().map(|quantile| Quantile::new(*quantile)).collect(), + ..self + } + } +} + +impl Default for RollingSummaryOpts { + fn default() -> Self { + Self { + quantiles: DEFAULT_QUANTILES.iter().map(|quantile| Quantile::new(*quantile)).collect(), + duration: DEFAULT_SUMMARY_BUCKET_DURATION, + max_buckets_count: DEFAULT_SUMMARY_BUCKET_COUNT, + } + } +} + +impl NonConcurrentSummaryProvider for RollingSummary { + type Opts = RollingSummaryOpts; + type Summary = RollingSummarySnapshot; + + fn new_provider(opts: &Self::Opts) -> Self { + let distribution = metrics_exporter_prometheus::DistributionBuilder::new( + opts.quantiles.clone(), + Some(opts.duration), + None, + Some(opts.max_buckets_count), + None, + ) + .get_distribution("name not relevant"); + + assert!( + matches!(distribution, RollingSummary::Summary(..)), + "DistributionBuilder didn't build a Summary!" + ); + + distribution + } + + fn observe(&mut self, sample: f64) { + // TODO: Determine if we want to also receive the measurement instant + let now = Instant::now(); + self.record_samples(&[(sample, now)]); + } + + fn snapshot(&self) -> RollingSummarySnapshot { + match self { + RollingSummary::Summary(summary, _, sum) => { + let count = summary.count(); + let snapshot = summary.snapshot(Instant::now()); + let inner = SimpleSummary { inner: snapshot, sum: *sum }; + + RollingSummarySnapshot { inner, count } + } + _ => unreachable!("Distribution forced to be a Summary"), + } + } +} diff --git a/prometric/src/summary/simple.rs b/prometric/src/summary/simple.rs new file mode 100644 index 0000000..7ff5892 --- /dev/null +++ b/prometric/src/summary/simple.rs @@ -0,0 +1,66 @@ +//! Simple summary implementation +//! +//! Uses [`metrics_util::storage::Summary`] for the undelying representation + +use metrics_util::storage::Summary as Inner; + +use crate::summary::traits::{NonConcurrentSummaryProvider, Summary}; + +/// A simple Summary metric implementation +/// +/// This Summary uses [`Inner`] for the underlying computation, which stores the measurements and +/// provides arbitrary quantiles over the observed measurements +#[derive(Debug, Clone)] +pub struct SimpleSummary { + pub(crate) inner: Inner, + // We track the sum separately because [`Inner`] doesn't expose it + pub(crate) sum: f64, +} + +/// Configuration for the Summary +/// +/// See [`metrics_util::storage::Summary::new`] for documentation on the various options +#[derive(Clone)] +pub struct SimpleSummaryOpts { + pub alpha: f64, + pub max_buckets: u32, + pub min_value: f64, +} + +impl Default for SimpleSummaryOpts { + fn default() -> Self { + // takes from Inner::with_defaults + Self { alpha: 0.0001, max_buckets: 32_768, min_value: 1.0e-9 } + } +} + +impl NonConcurrentSummaryProvider for SimpleSummary { + type Opts = SimpleSummaryOpts; + type Summary = Self; + + fn new_provider(opts: &Self::Opts) -> Self { + Self { inner: Inner::new(opts.alpha, opts.max_buckets, opts.min_value), sum: 0. } + } + + fn observe(&mut self, val: f64) { + self.inner.add(val); + } + + fn snapshot(&self) -> Self::Summary { + self.clone() + } +} + +impl Summary for SimpleSummary { + fn sample_sum(&self) -> f64 { + self.sum + } + + fn sample_count(&self) -> u64 { + self.inner.count() as u64 + } + + fn quantile(&self, quantile: f64) -> Option { + self.inner.quantile(quantile) + } +} diff --git a/prometric/src/summary/traits.rs b/prometric/src/summary/traits.rs new file mode 100644 index 0000000..8a5ad3e --- /dev/null +++ b/prometric/src/summary/traits.rs @@ -0,0 +1,65 @@ +/// Abstracts over the representation of the Summary data +pub trait Summary { + /// Computes the sum of all the samples in the summary + fn sample_sum(&self) -> f64; + + /// Returns the number of samples in the summary + fn sample_count(&self) -> u64; + + /// Attempt to compute the value for the given quantile + fn quantile(&self, _: f64) -> Option; +} + +/// Abstracts over the metric summary logic user to compute the given quantile results +pub trait SummaryProvider { + type Opts: Clone + Send + Sync; + type Summary: Summary; + + /// Create a new instance of the given provider + fn new_provider(opts: &Self::Opts) -> Self; + + /// Add a new data point to the summary's collection + fn observe(&self, _: f64); + + /// Return the current summary computed over the observations + fn snapshot(&self) -> Self::Summary; +} + +/// Abstracts over the metric summary logic user to compute the given quantile results +/// +/// Differing from [`SummaryProvider`] by the `observe` `&mut self` requirement. +pub trait NonConcurrentSummaryProvider { + type Opts: Clone + Send + Sync; + type Summary: Summary; + + /// Create a new instance of the given provider + fn new_provider(opts: &Self::Opts) -> Self; + + /// Add a new data point to the summary's collection + fn observe(&mut self, _: f64); + + /// Return the current summary computed over the observations + fn snapshot(&self) -> Self::Summary; +} + +impl NonConcurrentSummaryProvider for T { + type Opts = T::Opts; + type Summary = T::Summary; + + fn new_provider(opts: &Self::Opts) -> Self { + ::new_provider(opts) + } + + fn observe(&mut self, val: f64) { + SummaryProvider::observe(self, val) + } + + fn snapshot(&self) -> Self::Summary { + SummaryProvider::snapshot(self) + } +} + +/// Marker trait (or alias) for a [`Summary`] which can be used by +/// [`crate::summary::generic::GenericSummary`] to implement [`prometheus::Metric`] +pub trait SummaryMetric: NonConcurrentSummaryProvider + Send + Sync + Clone {} +impl SummaryMetric for T {}