diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..278bffd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2484 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "datadog-protos" +version = "0.1.0" +source = "git+https://github.com/DataDog/saluki/?rev=c89b58e5784b985819baf11f13f7d35876741222#c89b58e5784b985819baf11f13f7d35876741222" +dependencies = [ + "bytes", + "prost", + "protobuf", + "protobuf-codegen", + "tonic", + "tonic-build", +] + +[[package]] +name = "ddsketch-agent" +version = "0.1.0" +source = "git+https://github.com/DataDog/saluki/?rev=c89b58e5784b985819baf11f13f7d35876741222#c89b58e5784b985819baf11f13f7d35876741222" +dependencies = [ + "datadog-protos", + "float-cmp", + "ordered-float", + "smallvec", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dogstatsd" +version = "0.0.0" +dependencies = [ + "datadog-protos", + "ddsketch-agent", + "derive_more", + "fnv", + "hashbrown 0.14.5", + "mockito", + "proptest", + "protobuf", + "regex", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing", + "tracing-test", + "ustr", + "zstd", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mockito" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48" +dependencies = [ + "assert-json-diff", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.0", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "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 = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[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 = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] + +[[package]] +name = "prettyplease" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "lazy_static", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_xorshift", + "regex-syntax 0.8.5", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "rand 0.9.0", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.23", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[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 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[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.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "prost", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "ustr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b19e258aa08450f93369cf56dd78063586adf19e92a75b338a800f799a0208" +dependencies = [ + "ahash", + "byteorder", + "lazy_static", + "parking_lot", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.14+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/crates/dogstatsd/Cargo.toml b/crates/dogstatsd/Cargo.toml new file mode 100644 index 0000000..9dadb38 --- /dev/null +++ b/crates/dogstatsd/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "dogstatsd" +rust-version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +bench = false + +[dependencies] +datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "c89b58e5784b985819baf11f13f7d35876741222" } +ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "c89b58e5784b985819baf11f13f7d35876741222" } +derive_more = { version = "1.0.0", features = ["display", "into"] } +hashbrown = { version = "0.14.3", default-features = false, features = ["inline-more"] } +protobuf = { version = "3.5.0", default-features = false } +ustr = { version = "1.0.0", default-features = false } +fnv = { version = "1.0.7", default-features = false } +reqwest = { version = "0.12.4", features = ["json", "http2", "rustls-tls"], default-features = false } +serde = { version = "1.0.197", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.116", default-features = false, features = ["alloc"] } +thiserror = { version = "1.0.58", default-features = false } +tokio = { version = "1.37.0", default-features = false, features = ["macros", "rt-multi-thread"] } +tokio-util = { version = "0.7.11", default-features = false } +tracing = { version = "0.1.40", default-features = false } +regex = { version = "1.10.6", default-features = false } +zstd = { version = "0.13.3", default-features = false } + +[dev-dependencies] +mockito = { version = "1.5.0", default-features = false } +proptest = "1.4.0" +tracing-test = { version = "0.2.5", default-features = false } diff --git a/crates/dogstatsd/README.md b/crates/dogstatsd/README.md new file mode 100644 index 0000000..dd0c3e5 --- /dev/null +++ b/crates/dogstatsd/README.md @@ -0,0 +1,14 @@ +# DogStatsD + +Provides a DogStatsD implementation which uses [Saluki](https://github.com/DataDog/saluki) for distribution metrics. + +## Status +This project is in beta and possible frequent changes should be expected. It's primary purpose is for Serverless to send metrics from AWS Lambda Functions, Azure Functions, and Azure Spring Apps. It is still considered unstable for general purposes. + +- No UDS support +- Uses `ustr`, so prone to memory leaks +- Arbitrary constraints in [src/constants.rs](src/constants.rs) + +## Additional Notes + +Upstreamed from [Bottlecap](https://github.com/DataDog/datadog-lambda-extension/tree/main/bottlecap) diff --git a/crates/dogstatsd/src/aggregator.rs b/crates/dogstatsd/src/aggregator.rs new file mode 100644 index 0000000..f31a3b0 --- /dev/null +++ b/crates/dogstatsd/src/aggregator.rs @@ -0,0 +1,755 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! The aggregation of metrics. + +use crate::constants; +use crate::datadog::{self, Metric as MetricToShip, Series}; +use crate::errors; +use crate::metric::{self, Metric, MetricValue, SortedTags}; + +use datadog_protos::metrics::{Dogsketch, Sketch, SketchPayload}; +use ddsketch_agent::DDSketch; +use hashbrown::hash_table; +use protobuf::Message; +use tracing::{error, warn}; +use ustr::Ustr; + +impl MetricValue { + fn aggregate(&mut self, metric: Metric) { + // safe because we know there's at least one value when we parse + // TODO aggregating different types should return error + match self { + MetricValue::Count(count) => *count += metric.value.get_value().unwrap_or_default(), + MetricValue::Gauge(gauge) => *gauge = metric.value.get_value().unwrap_or_default(), + MetricValue::Distribution(distribution) => { + if let Some(value) = metric.value.get_sketch() { + distribution.merge(value); + } + } + } + } + + pub fn get_value(&self) -> Option { + match self { + MetricValue::Count(count) => Some(*count), + MetricValue::Gauge(gauge) => Some(*gauge), + MetricValue::Distribution(_) => None, + } + } + + pub fn get_sketch(&self) -> Option<&DDSketch> { + match self { + MetricValue::Distribution(distribution) => Some(distribution), + _ => None, + } + } +} + +#[derive(Clone)] +// NOTE by construction we know that intervals and contexts do not explore the +// full space of usize but the type system limits how we can express this today. +pub struct Aggregator { + tags: SortedTags, + map: hash_table::HashTable, + max_batch_entries_single_metric: usize, + max_batch_bytes_single_metric: u64, + max_batch_entries_sketch_metric: usize, + max_batch_bytes_sketch_metric: u64, + max_context: usize, +} + +impl Aggregator { + /// Create a new instance of `Aggregator` + /// + /// # Errors + /// + /// Will fail at runtime if the type `INTERVALS` and `CONTEXTS` exceed their + /// counterparts in `constants`. This would be better as a compile-time + /// issue, although leaving this open allows for runtime configuration. + #[allow(clippy::cast_precision_loss)] + pub fn new(tags: SortedTags, max_context: usize) -> Result { + if max_context > constants::MAX_CONTEXTS { + return Err(errors::Creation::Contexts); + } + Ok(Self { + tags, + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: constants::MAX_ENTRIES_SINGLE_METRIC, + max_batch_bytes_single_metric: constants::MAX_SIZE_BYTES_SINGLE_METRIC, + max_batch_entries_sketch_metric: constants::MAX_ENTRIES_SKETCH_METRIC, + max_batch_bytes_sketch_metric: constants::MAX_SIZE_SKETCH_METRIC, + max_context, + }) + } + + /// Insert a `Metric` into the `Aggregator` at the current interval + /// + /// # Errors + /// + /// Function will return overflow error if more than + /// `min(constants::MAX_CONTEXTS, CONTEXTS)` is exceeded. + pub fn insert(&mut self, metric: Metric) -> Result<(), errors::Insert> { + let id = metric::id(metric.name, &metric.tags, metric.timestamp); + let len = self.map.len(); + + match self.map.entry( + id, + |m| m.id == id, + |m| metric::id(m.name, &m.tags, m.timestamp), + ) { + hash_table::Entry::Vacant(entry) => { + if len >= self.max_context { + return Err(errors::Insert::Overflow); + } + entry.insert(metric); + } + hash_table::Entry::Occupied(mut entry) => { + entry.get_mut().value.aggregate(metric); + } + } + Ok(()) + } + + pub fn clear(&mut self) { + self.map.clear(); + } + + #[must_use] + pub fn distributions_to_protobuf(&self) -> SketchPayload { + let mut sketch_payload = SketchPayload::new(); + + sketch_payload.sketches = self + .map + .iter() + .filter_map(|entry| match entry.value { + MetricValue::Distribution(_) => build_sketch(entry, self.tags.clone()), + _ => None, + }) + .collect(); + sketch_payload + } + + #[must_use] + pub fn consume_distributions(&mut self) -> Vec { + let mut batched_payloads = Vec::new(); + let mut sketch_payload = SketchPayload::new(); + let mut this_batch_size = 0u64; + for sketch in self + .map + .extract_if(|entry| { + if let MetricValue::Distribution(_) = entry.value { + return true; + } + false + }) + .filter_map(|entry| build_sketch(&entry, self.tags.clone())) + { + let next_chunk_size = sketch.compute_size(); + + if (sketch_payload.sketches.len() >= self.max_batch_entries_sketch_metric) + || (this_batch_size + next_chunk_size >= self.max_batch_bytes_sketch_metric) + { + if this_batch_size == 0 { + warn!("Only one distribution exceeds max batch size, adding it anyway: {:?} with {}", sketch.metric, next_chunk_size); + } else { + batched_payloads.push(sketch_payload); + sketch_payload = SketchPayload::new(); + this_batch_size = 0u64; + } + } + this_batch_size += next_chunk_size; + sketch_payload.sketches.push(sketch); + } + if !sketch_payload.sketches.is_empty() { + batched_payloads.push(sketch_payload); + } + batched_payloads + } + + #[must_use] + pub fn to_series(&self) -> Series { + let mut series_payload = Series { + series: Vec::with_capacity(1_024), + }; + + self.map + .iter() + .filter_map(|entry| match entry.value { + MetricValue::Distribution(_) => None, + _ => build_metric(entry, self.tags.clone()), + }) + .for_each(|metric| series_payload.series.push(metric)); + series_payload + } + + #[must_use] + pub fn consume_metrics(&mut self) -> Vec { + let mut batched_payloads = Vec::new(); + let mut series_payload = Series { + series: Vec::with_capacity(1_024), + }; + let mut this_batch_size = 0u64; + for metric in self + .map + .extract_if(|entry| { + if let MetricValue::Distribution(_) = entry.value { + return false; + } + true + }) + .filter_map(|entry| build_metric(&entry, self.tags.clone())) + { + // TODO serialization is made twice for each point. If we return a Vec we can avoid + // that + let serialized_metric_size = match serde_json::to_vec(&metric) { + Ok(serialized_metric) => serialized_metric.len() as u64, + Err(e) => { + error!("failed to serialize metric: {:?}", e); + 0u64 + } + }; + + if serialized_metric_size > 0 { + if (series_payload.series.len() >= self.max_batch_entries_single_metric) + || (this_batch_size + serialized_metric_size + >= self.max_batch_bytes_single_metric) + { + if this_batch_size == 0 { + warn!("Only one metric exceeds max batch size, adding it anyway: {:?} with {}", metric.metric, serialized_metric_size); + } else { + batched_payloads.push(series_payload); + series_payload = Series { + series: Vec::with_capacity(1_024), + }; + this_batch_size = 0u64; + } + } + series_payload.series.push(metric); + this_batch_size += serialized_metric_size; + } + } + + if !series_payload.series.is_empty() { + batched_payloads.push(series_payload); + } + batched_payloads + } + + pub fn get_entry_by_id( + &self, + name: Ustr, + tags: &Option, + timestamp: i64, + ) -> Option<&Metric> { + let id = metric::id(name, tags, timestamp); + self.map.find(id, |m| m.id == id) + } +} + +fn build_sketch(entry: &Metric, mut base_tag_vec: SortedTags) -> Option { + let sketch = entry.value.get_sketch()?; + let mut dogsketch = Dogsketch::default(); + sketch.merge_to_dogsketch(&mut dogsketch); + // TODO(Astuyve) allow users to specify timestamp + dogsketch.set_ts(entry.timestamp); + let mut sketch = Sketch::default(); + sketch.set_dogsketches(vec![dogsketch]); + let name = entry.name.to_string(); + sketch.set_metric(name.clone().into()); + if let Some(tags) = entry.tags.clone() { + base_tag_vec.extend(&tags); + } + sketch.set_tags(base_tag_vec.to_chars()); + Some(sketch) +} + +fn build_metric(entry: &Metric, mut base_tag_vec: SortedTags) -> Option { + let resources; + if let Some(tags) = entry.tags.clone() { + resources = tags.to_resources(); + } else { + resources = Vec::new(); + } + let kind = match entry.value { + MetricValue::Count(_) => datadog::DdMetricKind::Count, + MetricValue::Gauge(_) => datadog::DdMetricKind::Gauge, + MetricValue::Distribution(_) => unreachable!(), + }; + let point = datadog::Point { + value: entry.value.get_value()?, + // TODO(astuyve) allow user to specify timestamp + timestamp: entry.timestamp as u64, + }; + + if let Some(tags) = entry.tags.clone() { + base_tag_vec.extend(&tags); + } + + Some(MetricToShip { + metric: entry.name.as_str(), + resources, + kind, + points: [point; 1], + tags: base_tag_vec.to_strings(), + }) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +pub mod tests { + use crate::aggregator::Aggregator; + use crate::metric; + use crate::metric::{parse, SortedTags, EMPTY_TAGS}; + use datadog_protos::metrics::SketchPayload; + use hashbrown::hash_table; + use protobuf::Message; + use std::sync::Mutex; + + const PRECISION: f64 = 0.000_000_01; + + const SINGLE_METRIC_SIZE: usize = 193; // taken from the test, size of a serialized metric with one tag and 1 digit counter value + const SINGLE_DISTRIBUTION_SIZE: u64 = 140; + const DEFAULT_TAGS: &str = + "dd_extension_version:63-next,architecture:x86_64,_dd.compute_stats:1"; + + pub fn assert_value( + aggregator_mutex: &Mutex, + metric_id: &str, + value: f64, + tags: &str, + timestamp: i64, + ) { + let aggregator = aggregator_mutex.lock().unwrap(); + if let Some(e) = aggregator.get_entry_by_id( + metric_id.into(), + &Some(SortedTags::parse(tags).unwrap()), + timestamp, + ) { + let metric = e.value.get_value().unwrap(); + assert!((metric - value).abs() < PRECISION); + } else { + panic!("{}", format!("{metric_id} not found")); + } + } + + pub fn assert_sketch( + aggregator_mutex: &Mutex, + metric_id: &str, + value: f64, + timestamp: i64, + ) { + let aggregator = aggregator_mutex.lock().unwrap(); + if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), &None, timestamp) { + let metric = e.value.get_sketch().unwrap(); + assert!((metric.max().unwrap() - value).abs() < PRECISION); + assert!((metric.min().unwrap() - value).abs() < PRECISION); + assert!((metric.sum().unwrap() - value).abs() < PRECISION); + assert!((metric.avg().unwrap() - value).abs() < PRECISION); + } else { + panic!("{}", format!("{metric_id} not found")); + } + } + + #[test] + #[cfg_attr(miri, ignore)] + fn insertion() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); + + let metric1 = parse("test:1|c|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|c|#k:v").expect("metric parse failed"); + + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); + + // Both unique contexts get one slot. + assert_eq!(aggregator.map.len(), 2); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn distribution_insertion() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); + + let metric1 = parse("test:1|d|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|d|#k:v").expect("metric parse failed"); + + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); + + // Both unique contexts get one slot. + assert_eq!(aggregator.map.len(), 2); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn overflow() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); + let mut now = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + now = (now / 10) * 10; + let metric1 = parse("test:1|c|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|c|#k:v").expect("metric parse failed"); + let metric3 = parse("bar:1|c|#k:v").expect("metric parse failed"); + + let id1 = metric::id(metric1.name, &metric1.tags, now); + let id2 = metric::id(metric2.name, &metric2.tags, now); + let id3 = metric::id(metric3.name, &metric3.tags, now); + + assert_ne!(id1, id2); + assert_ne!(id1, id3); + assert_ne!(id2, id3); + + assert!(aggregator.insert(metric1).is_ok()); + assert_eq!(aggregator.map.len(), 1); + + assert!(aggregator.insert(metric2.clone()).is_ok()); + assert!(aggregator.insert(metric2.clone()).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); + assert_eq!(aggregator.map.len(), 2); + + assert!(aggregator.insert(metric3).is_err()); + assert_eq!(aggregator.map.len(), 2); + } + + #[test] + #[allow(clippy::float_cmp)] + #[cfg_attr(miri, ignore)] + fn clear() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); + let mut now = 1656581409; + now = (now / 10) * 10; + let metric1 = parse("test:3|c|#k1:v1|T1656581409").expect("metric parse failed"); + let metric2 = parse("foo:5|c|#k2:v2|T1656581409").expect("metric parse failed"); + + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); + + assert_eq!(aggregator.map.len(), 2); + if let Some(v) = aggregator.get_entry_by_id( + "foo".into(), + &Some(SortedTags::parse("k2:v2").unwrap()), + now, + ) { + assert_eq!(v.value.get_value().unwrap(), 5f64); + } else { + panic!("failed to get value by id"); + } + + if let Some(v) = aggregator.get_entry_by_id( + "test".into(), + &Some(SortedTags::parse("k1:v1").unwrap()), + now, + ) { + assert_eq!(v.value.get_value().unwrap(), 3f64); + } else { + panic!("failed to get value by id"); + } + + aggregator.clear(); + assert_eq!(aggregator.map.len(), 0); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn to_series() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); + + let metric1 = parse("test:1|c|#k1:v1,k2:v2").expect("metric parse failed"); + let metric2 = parse("foo:1|c|#k:v").expect("metric parse failed"); + let metric3 = parse("bar:1|c|#k:v").expect("metric parse failed"); + + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); + + assert_eq!(aggregator.map.len(), 2); + assert_eq!(aggregator.to_series().len(), 2); + // to_series should not mutate the state + assert_eq!(aggregator.map.len(), 2); + assert_eq!(aggregator.to_series().len(), 2); + assert_eq!(aggregator.map.len(), 2); + + assert!(aggregator.insert(metric3).is_err()); + assert_eq!(aggregator.to_series().len(), 2); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn distributions_to_protobuf() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); + + let metric1 = parse("test:1|d|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|d|#k:v").expect("metric parse failed"); + + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); + + assert_eq!(aggregator.map.len(), 2); + assert_eq!(aggregator.distributions_to_protobuf().sketches().len(), 2); + assert_eq!(aggregator.map.len(), 2); + assert_eq!(aggregator.distributions_to_protobuf().sketches().len(), 2); + assert_eq!(aggregator.map.len(), 2); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn consume_distributions_ignore_single_metrics() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); + + assert!(aggregator + .insert(parse("test1:1|d|#k:v".to_string().as_str()).expect("metric parse failed")) + .is_ok()); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 1); + + assert!(aggregator + .insert(parse("foo:1|c|#k:v").expect("metric parse failed")) + .is_ok()); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 1); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn consume_distributions_batch_entries() { + let max_batch = 5; + let tot = 12; + let mut aggregator = Aggregator { + tags: EMPTY_TAGS, + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: 1_000, + max_batch_entries_sketch_metric: max_batch, + max_batch_bytes_sketch_metric: 1_500, + max_context: 1_000, + }; + + add_metrics(tot, &mut aggregator, "d".to_string()); + let batched = aggregator.consume_distributions(); + assert_eq!(aggregator.consume_distributions().len(), 0); + + assert_eq!(batched.len(), 3); + assert_eq!(batched.first().unwrap().sketches.len(), max_batch); + assert_eq!(batched.get(1).unwrap().sketches.len(), max_batch); + assert_eq!(batched.get(2).unwrap().sketches.len(), tot - max_batch * 2); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn consume_distributions_batch_bytes() { + let expected_distribution_per_batch = 2; + let total_number_of_distributions = 5; + let max_bytes = SINGLE_DISTRIBUTION_SIZE * expected_distribution_per_batch as u64; + let mut aggregator = Aggregator { + tags: to_sorted_tags(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: 1_000, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: max_bytes, + max_context: 1_000, + }; + + add_metrics( + total_number_of_distributions, + &mut aggregator, + "d".to_string(), + ); + let batched = aggregator.consume_distributions(); + + assert_eq!( + batched.len(), + total_number_of_distributions / expected_distribution_per_batch + 1 + ); + assert_eq!( + batched.first().unwrap().compute_size(), + SINGLE_DISTRIBUTION_SIZE * expected_distribution_per_batch as u64 + ); + assert_eq!( + batched.get(1).unwrap().compute_size(), + SINGLE_DISTRIBUTION_SIZE * expected_distribution_per_batch as u64 + ); + assert_eq!( + batched.get(2).unwrap().compute_size(), + SINGLE_DISTRIBUTION_SIZE + ); + } + + fn to_sorted_tags() -> SortedTags { + SortedTags::parse(DEFAULT_TAGS).unwrap() + } + + #[test] + #[cfg_attr(miri, ignore)] + fn consume_distribution_one_element_bigger_than_max_size() { + let max_bytes = 1; + let tot = 5; + let mut aggregator = Aggregator { + tags: to_sorted_tags(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: 1_000, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: max_bytes, + max_context: 1_000, + }; + + add_metrics(tot, &mut aggregator, "d".to_string()); + let batched = aggregator.consume_distributions(); + + assert_eq!(batched.len(), tot); + for a_batch in batched { + assert_eq!(a_batch.compute_size(), SINGLE_DISTRIBUTION_SIZE); + } + } + + fn add_metrics(tot: usize, aggregator: &mut Aggregator, counter_or_distro: String) { + for i in 1..=tot { + assert!(aggregator + .insert( + parse(format!("test{i}:{i}|{counter_or_distro}|#k:v").as_str()) + .expect("metric parse failed") + ) + .is_ok()); + } + } + + #[test] + #[cfg_attr(miri, ignore)] + fn consume_series_ignore_distribution() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); + + assert_eq!(aggregator.consume_metrics().len(), 0); + + assert!(aggregator + .insert(parse("test1:1|c|#k:v".to_string().as_str()).expect("metric parse failed")) + .is_ok()); + assert_eq!(aggregator.consume_distributions().len(), 0); + assert_eq!(aggregator.consume_metrics().len(), 1); + assert_eq!(aggregator.consume_metrics().len(), 0); + + assert!(aggregator + .insert(parse("test1:1|c|#k:v".to_string().as_str()).expect("metric parse failed")) + .is_ok()); + assert!(aggregator + .insert(parse("foo:1|d|#k:v").expect("metric parse failed")) + .is_ok()); + assert_eq!(aggregator.consume_metrics().len(), 1); + assert_eq!(aggregator.consume_distributions().len(), 1); + assert_eq!(aggregator.consume_distributions().len(), 0); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn consume_series_batch_entries() { + let max_batch = 5; + let tot = 13; + let mut aggregator = Aggregator { + tags: EMPTY_TAGS, + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: max_batch, + max_batch_bytes_single_metric: 10_000, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: 1_500, + max_context: 1_000, + }; + + add_metrics(tot, &mut aggregator, "c".to_string()); + + let batched = aggregator.consume_metrics(); + assert_eq!(batched.len(), 3); + assert_eq!(batched.first().unwrap().series.len(), max_batch); + assert_eq!(batched.get(1).unwrap().series.len(), max_batch); + assert_eq!(batched.get(2).unwrap().series.len(), tot - max_batch * 2); + + assert_eq!(aggregator.consume_metrics().len(), 0); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn consume_metrics_batch_bytes() { + let expected_metrics_per_batch = 2; + let total_number_of_metrics = 5; + let two_metrics_size = 374; + let max_bytes = SINGLE_METRIC_SIZE * expected_metrics_per_batch + 13; + let mut aggregator = Aggregator { + tags: to_sorted_tags(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: max_bytes as u64, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: 1_000, + max_context: 1_000, + }; + + add_metrics(total_number_of_metrics, &mut aggregator, "c".to_string()); + let batched = aggregator.consume_metrics(); + + assert_eq!( + batched.len(), + total_number_of_metrics / expected_metrics_per_batch + 1 + ); + assert_eq!( + serde_json::to_vec(batched.first().unwrap()).unwrap().len(), + two_metrics_size + ); + assert_eq!( + serde_json::to_vec(batched.get(1).unwrap()).unwrap().len(), + two_metrics_size + ); + assert_eq!( + serde_json::to_vec(batched.get(2).unwrap()).unwrap().len(), + SINGLE_METRIC_SIZE + ); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn consume_series_one_element_bigger_than_max_size() { + let max_bytes = 1; + let tot = 5; + let mut aggregator = Aggregator { + tags: to_sorted_tags(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: max_bytes, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: 1_000, + max_context: 1_000, + }; + + add_metrics(tot, &mut aggregator, "c".to_string()); + let batched = aggregator.consume_metrics(); + + assert_eq!(batched.len(), tot); + for a_batch in batched { + assert_eq!( + serde_json::to_vec(&a_batch).unwrap().len(), + SINGLE_METRIC_SIZE + ); + } + } + + #[test] + #[cfg_attr(miri, ignore)] + fn distribution_serialized_deserialized() { + let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); + + add_metrics(10, &mut aggregator, "d".to_string()); + let distribution = aggregator.distributions_to_protobuf(); + assert_eq!(distribution.sketches().len(), 10); + + let serialized = distribution + .write_to_bytes() + .expect("Can't serialized proto"); + + let deserialized = + SketchPayload::parse_from_bytes(serialized.as_slice()).expect("failed to parse proto"); + + assert_eq!(deserialized.sketches().len(), 10); + assert_eq!(deserialized, distribution); + } +} diff --git a/crates/dogstatsd/src/constants.rs b/crates/dogstatsd/src/constants.rs new file mode 100644 index 0000000..adf2631 --- /dev/null +++ b/crates/dogstatsd/src/constants.rs @@ -0,0 +1,19 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +/// The maximum tags that a `Metric` may hold. +pub const MAX_TAGS: usize = 100; + +pub const CONTEXTS: usize = 10_240; + +pub static MAX_CONTEXTS: usize = 65_536; // 2**16, arbitrary + +const MB: u64 = 1_024 * 1_024; + +pub(crate) const MAX_ENTRIES_SINGLE_METRIC: usize = 1_000; + +pub(crate) const MAX_SIZE_BYTES_SINGLE_METRIC: u64 = 5 * MB; + +pub(crate) const MAX_ENTRIES_SKETCH_METRIC: usize = 1_000; + +pub(crate) const MAX_SIZE_SKETCH_METRIC: u64 = 62 * MB; diff --git a/crates/dogstatsd/src/datadog.rs b/crates/dogstatsd/src/datadog.rs new file mode 100644 index 0000000..4bbcd13 --- /dev/null +++ b/crates/dogstatsd/src/datadog.rs @@ -0,0 +1,438 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//!Types to serialize data into the Datadog API + +use crate::flusher::ShippingError; +use datadog_protos::metrics::SketchPayload; +use derive_more::{Display, Into}; +use protobuf::Message; +use regex::Regex; +use reqwest; +use reqwest::{Client, Response}; +use serde::{Serialize, Serializer}; +use serde_json; +use std::io::Write; +use std::sync::OnceLock; +use std::time::Duration; +use tracing::{debug, error}; +use zstd::stream::write::Encoder; + +// TODO: Move to the more ergonomic LazyLock when MSRV is 1.80 +static SITE_RE: OnceLock = OnceLock::new(); +fn get_site_re() -> &'static Regex { + #[allow(clippy::expect_used)] + SITE_RE.get_or_init(|| Regex::new(r"^[a-zA-Z0-9._:-]+$").expect("invalid regex")) +} +static URL_PREFIX_RE: OnceLock = OnceLock::new(); +fn get_url_prefix_re() -> &'static Regex { + #[allow(clippy::expect_used)] + URL_PREFIX_RE.get_or_init(|| Regex::new(r"^https?://[a-zA-Z0-9._:-]+$").expect("invalid regex")) +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] +pub struct Site(String); + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[error("Invalid site: {0}")] +pub struct SiteError(String); + +impl Site { + pub fn new(site: String) -> Result { + // Datadog sites are generally domain names. In particular, they shouldn't have any slashes + // in them. We expect this to be coming from a `DD_SITE` environment variable or the `site` + // config field. + if get_site_re().is_match(&site) { + Ok(Site(site)) + } else { + Err(SiteError(site)) + } + } +} + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[error("Invalid URL prefix: {0}")] +pub struct UrlPrefixError(String); + +fn validate_url_prefix(prefix: &str) -> Result<(), UrlPrefixError> { + if get_url_prefix_re().is_match(prefix) { + Ok(()) + } else { + Err(UrlPrefixError(prefix.to_owned())) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] +pub struct DdUrl(String); + +impl DdUrl { + pub fn new(prefix: String) -> Result { + validate_url_prefix(&prefix)?; + Ok(Self(prefix)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] +pub struct DdDdUrl(String); + +impl DdDdUrl { + pub fn new(prefix: String) -> Result { + validate_url_prefix(&prefix)?; + Ok(Self(prefix)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] +pub struct MetricsIntakeUrlPrefixOverride(String); + +impl MetricsIntakeUrlPrefixOverride { + pub fn maybe_new(dd_url: Option, dd_dd_url: Option) -> Option { + match (dd_url, dd_dd_url) { + (None, None) => None, + (_, Some(dd_dd_url)) => Some(Self(dd_dd_url.into())), + (Some(dd_url), None) => Some(Self(dd_url.into())), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display)] +pub struct MetricsIntakeUrlPrefix(String); + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[error("Missing intake URL configuration")] +pub struct MissingIntakeUrlError; + +impl MetricsIntakeUrlPrefix { + #[inline] + pub fn new( + site: Option, + overridden_prefix: Option, + ) -> Result { + match (site, overridden_prefix) { + (None, None) => Err(MissingIntakeUrlError), + (_, Some(prefix)) => Ok(Self::new_expect_validated(prefix.into())), + (Some(site), None) => Ok(Self::from_site(site)), + } + } + + #[inline] + fn new_expect_validated(validated_prefix: String) -> Self { + #[allow(clippy::expect_used)] + validate_url_prefix(&validated_prefix).expect("Invalid URL prefix"); + + Self(validated_prefix) + } + + #[inline] + fn from_site(site: Site) -> Self { + Self(format!("https://api.{site}")) + } +} + +/// Interface for the `DogStatsD` metrics intake API. +#[derive(Debug, Clone)] +pub struct DdApi { + api_key: String, + metrics_intake_url_prefix: MetricsIntakeUrlPrefix, + client: Option, + retry_strategy: RetryStrategy, +} + +impl DdApi { + #[must_use] + pub fn new( + api_key: String, + metrics_intake_url_prefix: MetricsIntakeUrlPrefix, + https_proxy: Option, + timeout: Duration, + retry_strategy: RetryStrategy, + ) -> Self { + let client = build_client(https_proxy, timeout) + .inspect_err(|e| { + error!("Unable to create client {:?}", e); + }) + .ok(); + DdApi { + api_key, + metrics_intake_url_prefix, + client, + retry_strategy, + } + } + + /// Ship a serialized series to the API, blocking + pub async fn ship_series(&self, series: &Series) -> Result { + let url = format!("{}/api/v2/series", &self.metrics_intake_url_prefix); + let safe_body = serde_json::to_vec(&series) + .map_err(|e| ShippingError::Payload(format!("Failed to serialize series: {e}")))?; + debug!("Sending body: {:?}", &series); + self.ship_data(url, safe_body, "application/json").await + } + + pub async fn ship_distributions( + &self, + sketches: &SketchPayload, + ) -> Result { + let url = format!("{}/api/beta/sketches", &self.metrics_intake_url_prefix); + let safe_body = sketches + .write_to_bytes() + .map_err(|e| ShippingError::Payload(format!("Failed to serialize series: {e}")))?; + debug!("Sending distributions: {:?}", &sketches); + self.ship_data(url, safe_body, "application/x-protobuf") + .await + // TODO maybe go to coded output stream if we incrementally + // add sketch payloads to the buffer + // something like this, but fix the utf-8 encoding issue + // { + // let mut output_stream = CodedOutputStream::vec(&mut buf); + // let _ = output_stream.write_tag(1, protobuf::rt::WireType::LengthDelimited); + // let _ = output_stream.write_message_no_tag(&sketches); + // TODO not working, has utf-8 encoding issue in dist-intake + //} + } + + async fn ship_data( + &self, + url: String, + body: Vec, + content_type: &str, + ) -> Result { + let client = &self + .client + .as_ref() + .ok_or_else(|| ShippingError::Destination(None, "No client".to_string()))?; + let start = std::time::Instant::now(); + + let result = (|| -> std::io::Result> { + let mut encoder = Encoder::new(Vec::new(), 6)?; + encoder.write_all(&body)?; + encoder.finish() + })(); + + let mut builder = client + .post(&url) + .header("DD-API-KEY", &self.api_key) + .header("Content-Type", content_type); + + builder = match result { + Ok(compressed) => builder.header("Content-Encoding", "zstd").body(compressed), + Err(err) => { + debug!("Sending uncompressed data, failed to compress: {err}"); + builder.body(body) + } + }; + + let resp = self.send_with_retry(builder).await; + + let elapsed = start.elapsed(); + debug!("Request to {} took {}ms", url, elapsed.as_millis()); + resp + } + + async fn send_with_retry( + &self, + builder: reqwest::RequestBuilder, + ) -> Result { + let mut attempts = 0; + loop { + attempts += 1; + let cloned_builder = match builder.try_clone() { + Some(b) => b, + None => { + return Err(ShippingError::Destination( + None, + "Failed to clone request".to_string(), + )); + } + }; + + let response = cloned_builder.send().await; + match response { + Ok(response) if response.status().is_success() => { + return Ok(response); + } + _ => {} + } + + match self.retry_strategy { + RetryStrategy::LinearBackoff(max_attempts, _) + | RetryStrategy::Immediate(max_attempts) + if attempts >= max_attempts => + { + let status = match response { + Ok(response) => Some(response.status()), + Err(err) => err.status(), + }; + // handle if status code missing like timeout + return Err(ShippingError::Destination( + status, + format!("Failed to send request after {} attempts", max_attempts) + .to_string(), + )); + } + RetryStrategy::LinearBackoff(_, delay) => { + tokio::time::sleep(Duration::from_millis(delay)).await; + } + _ => {} + } + } + } +} + +#[derive(Debug, Clone)] +pub enum RetryStrategy { + Immediate(u64), // attempts + LinearBackoff(u64, u64), // attempts, delay +} + +fn build_client(https_proxy: Option, timeout: Duration) -> Result { + let mut builder = Client::builder().timeout(timeout); + if let Some(proxy) = https_proxy { + builder = builder.proxy(reqwest::Proxy::https(proxy)?); + } + builder.build() +} + +#[derive(Debug, Serialize, Clone, Copy)] +/// A single point in time +pub(crate) struct Point { + /// The time at which the point exists + pub(crate) timestamp: u64, + /// The point's value + pub(crate) value: f64, +} + +#[derive(Debug, Serialize)] +/// A named resource +pub(crate) struct Resource { + /// The name of this resource + pub(crate) name: String, + #[serde(rename = "type")] + /// The kind of this resource + pub(crate) kind: String, +} + +#[derive(Debug, Clone, Copy)] +/// The kinds of metrics the Datadog API supports +pub(crate) enum DdMetricKind { + /// An accumulating sum + Count, + /// An instantaneous value + Gauge, +} + +impl Serialize for DdMetricKind { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + DdMetricKind::Count => serializer.serialize_u32(0), + DdMetricKind::Gauge => serializer.serialize_u32(1), + } + } +} + +#[derive(Debug, Serialize)] +#[allow(clippy::struct_field_names)] +/// A named collection of `Point` instances. +pub(crate) struct Metric { + /// The name of the point collection + pub(crate) metric: &'static str, + /// The collection of points + pub(crate) points: [Point; 1], + /// The resources associated with the points + pub(crate) resources: Vec, + #[serde(rename = "type")] + /// The kind of metric + pub(crate) kind: DdMetricKind, + pub(crate) tags: Vec, +} + +#[derive(Debug, Serialize)] +/// A collection of metrics as defined by the Datadog Metrics API. +// NOTE we have a number of `Vec` instances in this implementation that could +// otherwise be arrays, given that we have constants. Serializing to JSON would +// require us to avoid serializing None or Uninit values, so there's some custom +// work that's needed. For protobuf this more or less goes away. +pub struct Series { + /// The collection itself + pub(crate) series: Vec, +} + +impl Series { + #[cfg(test)] + pub(crate) fn len(&self) -> usize { + self.series.len() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn override_can_be_empty() { + assert_eq!(MetricsIntakeUrlPrefixOverride::maybe_new(None, None), None); + } + + #[test] + fn override_prefers_dd_dd_url() { + assert_eq!( + MetricsIntakeUrlPrefixOverride::maybe_new( + Some(DdUrl::new("http://a_dd_url".to_string()).unwrap()), + Some(DdDdUrl::new("https://a_dd_dd_url".to_string()).unwrap()) + ), + Some(MetricsIntakeUrlPrefixOverride( + "https://a_dd_dd_url".to_string() + )) + ); + } + + #[test] + fn override_will_take_dd_url() { + assert_eq!( + MetricsIntakeUrlPrefixOverride::maybe_new( + Some(DdUrl::new("http://a_dd_url".to_string()).unwrap()), + None + ), + Some(MetricsIntakeUrlPrefixOverride( + "http://a_dd_url".to_string() + )) + ); + } + + #[test] + fn test_intake_url_prefix_new_requires_something() { + assert_eq!( + MetricsIntakeUrlPrefix::new(None, None), + Err(MissingIntakeUrlError) + ); + } + + #[test] + fn test_intake_url_prefix_new_picks_the_override() { + assert_eq!( + MetricsIntakeUrlPrefix::new( + Some(Site::new("a_site".to_string()).unwrap()), + MetricsIntakeUrlPrefixOverride::maybe_new( + Some(DdUrl::new("http://a_dd_url".to_string()).unwrap()), + None + ), + ), + Ok(MetricsIntakeUrlPrefix::new_expect_validated( + "http://a_dd_url".to_string() + )) + ); + } + + #[test] + fn test_intake_url_prefix_new_picks_site_as_a_fallback() { + assert_eq!( + MetricsIntakeUrlPrefix::new(Some(Site::new("a_site".to_string()).unwrap()), None,), + Ok(MetricsIntakeUrlPrefix::new_expect_validated( + "https://api.a_site".to_string() + )) + ); + } +} diff --git a/crates/dogstatsd/src/dogstatsd.rs b/crates/dogstatsd/src/dogstatsd.rs new file mode 100644 index 0000000..ea8b3c8 --- /dev/null +++ b/crates/dogstatsd/src/dogstatsd.rs @@ -0,0 +1,274 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use std::net::SocketAddr; +use std::str::Split; +use std::sync::{Arc, Mutex}; + +use crate::aggregator::Aggregator; +use crate::errors::ParseError::UnsupportedType; +use crate::metric::{parse, Metric}; +use tracing::{debug, error}; + +pub struct DogStatsD { + cancel_token: tokio_util::sync::CancellationToken, + aggregator: Arc>, + buffer_reader: BufferReader, +} + +pub struct DogStatsDConfig { + pub host: String, + pub port: u16, +} + +enum BufferReader { + UdpSocketReader(tokio::net::UdpSocket), + #[allow(dead_code)] + MirrorReader(Vec, SocketAddr), +} + +impl BufferReader { + async fn read(&self) -> std::io::Result<(Vec, SocketAddr)> { + match self { + BufferReader::UdpSocketReader(socket) => { + // TODO(astuyve) this should be dynamic + // Max buffer size is configurable in Go Agent and the default is 8KB + // https://github.com/DataDog/datadog-agent/blob/85939a62b5580b2a15549f6936f257e61c5aa153/pkg/config/config_template.yaml#L2154-L2158 + let mut buf = [0; 8192]; + + #[allow(clippy::expect_used)] + let (amt, src) = socket + .recv_from(&mut buf) + .await + .expect("didn't receive data"); + Ok((buf[..amt].to_owned(), src)) + } + BufferReader::MirrorReader(data, socket) => Ok((data.clone(), *socket)), + } + } +} + +impl DogStatsD { + #[must_use] + pub async fn new( + config: &DogStatsDConfig, + aggregator: Arc>, + cancel_token: tokio_util::sync::CancellationToken, + ) -> DogStatsD { + let addr = format!("{}:{}", config.host, config.port); + + // TODO (UDS socket) + #[allow(clippy::expect_used)] + let socket = tokio::net::UdpSocket::bind(addr) + .await + .expect("couldn't bind to address"); + DogStatsD { + cancel_token, + aggregator, + buffer_reader: BufferReader::UdpSocketReader(socket), + } + } + + pub async fn spin(self) { + let mut spin_cancelled = false; + while !spin_cancelled { + self.consume_statsd().await; + spin_cancelled = self.cancel_token.is_cancelled(); + } + } + + async fn consume_statsd(&self) { + #[allow(clippy::expect_used)] + let (buf, src) = self + .buffer_reader + .read() + .await + .expect("didn't receive data"); + + #[allow(clippy::expect_used)] + let msgs = std::str::from_utf8(&buf).expect("couldn't parse as string"); + debug!("Received message: {} from {}", msgs, src); + let statsd_metric_strings = msgs.split('\n'); + self.insert_metrics(statsd_metric_strings); + } + + fn insert_metrics(&self, msg: Split) { + let all_valid_metrics: Vec = msg + .filter(|m| { + !m.is_empty() + && !m.starts_with("_sc|") + && !m.starts_with("_e{") + // todo(serverless): remove this hack, and create a blocklist for metrics + // or another mechanism for this. + // + // avoid metric duplication with lambda layer + && !m.starts_with("aws.lambda.enhanced.invocations") + }) // exclude empty messages, service checks, and events + .map(|m| m.replace('\n', "")) + .filter_map(|m| match parse(m.as_str()) { + Ok(metric) => Some(metric), + Err(e) => { + // unsupported type is quite common with dd_trace metrics. Avoid perf issue and + // log spam in that case + match e { + UnsupportedType(_) => debug!("Unsupported metric type: {}. {}", m, e), + _ => error!("Failed to parse metric {}: {}", m, e), + } + None + } + }) + .collect(); + if !all_valid_metrics.is_empty() { + #[allow(clippy::expect_used)] + let mut guarded_aggregator = self.aggregator.lock().expect("lock poisoned"); + for a_valid_value in all_valid_metrics { + let _ = guarded_aggregator.insert(a_valid_value); + } + } + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use crate::aggregator::tests::assert_sketch; + use crate::aggregator::tests::assert_value; + use crate::aggregator::Aggregator; + use crate::dogstatsd::{BufferReader, DogStatsD}; + use crate::metric::EMPTY_TAGS; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::sync::{Arc, Mutex}; + use tracing_test::traced_test; + + #[tokio::test] + #[cfg_attr(miri, ignore)] + async fn test_dogstatsd_multi_distribution() { + let locked_aggregator = setup_dogstatsd( + "single_machine_performance.rouster.api.series_v2.payload_size_bytes:269942|d|T1656581409 +single_machine_performance.rouster.metrics_min_timestamp_latency:1426.90870216|d|T1656581409 +single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d|T1656581409 +", + ) + .await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + + let parsed_metrics = aggregator.distributions_to_protobuf(); + + assert_eq!(parsed_metrics.sketches.len(), 3); + assert_eq!(aggregator.to_series().len(), 0); + drop(aggregator); + + assert_sketch( + &locked_aggregator, + "single_machine_performance.rouster.api.series_v2.payload_size_bytes", + 269_942_f64, + 1656581400, + ); + assert_sketch( + &locked_aggregator, + "single_machine_performance.rouster.metrics_min_timestamp_latency", + 1_426.908_702_16, + 1656581400, + ); + assert_sketch( + &locked_aggregator, + "single_machine_performance.rouster.metrics_max_timestamp_latency", + 1_376.908_702_16, + 1656581400, + ); + } + + #[tokio::test] + #[cfg_attr(miri, ignore)] + async fn test_dogstatsd_multi_metric() { + let mut now = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + now = (now / 10) * 10; + let locked_aggregator = setup_dogstatsd( + format!( + "metric3:3|c|#tag3:val3,tag4:val4\nmetric1:1|c\nmetric2:2|c|#tag2:val2|T{:}\n", + now + ) + .as_str(), + ) + .await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + + let parsed_metrics = aggregator.to_series(); + + assert_eq!(parsed_metrics.len(), 3); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); + drop(aggregator); + + assert_value(&locked_aggregator, "metric1", 1.0, "", now); + assert_value(&locked_aggregator, "metric2", 2.0, "tag2:val2", now); + assert_value( + &locked_aggregator, + "metric3", + 3.0, + "tag3:val3,tag4:val4", + now, + ); + } + + #[tokio::test] + #[cfg_attr(miri, ignore)] + async fn test_dogstatsd_single_metric() { + let locked_aggregator = setup_dogstatsd("metric123:99123|c|T1656581409").await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + let parsed_metrics = aggregator.to_series(); + + assert_eq!(parsed_metrics.len(), 1); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); + drop(aggregator); + + assert_value(&locked_aggregator, "metric123", 99_123.0, "", 1656581400); + } + + #[tokio::test] + #[traced_test] + #[cfg_attr(miri, ignore)] + async fn test_dogstatsd_filter_service_check() { + let locked_aggregator = setup_dogstatsd("_sc|servicecheck|0").await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + let parsed_metrics = aggregator.to_series(); + + assert!(!logs_contain("Failed to parse metric")); + assert_eq!(parsed_metrics.len(), 0); + } + + #[tokio::test] + #[traced_test] + #[cfg_attr(miri, ignore)] + async fn test_dogstatsd_filter_event() { + let locked_aggregator = setup_dogstatsd("_e{5,10}:event|test event").await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + let parsed_metrics = aggregator.to_series(); + + assert!(!logs_contain("Failed to parse metric")); + assert_eq!(parsed_metrics.len(), 0); + } + + async fn setup_dogstatsd(statsd_string: &str) -> Arc> { + let aggregator_arc = Arc::new(Mutex::new( + Aggregator::new(EMPTY_TAGS, 1_024).expect("aggregator creation failed"), + )); + let cancel_token = tokio_util::sync::CancellationToken::new(); + + let dogstatsd = DogStatsD { + cancel_token, + aggregator: Arc::clone(&aggregator_arc), + buffer_reader: BufferReader::MirrorReader( + statsd_string.as_bytes().to_vec(), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(111, 112, 113, 114)), 0), + ), + }; + dogstatsd.consume_statsd().await; + + aggregator_arc + } +} diff --git a/crates/dogstatsd/src/errors.rs b/crates/dogstatsd/src/errors.rs new file mode 100644 index 0000000..fffcca8 --- /dev/null +++ b/crates/dogstatsd/src/errors.rs @@ -0,0 +1,35 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Error types for `metrics` module + +/// Errors for the function [`crate::metric::Metric::parse`] +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ParseError { + /// Parse failure given in text + #[error("parse failure: {0}")] + Raw(String), + #[error("unsupported metric type: {0}")] + UnsupportedType(String), +} + +/// Failure to create a new `Aggregator` +#[derive(Debug, thiserror::Error, Clone, Copy)] +pub enum Creation { + /// The specified context max is too large given our constants. Indicates a + /// serious programming error. + #[error("context max is too large")] + Contexts, +} + +/// Failures from `Aggregator::insert` +#[derive(Debug, thiserror::Error)] +pub enum Insert { + /// The current interval is full and no further metrics can be inserted. The + /// inserted metric is returned. + #[error("interval is full")] + Overflow, + /// Unable to parse passed values + #[error(transparent)] + ValuesIteration(#[from] std::num::ParseFloatError), +} diff --git a/crates/dogstatsd/src/flusher.rs b/crates/dogstatsd/src/flusher.rs new file mode 100644 index 0000000..35a3f8c --- /dev/null +++ b/crates/dogstatsd/src/flusher.rs @@ -0,0 +1,115 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use crate::aggregator::Aggregator; +use crate::datadog::{DdApi, MetricsIntakeUrlPrefix, RetryStrategy}; +use reqwest::{Response, StatusCode}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tracing::{debug, error}; + +pub struct Flusher { + dd_api: DdApi, + aggregator: Arc>, +} + +pub struct FlusherConfig { + pub api_key: String, + pub aggregator: Arc>, + pub metrics_intake_url_prefix: MetricsIntakeUrlPrefix, + pub https_proxy: Option, + pub timeout: Duration, + pub retry_strategy: RetryStrategy, +} + +#[allow(clippy::await_holding_lock)] +impl Flusher { + pub fn new(config: FlusherConfig) -> Self { + let dd_api = DdApi::new( + config.api_key, + config.metrics_intake_url_prefix, + config.https_proxy, + config.timeout, + config.retry_strategy, + ); + Flusher { + dd_api, + aggregator: config.aggregator, + } + } + + pub async fn flush(&mut self) { + let (all_series, all_distributions) = { + #[allow(clippy::expect_used)] + let mut aggregator = self.aggregator.lock().expect("lock poisoned"); + ( + aggregator.consume_metrics(), + aggregator.consume_distributions(), + ) + }; + + let n_series = all_series.len(); + let n_distributions = all_distributions.len(); + + debug!("Flushing {n_series} series and {n_distributions} distributions"); + + let dd_api_clone = self.dd_api.clone(); + let series_handle = tokio::spawn(async move { + for a_batch in all_series { + let continue_shipping = + should_try_next_batch(dd_api_clone.ship_series(&a_batch).await).await; + if !continue_shipping { + break; + } + } + }); + let dd_api_clone = self.dd_api.clone(); + let distributions_handle = tokio::spawn(async move { + for a_batch in all_distributions { + let continue_shipping = + should_try_next_batch(dd_api_clone.ship_distributions(&a_batch).await).await; + if !continue_shipping { + break; + } + } + }); + + match tokio::try_join!(series_handle, distributions_handle) { + Ok(_) => { + debug!("Successfully flushed {n_series} series and {n_distributions} distributions") + } + Err(err) => { + error!("Failed to flush metrics{err}") + } + }; + } +} + +pub enum ShippingError { + Payload(String), + Destination(Option, String), +} + +async fn should_try_next_batch(resp: Result) -> bool { + match resp { + Ok(resp_payload) => match resp_payload.status() { + StatusCode::ACCEPTED => true, + unexpected_status_code => { + error!( + "{}: Failed to push to API: {:?}", + unexpected_status_code, + resp_payload.text().await.unwrap_or_default() + ); + true + } + }, + Err(ShippingError::Payload(msg)) => { + error!("Failed to prepare payload. Data dropped: {}", msg); + true + } + Err(ShippingError::Destination(sc, msg)) => { + error!("Error shipping data: {:?} {}", sc, msg); + false + } + } +} diff --git a/crates/dogstatsd/src/lib.rs b/crates/dogstatsd/src/lib.rs new file mode 100644 index 0000000..4009db1 --- /dev/null +++ b/crates/dogstatsd/src/lib.rs @@ -0,0 +1,16 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unimplemented))] + +pub mod aggregator; +pub mod constants; +pub mod datadog; +pub mod dogstatsd; +pub mod errors; +pub mod flusher; +pub mod metric; diff --git a/crates/dogstatsd/src/metric.rs b/crates/dogstatsd/src/metric.rs new file mode 100644 index 0000000..08d68ab --- /dev/null +++ b/crates/dogstatsd/src/metric.rs @@ -0,0 +1,656 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use crate::errors::ParseError; +use crate::{constants, datadog}; +use ddsketch_agent::DDSketch; +use fnv::FnvHasher; +use protobuf::Chars; +use regex::Regex; +use std::hash::{Hash, Hasher}; +use std::sync::OnceLock; +use ustr::Ustr; + +pub const EMPTY_TAGS: SortedTags = SortedTags { values: Vec::new() }; + +// https://docs.datadoghq.com/developers/dogstatsd/datagram_shell?tab=metrics#dogstatsd-protocol-v13 +static METRIC_REGEX: OnceLock = OnceLock::new(); +fn get_metric_regex() -> &'static Regex { + #[allow(clippy::expect_used)] + METRIC_REGEX.get_or_init(|| { + Regex::new( + r"^(?P[^:]+):(?P[^|]+)\|(?P[a-zA-Z]+)(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?(?:\|c:(?P[^|]+))?(?:\|T(?P[^|]+))?$", + ) + .expect("Failed to create metric regex") + }) +} + +#[derive(Clone, Debug)] +pub enum MetricValue { + /// Dogstatsd 'count' metric type, monotonically increasing counter + Count(f64), + /// Dogstatsd 'gauge' metric type, point-in-time value + Gauge(f64), + /// Dogstatsd 'distribution' metric type, histogram + Distribution(DDSketch), +} + +impl MetricValue { + pub fn count(v: f64) -> MetricValue { + MetricValue::Count(v) + } + + pub fn gauge(v: f64) -> MetricValue { + MetricValue::Gauge(v) + } + + pub fn distribution(v: f64) -> MetricValue { + let sketch = &mut DDSketch::default(); + sketch.insert(v); + MetricValue::Distribution(sketch.to_owned()) + } +} + +#[derive(Clone, Debug)] +pub struct SortedTags { + // We sort tags. This is in feature parity with DogStatsD and also means + // that we avoid storing the same context multiple times because users have + // passed tags in different order through time. + values: Vec<(Ustr, Ustr)>, +} + +impl SortedTags { + pub fn extend(&mut self, other: &SortedTags) { + self.values.extend_from_slice(&other.values); + self.values.dedup(); + self.values.sort_unstable(); + } + + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + pub fn parse(tags_section: &str) -> Result { + let total_tags = tags_section.bytes().filter(|&b| b == b',').count() + 1; + let mut parsed_tags = Vec::with_capacity(total_tags); + + for part in tags_section.split(',').filter(|s| !s.is_empty()) { + if let Some(i) = part.find(':') { + // Avoid creating a new string via split_once + let (k, v) = (&part[..i], &part[i + 1..]); + parsed_tags.push((Ustr::from(k), Ustr::from(v))); + } else { + parsed_tags.push((Ustr::from(part), Ustr::from(""))); + } + } + + parsed_tags.dedup(); + if parsed_tags.len() > constants::MAX_TAGS { + return Err(ParseError::Raw(format!( + "Too many tags, more than {c}", + c = constants::MAX_TAGS + ))); + } + + parsed_tags.sort_unstable(); + Ok(SortedTags { + values: parsed_tags, + }) + } + + pub fn to_chars(&self) -> Vec { + let mut tags_as_chars = Vec::new(); + for (k, v) in &self.values { + if v.is_empty() { + tags_as_chars.push(Chars::from(k.to_string())); + } else { + let mut a_tag = String::with_capacity(k.len() + v.len() + 1); + a_tag.push_str(k); + a_tag.push(':'); + a_tag.push_str(v); + tags_as_chars.push(a_tag.into()); + } + } + tags_as_chars + } + + pub fn to_strings(&self) -> Vec { + let mut tags_as_vec = Vec::new(); + for (k, v) in &self.values { + if v.is_empty() { + tags_as_vec.push(k.to_string()); + } else { + let mut a_tag = String::with_capacity(k.len() + v.len() + 1); + a_tag.push_str(k); + a_tag.push(':'); + a_tag.push_str(v); + tags_as_vec.push(a_tag); + } + } + tags_as_vec + } + + pub(crate) fn to_resources(&self) -> Vec { + let mut resources = Vec::with_capacity(constants::MAX_TAGS); + for (key, val) in &self.values { + if key == "dd.internal.resource" { + //anything coming in via dd.internal.resource: has to be a key/value pair + // (e.g., dd.internal.resource:key:value) + if let Some(valid_name_kind) = val.split_once(':') { + let resource = datadog::Resource { + name: valid_name_kind.0.to_string(), + kind: valid_name_kind.1.to_string(), + }; + resources.push(resource); + } + } + } + resources + } +} + +/// Representation of a dogstatsd Metric +/// +/// For now this implementation covers only counters and gauges. We hope this is +/// enough to demonstrate the impact of this program's design goals. +#[derive(Clone, Debug)] +pub struct Metric { + /// Name of the metric. + /// + /// Never more bytes than `constants::MAX_METRIC_NAME_BYTES`, + /// enforced by construction. Note utf8 issues. + pub name: Ustr, + /// Values of the metric. A singular value may be either a floating point or + /// a integer. Although undocumented we assume 64 bit. A single metric may + /// encode multiple values a time in a message. There must be at least one + /// value here, meaning that there is guaranteed to be a Some value in the + /// 0th index. + /// + /// Parsing of the values to an integer type is deferred until the last + /// moment. + /// + /// Never longer than `constants::MAX_VALUE_BYTES`. Note utf8 issues. + pub value: MetricValue, + /// Tags of the metric. + /// + /// The key is never longer than `constants::MAX_TAG_KEY_BYTES`, the value + /// never more than `constants::MAX_TAG_VALUE_BYTES`. These are enforced by + /// the parser. We assume here that tags are not sent in random order by the + /// clien or that, if they are, the API will tidy that up. That is `a:1,b:2` + /// is a different tagset from `b:2,a:1`. + pub tags: Option, + + /// ID given a name and tagset. + pub id: u64, + // Timestamp + pub timestamp: i64, +} + +impl Metric { + pub fn new( + name: Ustr, + value: MetricValue, + tags: Option, + timestamp: Option, + ) -> Metric { + #[allow(clippy::expect_used)] + let parsed_timestamp = timestamp_to_bucket(timestamp.unwrap_or_else(|| { + std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default() + })); + + let id = id(name, &tags, parsed_timestamp); + Metric { + name, + value, + tags, + id, + timestamp: parsed_timestamp, + } + } +} + +// Round down to the nearest 10 seconds +// to form a bucket of metric contexts aggregated per 10s +pub fn timestamp_to_bucket(timestamp: i64) -> i64 { + #[allow(clippy::expect_used)] + let now_seconds: i64 = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + if timestamp > now_seconds { + return (now_seconds / 10) * 10; + } + (timestamp / 10) * 10 +} + +/// Parse a metric from given input. +/// +/// This function parses a passed `&str` into a `Metric`. We assume that +/// `DogStatsD` metrics must be utf8 and are not ascii or some other encoding. +/// +/// # Errors +/// +/// This function will return with an error if the input violates any of the +/// limits in [`constants`]. Any non-viable input will be discarded. +/// example aj-test.increment:1|c|#user:aj-test from 127.0.0.1:50983 +pub fn parse(input: &str) -> Result { + // TODO must enforce / exploit constraints given in `constants`. + if let Some(caps) = get_metric_regex().captures(input) { + // unused for now + // let sample_rate = caps.name("sample_rate").map(|m| m.as_str()); + + let tags; + if let Some(tags_section) = caps.name("tags") { + tags = Some(SortedTags::parse(tags_section.as_str())?); + } else { + tags = None; + } + + #[allow(clippy::unwrap_used)] + let val = first_value(caps.name("values").unwrap().as_str())?; + + #[allow(clippy::unwrap_used)] + let t = caps.name("type").unwrap().as_str(); + + #[allow(clippy::expect_used)] + let now = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + // let Metric::new() handle bucketing the timestamp + let parsed_timestamp: i64 = match caps.name("timestamp") { + Some(ts) => timestamp_to_bucket(ts.as_str().parse().unwrap_or(now)), + None => timestamp_to_bucket(now), + }; + let metric_value = match t { + "c" => MetricValue::Count(val), + "g" => MetricValue::Gauge(val), + "d" => { + let sketch = &mut DDSketch::default(); + sketch.insert(val); + MetricValue::Distribution(sketch.to_owned()) + } + "h" | "s" | "ms" => { + return Err(ParseError::UnsupportedType(t.to_string())); + } + _ => { + return Err(ParseError::Raw(format!("Invalid metric type: {t}"))); + } + }; + #[allow(clippy::unwrap_used)] + let name = Ustr::from(caps.name("name").unwrap().as_str()); + let id = id(name, &tags, parsed_timestamp); + return Ok(Metric { + name, + value: metric_value, + tags, + id, + timestamp: parsed_timestamp, + }); + } + Err(ParseError::Raw(format!("Invalid metric format {input}"))) +} + +fn first_value(values: &str) -> Result { + match values.split(':').next() { + Some(v) => match v.parse::() { + Ok(v) => Ok(v), + Err(e) => Err(ParseError::Raw(format!("Invalid value {e}"))), + }, + None => Err(ParseError::Raw("Missing value".to_string())), + } +} + +/// Create an ID given a name and tagset. +/// +/// This function constructs a hash of the name, the tagset and that hash is +/// identical no matter the internal order of the tagset. That is, we consider a +/// tagset like "a:1,b:2,c:3" to be idential to "b:2,c:3,a:1" to "c:3,a:1,b:2" +/// etc. This implies that we must sort the tagset after parsing it, which we +/// do. Duplicate tags are removed, so "a:1,a:1" will +/// hash to the same ID of "a:1". +/// +/// Note also that because we take `Ustr` arguments its possible that we've +/// interned many possible combinations of a tagset, even if they are identical +/// from the point of view of this function. +#[inline] +#[must_use] +pub fn id(name: Ustr, tags: &Option, timestamp: i64) -> u64 { + let mut hasher = FnvHasher::default(); + + name.hash(&mut hasher); + timestamp.hash(&mut hasher); + if let Some(tags_present) = tags { + for kv in tags_present.values.iter() { + kv.0.as_bytes().hash(&mut hasher); + kv.1.as_bytes().hash(&mut hasher); + } + } + hasher.finish() +} +// :::||@|#:, +// :,|T|c: +// +// Types: +// * c -- COUNT, allows packed values, summed +// * g -- GAUGE, allows packed values, last one wins +// +// SAMPLE_RATE ignored for the sake of simplicity. + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use proptest::{collection, option, strategy::Strategy, string::string_regex}; + use ustr::Ustr; + + use crate::metric::{id, parse, timestamp_to_bucket, MetricValue, SortedTags}; + + use super::ParseError; + + fn metric_name() -> impl Strategy { + string_regex("[a-zA-Z0-9.-]{1,128}").unwrap() + } + + fn metric_values() -> impl Strategy { + string_regex("[0-9]+(:[0-9]){0,8}").unwrap() + } + + fn metric_type() -> impl Strategy { + string_regex("g|c").unwrap() + } + + fn metric_tagset() -> impl Strategy> { + option::of( + string_regex("[a-zA-Z]{1,64}:[a-zA-Z]{1,64}(,[a-zA-Z]{1,64}:[a-zA-Z]{1,64}){0,31}") + .unwrap(), + ) + } + + fn metric_tags() -> impl Strategy> { + collection::vec(("[a-z]{1,8}", "[A-Z]{1,8}"), 0..32) + } + + proptest::proptest! { + // For any valid name, tags et al the parse routine is able to parse an + // encoded metric line. + #[test] + #[cfg_attr(miri, ignore)] + fn parse_valid_inputs( + name in metric_name(), + values in metric_values(), + mtype in metric_type(), + tagset in metric_tagset() + ) { + let input = if let Some(ref tagset) = tagset { + format!("{name}:{values}|{mtype}|#{tagset}") + } else { + format!("{name}:{values}|{mtype}") + }; + let metric = parse(&input).unwrap(); + assert_eq!(name, metric.name.as_str()); + + if let Some(tags) = tagset { + let parsed_metric_tags : SortedTags = metric.tags.unwrap(); + assert_eq!(tags.split(',').count(), parsed_metric_tags.values.len()); + tags.split(',').for_each(|kv| { + let (original_key, original_value) = kv.split_once(':').unwrap(); + let mut found = false; + for (k,v) in parsed_metric_tags.values.iter() { + // TODO not sure who to handle duplicate keys. To make the test pass, just find any match instead of first + if *k == Ustr::from(original_key) && *v == Ustr::from(original_value) { + found = true; + } + } + assert!(found); + }); + } else { + assert!(metric.tags.is_none()); + } + + match mtype.as_str() { + "c" => { + if let MetricValue::Count(v) = metric.value { + assert_eq!(v, values.split(':').next().unwrap().parse::().unwrap()); + } else { + panic!("Expected count metric"); + } + } + "g" => { + if let MetricValue::Gauge(v) = metric.value { + assert_eq!(v, values.split(':').next().unwrap().parse::().unwrap()); + } else { + panic!("Expected gauge metric"); + } + } + "d" => { + if let MetricValue::Distribution(d) = metric.value { + assert_eq!(d.min().unwrap(), values.split(':').next().unwrap().parse::().unwrap()); + } else { + panic!("Expected distribution metric"); + } + } + _ => { + panic!("Invalid metric format"); + } + } + } + + #[test] + #[cfg_attr(miri, ignore)] + fn parse_missing_name_and_value( + mtype in metric_type(), + tagset in metric_tagset() + ) { + let input = if let Some(ref tagset) = tagset { + format!("|{mtype}|#{tagset}") + } else { + format!("|{mtype}") + }; + let result = parse(&input); + + assert_eq!(result.unwrap_err(),ParseError::Raw(format!("Invalid metric format {input}"))); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn parse_invalid_name_and_value_format( + name in metric_name(), + values in metric_values(), + mtype in metric_type(), + tagset in metric_tagset() + ) { + // If there is a ':' in the values we cannot distinguish where the + // name and the first value are. + let value = values.split(':').next().unwrap(); + let input = if let Some(ref tagset) = tagset { + format!("{name}{value}|{mtype}|#{tagset}") + } else { + format!("{name}{value}|{mtype}") + }; + let result = parse(&input); + + let verify = result.unwrap_err().to_string(); + assert!(verify.starts_with("parse failure: Invalid metric format ")); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn parse_unsupported_metric_type( + name in metric_name(), + values in metric_values(), + mtype in "[abefijklmnopqrtuvwxyz]", + tagset in metric_tagset() + ) { + let input = if let Some(ref tagset) = tagset { + format!("{name}:{values}|{mtype}|#{tagset}") + } else { + format!("{name}:{values}|{mtype}") + }; + let result = parse(&input); + + assert_eq!( + result.unwrap_err(), + ParseError::Raw(format!("Invalid metric type: {mtype}")) + ); + } + + // The ID of a name, tagset is the same even if the tagset is in a + // distinct order. + // For any valid name, tags et al the parse routine is able to parse an + // encoded metric line. + #[test] + #[cfg_attr(miri, ignore)] + fn id_consistent(name in metric_name(), + mut tags in metric_tags()) { + let mut tagset1 = String::new(); + let mut tagset2 = String::new(); + let now = timestamp_to_bucket(std::time::UNIX_EPOCH.elapsed().expect("unable to poll clock, unrecoverable").as_secs().try_into().unwrap_or_default()); + + for (k,v) in &tags { + tagset1.push_str(k); + tagset1.push(':'); + tagset1.push_str(v); + tagset1.push(','); + } + tags.reverse(); + for (k,v) in &tags { + tagset2.push_str(k); + tagset2.push(':'); + tagset2.push_str(v); + tagset2.push(','); + } + if !tags.is_empty() { + tagset1.pop(); + tagset2.pop(); + } + + let id1 = id(Ustr::from(&name), &Some(SortedTags::parse(&tagset1).unwrap()), now); + let id2 = id(Ustr::from(&name), &Some(SortedTags::parse(&tagset2).unwrap()), now); + + assert_eq!(id1, id2); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn resources_key_val_order(tags in metric_tags()) { + let sorted_tags = SortedTags { values: tags.into_iter() + .map(|(kind, name)| (Ustr::from(&kind), Ustr::from(&name))) + .collect() }; + + let resources = sorted_tags.to_resources(); + + for (i, resource) in resources.iter().enumerate() { + assert_eq!(resource.kind, sorted_tags.values[i].0); + assert_eq!(resource.name, sorted_tags.values[i].1); + } + } + } + + #[test] + #[cfg_attr(miri, ignore)] + fn parse_too_many_tags() { + // 101 + assert_eq!( + parse( + "foo:1|g|#a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10,k:11,l:12,m:13,n:14,o:15,p:16,q:17,r:18,s:19,t:20,u:21,v:22,w:23,x:24,y:25,z:26,aa:27,ab:28,ac:29,ad:30,ae:31,af:32,ag:33,ah:34,ai:35,aj:36,ak:37,al:38,am:39,an:40,ao:41,ap:42,aq:43,ar:44,as:45,at:46,au:47,av:48,aw:49,ax:50,ay:51,az:52,ba:53,bb:54,bc:55,bd:56,be:57,bf:58,bg:59,bh:60,bi:61,bj:62,bk:63,bl:64,bm:65,bn:66,bo:67,bp:68,bq:69,br:70,bs:71,bt:72,bu:73,bv:74,bw:75,bx:76,by:77,bz:78,ca:79,cb:80,cc:81,cd:82,ce:83,cf:84,cg:85,ch:86,ci:87,cj:88,ck:89,cl:90,cm:91,cn:92,co:93,cp:94,cq:95,cr:96,cs:97,ct:98,cu:99,cv:100,cw:101" + ).unwrap_err(), + ParseError::Raw("Too many tags, more than 100".to_string()) + ); + + // 30 + assert!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").is_ok()); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn invalid_dogstatsd_no_panic() { + assert!(parse("somerandomstring|c+a;slda").is_err()); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn parse_container_id() { + assert!(parse("containerid.metric:0|c|#env:dev,client_transport:udp|c:0000000000000000000000000000000000000000000000000000000000000000").is_ok()); + } + + #[test] + fn parse_tag_no_value() { + let result = parse("datadog.tracer.flush_triggered:1|c|#lang:go,lang_version:go1.22.10,_dd.origin:lambda,runtime-id:d66f501c-d09b-4d0d-970f-515235c4eb56,v1.65.1,service:aws.lambda,reason:scheduled"); + assert!(result.is_ok()); + assert!(result + .unwrap() + .tags + .unwrap() + .values + .iter() + .any(|(k, v)| k == "v1.65.1" && v.is_empty())); + } + + #[test] + fn parse_tag_multi_column() { + let result = parse("datadog.tracer.flush_triggered:1|c|#lang:go:and:something:else"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap().tags.unwrap().values[0], + (Ustr::from("lang"), Ustr::from("go:and:something:else")) + ); + } + + #[test] + fn parse_tracer_metric() { + let input = "datadog.tracer.flush_duration:0.785551|ms|#lang:go,lang_version:go1.23.2,env:redacted_env,_dd.origin:lambda,runtime-id:redacted_runtime,tracer_version:v1.70.1,service:redacted_service,env:redacted_env,service:redacted_service,version:redacted_version"; + let expected_error = "ms".to_string(); + if let ParseError::UnsupportedType(actual_error) = parse(input).unwrap_err() { + assert_eq!(actual_error, expected_error); + } else { + panic!("Expected UnsupportedType error"); + } + } + + #[test] + fn parse_metric_timestamp() { + // Important to test that we round down to the nearest 10 seconds + // for our buckets + let input = "page.views:15|c|#env:dev|T1656581409"; + let metric = parse(input).unwrap(); + assert_eq!(metric.timestamp, 1656581400); + } + + #[test] + fn parse_metric_no_timestamp() { + // *wince* this could be a race condition + // we round the timestamp down to a 10s bucket and I want to test now + // but if the timestamp rolls over to the next bucket time and the test + // is somehow slower than 1s then the test will fail. + // come bug me if I wrecked your CI run + let input = "page.views:15|c|#env:dev"; + let metric = parse(input).unwrap(); + let now: i64 = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + assert_eq!(metric.timestamp, (now / 10) * 10); + } + + #[test] + fn sorting_tags() { + let mut tags = SortedTags::parse("z:z0,b:b2,c:c3").unwrap(); + tags.extend(&SortedTags::parse("z1:z11,d:d4,e:e5,f:f6").unwrap()); + tags.extend(&SortedTags::parse("a:a1").unwrap()); + assert_eq!(tags.values.len(), 8); + let first_element = tags.values.first().unwrap(); + assert_eq!(first_element.0, Ustr::from("a")); + assert_eq!(first_element.1, Ustr::from("a1")); + } +} diff --git a/crates/dogstatsd/tests/integration_test.rs b/crates/dogstatsd/tests/integration_test.rs new file mode 100644 index 0000000..641c3eb --- /dev/null +++ b/crates/dogstatsd/tests/integration_test.rs @@ -0,0 +1,268 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use dogstatsd::metric::SortedTags; +use dogstatsd::{ + aggregator::Aggregator as MetricsAggregator, + constants::CONTEXTS, + datadog::{DdDdUrl, MetricsIntakeUrlPrefix, MetricsIntakeUrlPrefixOverride}, + dogstatsd::{DogStatsD, DogStatsDConfig}, + flusher::{Flusher, FlusherConfig}, +}; +use mockito::Server; +use std::sync::{Arc, Mutex}; +use tokio::{ + net::UdpSocket, + time::{sleep, timeout, Duration}, +}; +use tokio_util::sync::CancellationToken; + +#[cfg(test)] +#[cfg(not(miri))] +#[tokio::test] +async fn dogstatsd_server_ships_series() { + use dogstatsd::datadog::RetryStrategy; + + let mut mock_server = Server::new_async().await; + + let mock = mock_server + .mock("POST", "/api/v2/series") + .match_header("DD-API-KEY", "mock-api-key") + .match_header("Content-Type", "application/json") + .with_status(202) + .create_async() + .await; + + let metrics_aggr = Arc::new(Mutex::new( + MetricsAggregator::new(SortedTags::parse("sometkey:somevalue").unwrap(), CONTEXTS) + .expect("failed to create aggregator"), + )); + + let _ = start_dogstatsd(&metrics_aggr).await; + + let mut metrics_flusher = Flusher::new(FlusherConfig { + api_key: "mock-api-key".to_string(), + aggregator: Arc::clone(&metrics_aggr), + metrics_intake_url_prefix: MetricsIntakeUrlPrefix::new( + None, + MetricsIntakeUrlPrefixOverride::maybe_new( + None, + Some(DdDdUrl::new(mock_server.url()).expect("failed to create URL")), + ), + ) + .expect("failed to create URL"), + https_proxy: None, + timeout: std::time::Duration::from_secs(5), + retry_strategy: RetryStrategy::Immediate(3), + }); + + let server_address = "127.0.0.1:18125"; + let socket = UdpSocket::bind("0.0.0.0:0") + .await + .expect("unable to bind UDP socket"); + let metric = "custom_metric:1|g"; + + socket + .send_to(metric.as_bytes(), &server_address) + .await + .expect("unable to send metric"); + + let flush = async { + while !mock.matched() { + sleep(Duration::from_millis(100)).await; + metrics_flusher.flush().await; + } + }; + + let result = timeout(Duration::from_millis(1000), flush).await; + + match result { + Ok(_) => mock.assert(), + Err(_) => panic!("timed out before server received metric flush"), + } +} + +async fn start_dogstatsd(metrics_aggr: &Arc>) -> CancellationToken { + let dogstatsd_config = DogStatsDConfig { + host: "127.0.0.1".to_string(), + port: 18125, + }; + let dogstatsd_cancel_token = tokio_util::sync::CancellationToken::new(); + let dogstatsd_client = DogStatsD::new( + &dogstatsd_config, + Arc::clone(metrics_aggr), + dogstatsd_cancel_token.clone(), + ) + .await; + + tokio::spawn(async move { + dogstatsd_client.spin().await; + }); + + dogstatsd_cancel_token +} + +#[cfg(test)] +#[cfg(not(miri))] +#[tokio::test] +async fn test_send_with_retry_immediate_failure() { + use dogstatsd::datadog::{DdApi, DdDdUrl, RetryStrategy}; + use dogstatsd::metric::{parse, SortedTags}; + + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/api/v2/series") + .with_status(500) + .with_body("Internal Server Error") + .expect(3) + .create_async() + .await; + + let retry_strategy = RetryStrategy::Immediate(3); + let dd_api = DdApi::new( + "test_key".to_string(), + MetricsIntakeUrlPrefix::new( + None, + MetricsIntakeUrlPrefixOverride::maybe_new( + None, + Some(DdDdUrl::new(server.url()).expect("failed to create URL")), + ), + ) + .expect("failed to create URL"), + None, + Duration::from_secs(1), + retry_strategy.clone(), + ); + + // Create a series using the Aggregator + let mut aggregator = MetricsAggregator::new(SortedTags::parse("test:value").unwrap(), 1) + .expect("failed to create aggregator"); + let metric = parse("test:1|c").expect("failed to parse metric"); + aggregator.insert(metric).expect("failed to insert metric"); + let series = aggregator.to_series(); + + let result = dd_api.ship_series(&series).await; + + // The result should be an error since we got a 500 response + assert!(result.is_err()); + + // Verify that the mock was called exactly 3 times + mock.assert_async().await; +} + +#[cfg(test)] +#[cfg(not(miri))] +#[tokio::test] +async fn test_send_with_retry_linear_backoff_success() { + use dogstatsd::datadog::{DdApi, DdDdUrl, RetryStrategy}; + use dogstatsd::metric::{parse, SortedTags}; + + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/api/v2/series") + .with_status(500) + .with_body("Internal Server Error") + .expect(1) + .create_async() + .await; + + let success_mock = server + .mock("POST", "/api/v2/series") + .with_status(200) + .with_body("Success") + .expect(1) + .create_async() + .await; + + let retry_strategy = RetryStrategy::LinearBackoff(3, 1); // 3 attempts, 1ms delay + let dd_api = DdApi::new( + "test_key".to_string(), + MetricsIntakeUrlPrefix::new( + None, + MetricsIntakeUrlPrefixOverride::maybe_new( + None, + Some(DdDdUrl::new(server.url()).expect("failed to create URL")), + ), + ) + .expect("failed to create URL"), + None, + Duration::from_secs(1), + retry_strategy.clone(), + ); + + // Create a series using the Aggregator + let mut aggregator = MetricsAggregator::new(SortedTags::parse("test:value").unwrap(), 1) + .expect("failed to create aggregator"); + let metric = parse("test:1|c").expect("failed to parse metric"); + aggregator.insert(metric).expect("failed to insert metric"); + let series = aggregator.to_series(); + + let result = dd_api.ship_series(&series).await; + + // The result should be Ok since we got a 200 response on retry + assert!(result.is_ok()); + if let Ok(response) = result { + assert_eq!(response.status(), reqwest::StatusCode::OK); + } else { + panic!("Expected Ok result"); + } + + // Verify that both mocks were called exactly once + mock.assert_async().await; + success_mock.assert_async().await; +} + +#[cfg(test)] +#[cfg(not(miri))] +#[tokio::test] +async fn test_send_with_retry_immediate_failure_after_one_attempt() { + use dogstatsd::datadog::{DdApi, DdDdUrl, RetryStrategy}; + use dogstatsd::flusher::ShippingError; + use dogstatsd::metric::{parse, SortedTags}; + + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/api/v2/series") + .with_status(500) + .with_body("Internal Server Error") + .expect(1) + .create_async() + .await; + + let retry_strategy = RetryStrategy::Immediate(1); // Only 1 attempt + let dd_api = DdApi::new( + "test_key".to_string(), + MetricsIntakeUrlPrefix::new( + None, + MetricsIntakeUrlPrefixOverride::maybe_new( + None, + Some(DdDdUrl::new(server.url()).expect("failed to create URL")), + ), + ) + .expect("failed to create URL"), + None, + Duration::from_secs(1), + retry_strategy.clone(), + ); + + // Create a series using the Aggregator + let mut aggregator = MetricsAggregator::new(SortedTags::parse("test:value").unwrap(), 1) + .expect("failed to create aggregator"); + let metric = parse("test:1|c").expect("failed to parse metric"); + aggregator.insert(metric).expect("failed to insert metric"); + let series = aggregator.to_series(); + + let result = dd_api.ship_series(&series).await; + + // The result should be an error since we got a 500 response + assert!(result.is_err()); + if let Err(ShippingError::Destination(Some(status), msg)) = result { + assert_eq!(status, reqwest::StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(msg, "Failed to send request after 1 attempts"); + } else { + panic!("Expected ShippingError::Destination with status 500"); + } + + // Verify that the mock was called exactly once + mock.assert_async().await; +}