diff --git a/Cargo.lock b/Cargo.lock index 462758d190..dcc1dff075 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ "aws-sdk-ssooidc", "aws-sdk-sts", "aws-smithy-async", - "aws-smithy-http", + "aws-smithy-http 0.60.11", "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.1" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +checksum = "faf26925f4a5b59eb76722b63c2892b1d70d06fa053c72e4a100ec308c1d47bc" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -325,17 +325,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aws-lc-rs" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" +dependencies = [ + "bindgen 0.72.1", + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "aws-runtime" -version = "1.5.3" +version = "1.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16d1aa50accc11a4b4d5c50f7fb81cc0cf60328259c587d0e6b0f11385bde46" +checksum = "bfa006bb32360ed90ac51203feafb9d02e3d21046e1fd3a450a404b90ea73e5d" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", - "aws-smithy-http", + "aws-smithy-http 0.62.4", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -344,13 +367,38 @@ dependencies = [ "fastrand", "http 0.2.12", "http-body 0.4.6", - "once_cell", "percent-encoding", "pin-project-lite", "tracing", "uuid", ] +[[package]] +name = "aws-sdk-cloudwatch" +version = "1.94.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d643daaf9ff124bb86ceda77e9c0aa0a480a42c0d670b95106602d34a05bd5a" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-compression", + "aws-smithy-http 0.62.4", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "flate2", + "http 0.2.12", + "http-body 0.4.6", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sdk-s3" version = "1.68.0" @@ -363,7 +411,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-checksums", "aws-smithy-eventstream", - "aws-smithy-http", + "aws-smithy-http 0.60.11", "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -394,7 +442,7 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", + "aws-smithy-http 0.60.11", "aws-smithy-json", "aws-smithy-query", "aws-smithy-runtime", @@ -417,7 +465,7 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", + "aws-smithy-http 0.60.11", "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -439,7 +487,7 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", + "aws-smithy-http 0.60.11", "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -461,7 +509,7 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", + "aws-smithy-http 0.60.11", "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -483,7 +531,7 @@ dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http", + "aws-smithy-http 0.60.11", "aws-smithy-json", "aws-smithy-query", "aws-smithy-runtime", @@ -499,13 +547,13 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.6" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3820e0c08d0737872ff3c7c1f21ebbb6693d832312d6152bf18ef50a5471c2" +checksum = "bffc03068fbb9c8dd5ce1c6fb240678a5cffb86fb2b7b1985c999c4b83c8df68" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", - "aws-smithy-http", + "aws-smithy-http 0.62.4", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", @@ -515,7 +563,6 @@ dependencies = [ "hmac", "http 0.2.12", "http 1.2.0", - "once_cell", "p256", "percent-encoding", "ring 0.17.8", @@ -528,9 +575,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427cb637d15d63d6f9aae26358e1c9a9c09d5aa490d64b09354c8217cfef0f28" +checksum = "127fcfad33b7dfc531141fda7e1c402ac65f88aca5511a4d31e2e3d2cd01ce9c" dependencies = [ "futures-util", "pin-project-lite", @@ -543,7 +590,7 @@ version = "0.60.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795" dependencies = [ - "aws-smithy-http", + "aws-smithy-http 0.60.11", "aws-smithy-types", "bytes", "crc32c", @@ -558,11 +605,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-smithy-compression" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a710da85cdc17745cc49cad5f24a3c92b26da14999ac3c7696981e6ef63666" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "flate2", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "pin-project-lite", + "tracing", +] + [[package]] name = "aws-smithy-eventstream" -version = "0.60.5" +version = "0.60.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" +checksum = "9656b85088f8d9dc7ad40f9a6c7228e1e8447cdf4b046c87e152e0805dea02fa" dependencies = [ "aws-smithy-types", "bytes", @@ -590,20 +654,79 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-smithy-http" +version = "0.62.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3feafd437c763db26aa04e0cc7591185d0961e64c61885bece0fb9d50ceac671" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.2.0", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1053b5e587e6fa40ce5a79ea27957b04ba660baa02b28b7436f64850152234f1" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.26", + "h2 0.4.12", + "http 0.2.12", + "http 1.2.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.7.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.5", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.33", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower 0.5.2", + "tracing", +] + [[package]] name = "aws-smithy-json" -version = "0.61.1" +version = "0.61.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e69cc50921eb913c6b662f8d909131bb3e6ad6cb6090d3a39b66fc5c52095" +checksum = "cff418fc8ec5cadf8173b10125f05c2e7e1d46771406187b2c878557d4503390" dependencies = [ "aws-smithy-types", ] +[[package]] +name = "aws-smithy-observability" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d1881b1ea6d313f9890710d65c158bdab6fb08c91ea825f74c1c8c357baf4cc" +dependencies = [ + "aws-smithy-runtime-api", +] + [[package]] name = "aws-smithy-query" -version = "0.60.7" +version = "0.60.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +checksum = "d28a63441360c477465f80c7abac3b9c4d075ca638f982e605b7dc2a2c7156c9" dependencies = [ "aws-smithy-types", "urlencoding", @@ -611,36 +734,33 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.6" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05dd41a70fc74051758ee75b5c4db2c0ca070ed9229c3df50e9475cda1cb985" +checksum = "40ab99739082da5347660c556689256438defae3bcefd66c52b095905730e404" dependencies = [ "aws-smithy-async", - "aws-smithy-http", + "aws-smithy-http 0.62.4", + "aws-smithy-http-client", + "aws-smithy-observability", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "fastrand", - "h2 0.3.26", "http 0.2.12", + "http 1.2.0", "http-body 0.4.6", "http-body 1.0.1", - "httparse", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "once_cell", "pin-project-lite", "pin-utils", - "rustls 0.21.12", "tokio", "tracing", ] [[package]] name = "aws-smithy-runtime-api" -version = "1.7.3" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" +checksum = "3683c5b152d2ad753607179ed71988e8cfd52964443b4f74fd8e552d0bbfeb46" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -655,9 +775,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.11" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ddc9bd6c28aeb303477170ddd183760a956a03e083b3902a990238a7e3792d" +checksum = "9f5b3a7486f6690ba25952cabf1e7d75e34d69eaff5081904a47bc79074d6457" dependencies = [ "base64-simd", "bytes", @@ -681,18 +801,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.9" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +checksum = "e9c34127e8c624bc2999f3b657e749c1393bedc9cd97b92a804db8ced4d2e163" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.3" +version = "1.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +checksum = "e2fd329bf0e901ff3f60425691410c69094dc2a1f34b331f37bfc4e9ac1565a1" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -744,7 +864,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.7.0", "hyper-util", "itoa", "matchit", @@ -884,7 +1004,7 @@ checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8" dependencies = [ "async-trait", "futures-util", - "parking_lot", + "parking_lot 0.12.3", "tokio", ] @@ -925,12 +1045,32 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.95", "which", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.9.4", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.95", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -976,6 +1116,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44aa969f86ffb99e5c2d51f393ec9ed6e9fe2f47b609c917b0071f129854d29" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e78cd86b6a6515d87392332fd63c4950ed3e50eab54275259a5f59f3666f90" +dependencies = [ + "darling 0.21.3", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.95", +] + [[package]] name = "brotli" version = "3.5.0" @@ -1037,9 +1202,12 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] [[package]] name = "bytes-utils" @@ -1065,10 +1233,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.7" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1089,6 +1258,12 @@ 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 = "chrono" version = "0.4.39" @@ -1168,9 +1343,9 @@ checksum = "aaa6b4b263a5d737e9bf6b7c09b72c41a5480aec4d7219af827f6564e950b6a5" [[package]] name = "cmake" -version = "0.1.52" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ "cc", ] @@ -1253,6 +1428,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1395,8 +1580,18 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1413,13 +1608,38 @@ dependencies = [ "syn 2.0.95", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.95", +] + [[package]] name = "darling_macro" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core", + "darling_core 0.20.10", + "quote", + "syn 2.0.95", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.95", ] @@ -1434,7 +1654,21 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", ] [[package]] @@ -1443,6 +1677,31 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "datadog-api-client" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c9e94efe831111077425078ad734568df3360461f5822de89c73ced862a046" +dependencies = [ + "async-stream", + "chrono", + "flate2", + "form-data-builder", + "futures-core", + "lazy_static", + "log", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", + "reqwest-retry", + "rustc_version", + "serde", + "serde_json", + "serde_with", + "url", + "uuid", + "zstd", +] + [[package]] name = "der" version = "0.6.1" @@ -1466,12 +1725,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1532,6 +1791,12 @@ dependencies = [ "shared_child", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "duration-string" version = "0.3.0" @@ -1631,8 +1896,10 @@ dependencies = [ "anyhow", "assert_matches", "async-stream", + "async-trait", "aws-config", "aws-credential-types", + "aws-sdk-cloudwatch", "aws-sdk-s3", "aws-sdk-sns", "aws-sdk-sqs", @@ -1649,6 +1916,8 @@ dependencies = [ "cidr", "colored", "cookie", + "dashmap 6.1.0", + "datadog-api-client", "duct", "email_address", "env_logger 0.10.2", @@ -1658,16 +1927,19 @@ dependencies = [ "futures-core", "futures-util", "gjson", - "google-cloud-gax", + "google-cloud-api", + "google-cloud-gax 0.17.0", "google-cloud-googleapis", + "google-cloud-monitoring-v3", "google-cloud-pubsub", "google-cloud-storage", + "google-cloud-wkt", "hex", "hmac", "http 1.2.0", "http-body-util", "httpdate", - "hyper 1.5.2", + "hyper 1.7.0", "indexmap 2.7.0", "insta", "jsonwebtoken", @@ -1675,6 +1947,7 @@ dependencies = [ "malachite", "matchit", "md5", + "metrics", "mime", "native-tls", "once_cell", @@ -1693,7 +1966,7 @@ dependencies = [ "radix_fmt", "rand 0.8.5", "regex", - "reqwest 0.12.12", + "reqwest 0.12.23", "rsa", "serde", "serde_json", @@ -1702,7 +1975,9 @@ dependencies = [ "serde_with", "sha2", "sha3", + "snap", "subtle", + "sysinfo", "thiserror 1.0.69", "tokio", "tokio-nsq", @@ -1711,7 +1986,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite 0.21.0", "tokio-util", - "tower-http", + "tower-http 0.5.2", "tower-service", "tracing", "tracing-subscriber", @@ -1733,7 +2008,7 @@ dependencies = [ "flate2", "futures", "http 1.2.0", - "hyper 1.5.2", + "hyper 1.7.0", "libc", "log", "openssl", @@ -1741,7 +2016,7 @@ dependencies = [ "prost 0.12.6", "prost-build 0.12.6", "prost-types 0.12.6", - "reqwest 0.12.12", + "reqwest 0.12.23", "serde", "serde_json", "tokio", @@ -1897,6 +2172,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1950,6 +2231,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form-data-builder" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ff8fb4527b05539a9f573ba2831a1127038a7b45eea385a338a63dc5ab6829" +dependencies = [ + "base64 0.13.1", + "rand 0.8.5", +] + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2133,9 +2424,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -2180,6 +2473,19 @@ dependencies = [ "walkdir", ] +[[package]] +name = "google-cloud-api" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a67dd9819a0fed2c9251b3455e76149e0ebd30f5ad96ff9ff2cec2e2a5037a5" +dependencies = [ + "bytes", + "google-cloud-wkt", + "serde", + "serde_json", + "serde_with", +] + [[package]] name = "google-cloud-auth" version = "0.13.2" @@ -2214,7 +2520,7 @@ dependencies = [ "google-cloud-token", "home", "jsonwebtoken", - "reqwest 0.12.12", + "reqwest 0.12.23", "serde", "serde_json", "thiserror 1.0.69", @@ -2224,6 +2530,28 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "google-cloud-auth" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d8757f37377c6436b94c6efaf7f9c3c6e48a213bb0790f7a639a9ba0183764" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bon", + "google-cloud-gax 1.0.0", + "http 1.2.0", + "reqwest 0.12.23", + "rustc_version", + "rustls 0.23.33", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "thiserror 2.0.10", + "time", + "tokio", +] + [[package]] name = "google-cloud-gax" version = "0.17.0" @@ -2240,6 +2568,47 @@ dependencies = [ "tracing", ] +[[package]] +name = "google-cloud-gax" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6b1718e2a6965a7fbb282f23645e7adb8e0a012848258bbd6f580307b684f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "google-cloud-rpc", + "google-cloud-wkt", + "http 1.2.0", + "pin-project", + "rand 0.9.2", + "serde", + "serde_json", + "thiserror 2.0.10", + "tokio", +] + +[[package]] +name = "google-cloud-gax-internal" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7ec794189133a50fd3901af3cd7faa37a0a6bb331e49e58df652a37f035552" +dependencies = [ + "bytes", + "google-cloud-auth 1.0.0", + "google-cloud-gax 1.0.0", + "google-cloud-rpc", + "http 1.2.0", + "http-body-util", + "percent-encoding", + "reqwest 0.12.23", + "rustc_version", + "serde", + "serde_json", + "thiserror 2.0.10", + "tokio", +] + [[package]] name = "google-cloud-googleapis" version = "0.12.0" @@ -2268,11 +2637,33 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04f945a208886a13d07636f38fb978da371d0abc3e34bad338124b9f8c135a8f" dependencies = [ - "reqwest 0.12.12", + "reqwest 0.12.23", "thiserror 1.0.69", "tokio", ] +[[package]] +name = "google-cloud-monitoring-v3" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fa201d8a275c591bdc79a2c04cc758ff220f5587c748787b182c9049c918a7" +dependencies = [ + "async-trait", + "bytes", + "google-cloud-api", + "google-cloud-gax 1.0.0", + "google-cloud-gax-internal", + "google-cloud-rpc", + "google-cloud-type", + "google-cloud-wkt", + "lazy_static", + "reqwest 0.12.23", + "serde", + "serde_json", + "serde_with", + "tracing", +] + [[package]] name = "google-cloud-pubsub" version = "0.22.1" @@ -2282,7 +2673,7 @@ dependencies = [ "async-channel", "async-stream", "google-cloud-auth 0.13.2", - "google-cloud-gax", + "google-cloud-gax 0.17.0", "google-cloud-googleapis", "google-cloud-token", "prost-types 0.12.6", @@ -2292,6 +2683,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "google-cloud-rpc" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa87d841e35c69efa61786e7e7462b4ebdb96df7be79775aeb698b4c0cc52e" +dependencies = [ + "bytes", + "google-cloud-wkt", + "serde", + "serde_json", + "serde_with", +] + [[package]] name = "google-cloud-storage" version = "0.22.1" @@ -2312,8 +2716,8 @@ dependencies = [ "percent-encoding", "pkcs8 0.10.2", "regex", - "reqwest 0.12.12", - "reqwest-middleware", + "reqwest 0.12.23", + "reqwest-middleware 0.3.3", "ring 0.17.8", "serde", "serde_json", @@ -2334,6 +2738,35 @@ dependencies = [ "async-trait", ] +[[package]] +name = "google-cloud-type" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a7190c5c073b273d952e6a01913302ce54f050861ead96d607089d380afdd8a" +dependencies = [ + "bytes", + "google-cloud-wkt", + "serde", + "serde_json", + "serde_with", +] + +[[package]] +name = "google-cloud-wkt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccfa292c272695eb966fe8b08c78cabb35e7775771a1c7f97b58f7dcb7bfb26" +dependencies = [ + "base64 0.22.1", + "bytes", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.10", + "time", + "url", +] + [[package]] name = "group" version = "0.12.1" @@ -2366,9 +2799,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -2498,7 +2931,7 @@ dependencies = [ "new_debug_unreachable", "once_cell", "phf 0.11.3", - "rustc-hash", + "rustc-hash 1.1.0", "triomphe", ] @@ -2547,12 +2980,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http 1.2.0", "http-body 1.0.1", "pin-project-lite", @@ -2599,7 +3032,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.8", "tokio", "tower-service", "tracing", @@ -2608,20 +3041,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.7", + "futures-core", + "h2 0.4.12", "http 1.2.0", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -2651,13 +3086,15 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", - "hyper 1.5.2", + "hyper 1.7.0", "hyper-util", - "rustls 0.23.20", + "rustls 0.23.33", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.1", + "tokio-rustls 0.26.4", "tower-service", + "webpki-roots 0.26.11", ] [[package]] @@ -2693,7 +3130,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.5.2", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio", @@ -2703,21 +3140,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.2", + "hyper 1.7.0", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2731,7 +3175,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -2948,6 +3392,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2956,6 +3403,16 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-macro" version = "0.3.7" @@ -3038,9 +3495,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -3097,9 +3554,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libloading" @@ -3203,6 +3660,12 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "malachite" version = "0.6.1" @@ -3313,6 +3776,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metrics" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" +dependencies = [ + "ahash", + "portable-atomic", +] + [[package]] name = "mime" version = "0.3.17" @@ -3434,7 +3907,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -3482,6 +3955,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3566,6 +4048,25 @@ dependencies = [ "libc", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.36.7" @@ -3674,6 +4175,17 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -3681,7 +4193,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -3692,7 +4218,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] @@ -3807,7 +4333,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ca6fdb8f9d32182abf17328789f87f305dd8c8ce5bf48c5aa2b5cffc94e1c04" dependencies = [ - "bindgen", + "bindgen 0.66.1", "cc", "fs_extra", "glob", @@ -3980,7 +4506,7 @@ dependencies = [ "log", "lru", "once_cell", - "parking_lot", + "parking_lot 0.12.3", "pingora-core", "pingora-error", "pingora-header-serde", @@ -4012,7 +4538,7 @@ dependencies = [ "daemonize", "flate2", "futures", - "h2 0.4.7", + "h2 0.4.12", "http 1.2.0", "httparse", "httpdate", @@ -4022,7 +4548,7 @@ dependencies = [ "nix", "once_cell", "openssl-probe", - "parking_lot", + "parking_lot 0.12.3", "percent-encoding", "pingora-error", "pingora-http", @@ -4036,7 +4562,7 @@ dependencies = [ "serde", "serde_yaml 0.8.26", "sfv", - "socket2", + "socket2 0.6.0", "strum", "strum_macros", "thread_local", @@ -4107,7 +4633,7 @@ dependencies = [ "pingora-http", "pingora-ketama", "pingora-runtime", - "rand 0.9.1", + "rand 0.9.2", "tokio", ] @@ -4119,8 +4645,8 @@ checksum = "cb50f65f06c4b81ccb3edcceaa54bb9439608506b0b3b8c048798169a64aad8e" dependencies = [ "arrayvec", "hashbrown 0.15.2", - "parking_lot", - "rand 0.9.1", + "parking_lot 0.12.3", + "rand 0.9.2", ] [[package]] @@ -4146,7 +4672,7 @@ dependencies = [ "crossbeam-queue", "log", "lru", - "parking_lot", + "parking_lot 0.12.3", "pingora-timeout", "thread_local", "tokio", @@ -4162,7 +4688,7 @@ dependencies = [ "bytes", "clap", "futures", - "h2 0.4.7", + "h2 0.4.12", "http 1.2.0", "log", "once_cell", @@ -4194,7 +4720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685bb8808cc1919c63a06ab14fdac9b84a4887ced49259a5c0adc8bfb2ffe558" dependencies = [ "once_cell", - "parking_lot", + "parking_lot 0.12.3", "pin-project-lite", "thread_local", "tokio", @@ -4248,6 +4774,12 @@ dependencies = [ "syn 2.0.95", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "postgres-native-tls" version = "0.5.0" @@ -4274,7 +4806,7 @@ dependencies = [ "log", "md-5", "memchr", - "rand 0.9.1", + "rand 0.9.2", "sha2", "stringprep", ] @@ -4417,7 +4949,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot", + "parking_lot 0.12.3", "protobuf", "thiserror 1.0.69", ] @@ -4433,7 +4965,7 @@ dependencies = [ "bitflags 2.9.4", "lazy_static", "num-traits", - "rand 0.9.1", + "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax 0.8.5", @@ -4570,6 +5102,61 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls 0.23.33", + "socket2 0.6.0", + "thiserror 2.0.10", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring 0.17.8", + "rustc-hash 2.1.1", + "rustls 0.23.33", + "rustls-pki-types", + "slab", + "thiserror 2.0.10", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.0", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.38" @@ -4630,9 +5217,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -4734,7 +5321,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" dependencies = [ - "rand_core 0.3.1", + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", ] [[package]] @@ -4825,6 +5421,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -4847,9 +5444,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -4857,40 +5454,56 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.7", + "h2 0.4.12", "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.7.0", "hyper-rustls 0.27.5", "hyper-tls 0.6.0", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 2.2.0", + "quinn", + "rustls 0.23.33", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", - "system-configuration 0.6.1", "tokio", "tokio-native-tls", + "tokio-rustls 0.26.4", "tokio-util", "tower 0.5.2", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "windows-registry", + "webpki-roots 1.0.2", +] + +[[package]] +name = "reqwest-middleware" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a735987236a8e238bf0296c7e351b999c188ccc11477f311b82b55c93984216" +dependencies = [ + "anyhow", + "async-trait", + "http 0.2.12", + "reqwest 0.11.27", + "serde", + "task-local-extensions", + "thiserror 1.0.69", ] [[package]] @@ -4902,12 +5515,46 @@ dependencies = [ "anyhow", "async-trait", "http 1.2.0", - "reqwest 0.12.12", + "reqwest 0.12.23", "serde", "thiserror 1.0.69", "tower-service", ] +[[package]] +name = "reqwest-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af20b65c2ee9746cc575acb6bd28a05ffc0d15e25c992a8f4462d8686aacb4f" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "futures", + "getrandom 0.2.15", + "http 0.2.12", + "hyper 0.14.32", + "parking_lot 0.11.2", + "reqwest 0.11.27", + "reqwest-middleware 0.2.5", + "retry-policies", + "task-local-extensions", + "tokio", + "tracing", + "wasm-timer", +] + +[[package]] +name = "retry-policies" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17dd00bff1d737c40dbcd47d4375281bf4c17933f9eef0a185fc7bacca23ecbd" +dependencies = [ + "anyhow", + "chrono", + "rand 0.8.5", +] + [[package]] name = "rfc6979" version = "0.3.1" @@ -5013,6 +5660,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -5076,13 +5729,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ + "aws-lc-rs", + "log", "once_cell", + "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.7", "subtle", "zeroize", ] @@ -5096,7 +5752,7 @@ dependencies = [ "openssl-probe", "rustls-pemfile 1.0.4", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] @@ -5109,7 +5765,19 @@ dependencies = [ "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", ] [[package]] @@ -5132,9 +5800,13 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] [[package]] name = "rustls-webpki" @@ -5157,6 +5829,18 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "aws-lc-rs", + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustracing" version = "0.5.1" @@ -5288,7 +5972,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.9.4", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -5296,9 +5993,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -5315,18 +6012,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -5410,7 +6117,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", "syn 2.0.95", @@ -5624,6 +6331,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -5676,7 +6393,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot", + "parking_lot 0.12.3", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -5830,7 +6547,7 @@ source = "git+https://github.com/encoredev/swc?branch=node-resolve-exports#3ccdd dependencies = [ "hstr", "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", "serde", ] @@ -5841,7 +6558,7 @@ source = "git+https://github.com/encoredev/swc?branch=node-resolve-exports#3ccdd dependencies = [ "ahash", "anyhow", - "dashmap", + "dashmap 5.5.3", "once_cell", "regex", "serde", @@ -5861,7 +6578,7 @@ dependencies = [ "new_debug_unreachable", "num-bigint", "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "siphasher 0.3.11", "string_cache", @@ -5896,7 +6613,7 @@ version = "0.45.9" source = "git+https://github.com/encoredev/swc?branch=node-resolve-exports#3ccddcb7d70380b6952296717b2d9f2056f4c2ac" dependencies = [ "anyhow", - "dashmap", + "dashmap 5.5.3", "indexmap 1.9.3", "normpath", "once_cell", @@ -5939,7 +6656,7 @@ dependencies = [ "indexmap 1.9.3", "once_cell", "phf 0.10.1", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "smallvec", "swc_atoms", @@ -5959,7 +6676,7 @@ dependencies = [ "indexmap 1.9.3", "num_cpus", "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -6092,6 +6809,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "sysinfo" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -6099,7 +6830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] @@ -6110,7 +6841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.4", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -6134,6 +6865,15 @@ dependencies = [ "libc", ] +[[package]] +name = "task-local-extensions" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba323866e5d033818e3240feeb9f7db2c4296674e4d9e16b97b7bf8f490434e8" +dependencies = [ + "pin-utils", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -6241,9 +6981,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -6256,15 +6996,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -6305,10 +7045,10 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.8", "tokio-macros", "windows-sys 0.52.0", ] @@ -6397,14 +7137,14 @@ dependencies = [ "futures-channel", "futures-util", "log", - "parking_lot", + "parking_lot 0.12.3", "percent-encoding", "phf 0.11.3", "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.9.1", - "socket2", + "rand 0.9.2", + "socket2 0.5.8", "tokio", "tokio-util", "whoami", @@ -6455,11 +7195,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.20", + "rustls 0.23.33", "tokio", ] @@ -6593,7 +7333,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", - "webpki-roots", + "webpki-roots 0.25.4", ] [[package]] @@ -6657,6 +7397,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -6971,6 +7729,23 @@ name = "uuid" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom 0.2.15", + "rand 0.8.5", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9384a660318abfbd7f8932c34d67e4d1ec511095f95972ddc01e19d7ba8413f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", +] [[package]] name = "valuable" @@ -7089,20 +7864,22 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -7114,9 +7891,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -7127,9 +7904,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7137,9 +7914,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -7150,9 +7927,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -7167,11 +7947,36 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -7193,6 +7998,24 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -7211,7 +8034,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall", + "redox_syscall 0.5.8", "wasite", "web-sys", ] @@ -7257,6 +8080,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -7267,33 +8112,94 @@ dependencies = [ ] [[package]] -name = "windows-registry" +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ + "windows-link", "windows-result", "windows-strings", - "windows-targets 0.52.6", ] [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -7354,6 +8260,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" diff --git a/proto/gen.sh b/proto/gen.sh index 1f0a7e0ef4..e522aeaf31 100755 --- a/proto/gen.sh +++ b/proto/gen.sh @@ -31,3 +31,9 @@ protoc -I . --go_out=. --go_opt=$GO_OPT \ protoc -I . --go_out=. --go_opt=$GO_OPT \ ./encore/runtime/v1/secretdata.proto + +# Prometheus protos for metrics exporter +protoc -I . --go_out=../runtimes/go/appruntime/infrasdk/metrics/prometheus --go_opt=$GO_OPT \ +./prompb/types.proto +protoc -I . --go_out=../runtimes/go/appruntime/infrasdk/metrics/prometheus --go_opt=$GO_OPT \ +./prompb/remote.proto diff --git a/runtimes/go/appruntime/infrasdk/metrics/prometheus/prompb/remote.proto b/proto/prompb/remote.proto similarity index 71% rename from runtimes/go/appruntime/infrasdk/metrics/prometheus/prompb/remote.proto rename to proto/prompb/remote.proto index 3c791320e4..7e13bab79e 100644 --- a/runtimes/go/appruntime/infrasdk/metrics/prometheus/prompb/remote.proto +++ b/proto/prompb/remote.proto @@ -16,13 +16,13 @@ package prometheus; option go_package = "encore.dev/appruntime/metrics/prometheus/prompb"; -import "types.proto"; +import "prompb/types.proto"; message WriteRequest { repeated prometheus.TimeSeries timeseries = 1; // Cortex uses this field to determine the source of the write request. // We reserve it to avoid any compatibility issues. - reserved 2; + reserved 2; repeated prometheus.MetricMetadata metadata = 3; } @@ -31,8 +31,9 @@ message ReadRequest { repeated Query queries = 1; enum ResponseType { - // Server will return a single ReadResponse message with matched series that includes list of raw samples. - // It's recommended to use streamed response types instead. + // Server will return a single ReadResponse message with matched series that + // includes list of raw samples. It's recommended to use streamed response + // types instead. // // Response headers: // Content-Type: "application/x-protobuf" @@ -44,16 +45,18 @@ message ReadRequest { // uint32 for CRC32 Castagnoli checksum. // // Response headers: - // Content-Type: "application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse" - // Content-Encoding: "" + // Content-Type: "application/x-streamed-protobuf; + // proto=prometheus.ChunkedReadResponse" Content-Encoding: "" STREAMED_XOR_CHUNKS = 1; } - // accepted_response_types allows negotiating the content type of the response. + // accepted_response_types allows negotiating the content type of the + // response. // - // Response types are taken from the list in the FIFO order. If no response type in `accepted_response_types` is - // implemented by server, error is returned. - // For request that do not contain `accepted_response_types` field the SAMPLES response type will be used. + // Response types are taken from the list in the FIFO order. If no response + // type in `accepted_response_types` is implemented by server, error is + // returned. For request that do not contain `accepted_response_types` field + // the SAMPLES response type will be used. repeated ResponseType accepted_response_types = 2; } @@ -75,13 +78,16 @@ message QueryResult { repeated prometheus.TimeSeries timeseries = 1; } -// ChunkedReadResponse is a response when response_type equals STREAMED_XOR_CHUNKS. -// We strictly stream full series after series, optionally split by time. This means that a single frame can contain -// partition of the single series, but once a new series is started to be streamed it means that no more chunks will -// be sent for previous one. Series are returned sorted in the same way TSDB block are internally. +// ChunkedReadResponse is a response when response_type equals +// STREAMED_XOR_CHUNKS. We strictly stream full series after series, optionally +// split by time. This means that a single frame can contain partition of the +// single series, but once a new series is started to be streamed it means that +// no more chunks will be sent for previous one. Series are returned sorted in +// the same way TSDB block are internally. message ChunkedReadResponse { repeated prometheus.ChunkedSeries chunked_series = 1; - // query_index represents an index of the query from ReadRequest.queries these chunks relates to. + // query_index represents an index of the query from ReadRequest.queries these + // chunks relates to. int64 query_index = 2; } diff --git a/runtimes/go/appruntime/infrasdk/metrics/prometheus/prompb/types.proto b/proto/prompb/types.proto similarity index 100% rename from runtimes/go/appruntime/infrasdk/metrics/prometheus/prompb/types.proto rename to proto/prompb/types.proto diff --git a/runtimes/core/Cargo.toml b/runtimes/core/Cargo.toml index e1695dd0c1..650544cfb9 100644 --- a/runtimes/core/Cargo.toml +++ b/runtimes/core/Cargo.toml @@ -10,6 +10,7 @@ rttrace = [] [dependencies] pingora = { version = "0.4", features = ["lb", "openssl"] } anyhow = "1.0.76" +async-trait = "0.1" base64 = "0.21.5" gjson = "0.8.1" prost = "0.12.3" @@ -123,6 +124,15 @@ email_address = "0.2.9" cookie = "0.18.1" malachite = "0.6.1" byteorder = "1.5.0" +metrics = "0.24.2" +dashmap = "6.1.0" +google-cloud-monitoring-v3 = "1.0.0" +google-cloud-api = "1.0.0" +google-cloud-wkt = "1.0.0" +sysinfo = "0.37.2" +aws-sdk-cloudwatch = "1.94.0" +datadog-api-client = "0.20.0" +snap = "1.1.1" [build-dependencies] prost-build = "0.12.3" diff --git a/runtimes/core/build.rs b/runtimes/core/build.rs index 94627339df..9700f87c2b 100644 --- a/runtimes/core/build.rs +++ b/runtimes/core/build.rs @@ -5,6 +5,7 @@ fn main() -> std::io::Result<()> { &[ "../../proto/encore/runtime/v1/runtime.proto", "../../proto/encore/parser/meta/v1/meta.proto", + "../../proto/prompb/remote.proto", ], &["../../proto/"], )?; diff --git a/runtimes/core/src/api/auth/local.rs b/runtimes/core/src/api/auth/local.rs index a9c88fad64..3eef2ea931 100644 --- a/runtimes/core/src/api/auth/local.rs +++ b/runtimes/core/src/api/auth/local.rs @@ -1,3 +1,5 @@ +use crate::metrics::counter; + use crate::api::auth::{AuthHandler, AuthPayload, AuthRequest, AuthResponse}; use crate::api::schema::encoding::Schema; use crate::api::{APIResult, HandlerResponse, HandlerResponseInner, PValues}; @@ -14,6 +16,7 @@ pub struct LocalAuthHandler { pub schema: Schema, pub handler: RwLock>>, pub tracer: Tracer, + pub requests_total: counter::Schema, } impl LocalAuthHandler { @@ -152,7 +155,9 @@ impl AuthHandler for LocalAuthHandler { user_id: auth_uid.clone(), })), }; + self.tracer.request_span_end(&model_resp, false); + self.requests_total.with([("code", "ok")]).increment(); Ok(AuthResponse::Authenticated { auth_uid, auth_data, @@ -165,6 +170,9 @@ impl AuthHandler for LocalAuthHandler { data: model::ResponseData::Auth(Err(e.clone())), }; self.tracer.request_span_end(&model_resp, false); + self.requests_total + .with([("code", e.code.to_string())]) + .increment(); Err(e) } } diff --git a/runtimes/core/src/api/endpoint.rs b/runtimes/core/src/api/endpoint.rs index 83f8697f03..5ba8ba7545 100644 --- a/runtimes/core/src/api/endpoint.rs +++ b/runtimes/core/src/api/endpoint.rs @@ -23,6 +23,7 @@ use crate::api::{jsonschema, schema, ErrCode, Error}; use crate::encore::parser::meta::v1::rpc; use crate::encore::parser::meta::v1::{self as meta, selector}; use crate::log::LogFromRust; +use crate::metrics::counter; use crate::model::StreamDirection; use crate::names::EndpointName; use crate::trace; @@ -406,6 +407,7 @@ pub(super) struct EndpointHandler { pub endpoint: Arc, pub handler: Arc, pub shared: Arc, + pub requests_total: counter::Schema, } #[derive(Debug)] @@ -427,6 +429,7 @@ impl Clone for EndpointHandler { endpoint: self.endpoint.clone(), handler: self.handler.clone(), shared: self.shared.clone(), + requests_total: self.requests_total.clone(), } } } @@ -601,7 +604,6 @@ impl EndpointHandler { let duration = tokio::time::Instant::now().duration_since(request.start); // If we had a request failure, log that separately. - if let ResponseData::Typed(Err(err)) = &resp { logger.error(Some(&request), "request failed", Some(err), { let mut fields = crate::log::Fields::new(); @@ -629,6 +631,12 @@ impl EndpointHandler { }); } + let code = match &resp { + ResponseData::Typed(Ok(_)) => "ok".to_string(), + ResponseData::Typed(Err(err)) => err.code.to_string(), + ResponseData::Raw(resp) => ErrCode::from(resp.status()).to_string(), + }; + logger.info(Some(&request), "request completed", { let mut fields = crate::log::Fields::new(); let dur_ms = (duration.as_secs() as f64 * 1000f64) @@ -644,13 +652,7 @@ impl EndpointHandler { )), ); - let code = match &resp { - ResponseData::Typed(Ok(_)) => "ok".to_string(), - ResponseData::Typed(Err(err)) => err.code.to_string(), - ResponseData::Raw(resp) => ErrCode::from(resp.status()).to_string(), - }; - - fields.insert("code".into(), serde_json::Value::String(code)); + fields.insert("code".into(), serde_json::Value::String(code.clone())); Some(fields) }); @@ -685,6 +687,7 @@ impl EndpointHandler { }), }; self.shared.tracer.request_span_end(&model_resp, sensitive); + self.requests_total.with([("code", code)]).increment(); } if let Ok(val) = HeaderValue::from_str(request.span.0.serialize_encore().as_str()) { diff --git a/runtimes/core/src/api/manager.rs b/runtimes/core/src/api/manager.rs index f4f2fe2fb4..49343d21f3 100644 --- a/runtimes/core/src/api/manager.rs +++ b/runtimes/core/src/api/manager.rs @@ -19,7 +19,7 @@ use crate::api::{ use crate::encore::parser::meta::v1 as meta; use crate::encore::runtime::v1 as runtime; use crate::trace::Tracer; -use crate::{api, model, pubsub, secrets, EncoreName, EndpointName, Hosted}; +use crate::{api, metrics, model, pubsub, secrets, EncoreName, EndpointName, Hosted}; use super::encore_routes::healthz; use super::websocket_client::WebSocketClient; @@ -43,6 +43,7 @@ pub struct ManagerConfig<'a> { pub runtime: tokio::runtime::Handle, pub testing: bool, pub proxied_push_subs: HashMap, + pub metrics: &'a metrics::Manager, } pub struct Manager { @@ -57,6 +58,7 @@ pub struct Manager { gateways: HashMap, testing: bool, + metrics: metrics::Manager, } impl ManagerConfig<'_> { @@ -164,6 +166,7 @@ impl ManagerConfig<'_> { &service_registry, self.http_client.clone(), self.tracer.clone(), + self.metrics.registry(), ) .context("unable to build authenticator")?; @@ -200,6 +203,7 @@ impl ManagerConfig<'_> { inbound_svc_auth, self.tracer.clone(), auth_data_schemas, + Arc::clone(self.metrics.registry()), ) .context("unable to create API server")?; Some(server) @@ -217,6 +221,7 @@ impl ManagerConfig<'_> { runtime: self.runtime, healthz: healthz_handler, testing: self.testing, + metrics: self.metrics.clone(), }) } } @@ -245,6 +250,7 @@ fn build_auth_handler( service_registry: &ServiceRegistry, http_client: reqwest::Client, tracer: Tracer, + metrics_registry: &Arc, ) -> anyhow::Result> { let Some(explicit) = &gw.explicit else { return Ok(None); @@ -283,6 +289,8 @@ fn build_auth_handler( // let is_local = hosted_services.contains(&explicit.service_name); let is_local = true; let name = EndpointName::new(explicit.service_name.clone(), auth.name.clone()); + let requests_total = + metrics::requests_total_counter(metrics_registry, &explicit.service_name, &auth.name); let auth_data = registry.schema(auth_data_schema_idx); let auth_handler = if is_local { @@ -294,6 +302,7 @@ fn build_auth_handler( schema, handler: Default::default(), tracer, + requests_total, }, )? } else { @@ -330,6 +339,10 @@ impl Manager { self.service_registry.endpoints() } + pub fn metrics_registry(&self) -> &Arc { + self.metrics.registry() + } + pub fn stream( &self, endpoint_name: EndpointName, diff --git a/runtimes/core/src/api/server.rs b/runtimes/core/src/api/server.rs index 3c922fbf49..b06d920985 100644 --- a/runtimes/core/src/api/server.rs +++ b/runtimes/core/src/api/server.rs @@ -37,6 +37,9 @@ pub struct Server { /// Data shared between all endpoints. shared: Arc, + + /// Metrics registry for creating metrics + metrics_registry: Arc, } impl Server { @@ -47,6 +50,7 @@ impl Server { inbound_svc_auth: Vec>, tracer: trace::Tracer, auth_data_schemas: HashMap>, + metrics_registry: Arc, ) -> anyhow::Result { // Register the routes, and track the handlers in a map so we can easily // set the request handler when registered. @@ -92,11 +96,17 @@ impl Server { // For static asset routes, configure the static asset handler directly. // There's no need to defer it for dynamic runtime registration. let static_handler = StaticAssetsHandler::new(assets); + let requests_total = crate::metrics::requests_total_counter( + &metrics_registry, + ep.name.service(), + ep.name.endpoint(), + ); let handler = EndpointHandler { endpoint: ep.clone(), handler: Arc::new(static_handler), shared: shared.clone(), + requests_total, }; server_handler.set(handler); } @@ -130,6 +140,7 @@ impl Server { hosted_endpoints: Mutex::new(handler_map), router: Mutex::new(Some(router)), shared, + metrics_registry, }) } @@ -153,11 +164,17 @@ impl Server { None => Ok(()), // anyhow::bail!("no handler found for endpoint: {}", endpoint_name), Some(h) => { let endpoint = self.endpoints.get(&endpoint_name).unwrap().to_owned(); + let requests_total = crate::metrics::requests_total_counter( + &self.metrics_registry, + endpoint.name.service(), + endpoint.name.endpoint(), + ); let handler = EndpointHandler { endpoint, handler, shared: self.shared.clone(), + requests_total, }; h.add(handler); diff --git a/runtimes/core/src/lib.rs b/runtimes/core/src/lib.rs index 150cd6e126..8cc24122d4 100644 --- a/runtimes/core/src/lib.rs +++ b/runtimes/core/src/lib.rs @@ -24,6 +24,8 @@ pub mod error; pub mod infracfg; pub mod log; pub mod meta; +pub mod metadata; +pub mod metrics; pub mod model; mod names; pub mod objects; @@ -209,6 +211,7 @@ pub struct Runtime { app_meta: meta::AppMeta, compute: ComputeConfig, runtime: tokio::runtime::Runtime, + metrics: metrics::Manager, } impl Runtime { @@ -238,6 +241,7 @@ impl Runtime { let mut deployment = cfg.deployment.take().unwrap_or_default(); let service_discovery = deployment.service_discovery.take().unwrap_or_default(); + let observability = deployment.observability.take().unwrap_or_default(); let http_client = reqwest::Client::builder() .build() @@ -250,11 +254,19 @@ impl Runtime { ); let platform_validator = Arc::new(platform_validator); + // Initialize metrics manager from runtime config + let metrics_manager = metrics::Manager::from_runtime_config( + &observability, + &environment, + &secrets, + &http_client, + tokio_rt.handle().clone(), + ); + // Set up observability. let disable_tracing = testing || std::env::var("ENCORE_NOTRACE").is_ok_and(|v| !v.is_empty()); let tracer = if !disable_tracing { - let observability = deployment.observability.take().unwrap_or_default(); let trace_endpoint = observability .tracing .into_iter() @@ -388,6 +400,7 @@ impl Runtime { runtime: tokio_rt.handle().clone(), testing, proxied_push_subs, + metrics: &metrics_manager, } .build() .context("unable to initialize api manager")?; @@ -411,6 +424,7 @@ impl Runtime { app_meta, compute, runtime: tokio_rt, + metrics: metrics_manager, }) } @@ -444,6 +458,11 @@ impl Runtime { &self.api } + #[inline] + pub fn metrics(&self) -> &metrics::Manager { + &self.metrics + } + #[inline] pub fn endpoints(&self) -> &api::EndpointMap { self.api.endpoints() diff --git a/runtimes/core/src/metadata/aws.rs b/runtimes/core/src/metadata/aws.rs new file mode 100644 index 0000000000..bff93a072d --- /dev/null +++ b/runtimes/core/src/metadata/aws.rs @@ -0,0 +1,50 @@ +use std::time::Duration; + +use anyhow::Context; + +#[derive(serde::Deserialize)] +pub struct AwsTaskMeta { + #[serde(rename = "ServiceName")] + pub service_name: String, + #[serde(rename = "Revision")] + pub revision: String, + #[serde(rename = "TaskARN")] + pub task_arn: String, +} + +#[derive(Debug)] +pub struct AwsMetadataClient { + http_client: reqwest::Client, + metadata_uri: String, +} + +impl AwsMetadataClient { + pub fn new(http_client: reqwest::Client, metadata_uri: String) -> Self { + AwsMetadataClient { + http_client, + metadata_uri, + } + } + + pub async fn fetch_task_meta(&self) -> anyhow::Result { + let req = self + .http_client + .get(format!("{}/task", self.metadata_uri)) + .timeout(Duration::from_secs(30)) + .build() + .context("create metadata request")?; + + let resp = self + .http_client + .execute(req) + .await + .context("send metadata request")?; + + let task_meta = resp + .json::() + .await + .context("deserialize task metadata")?; + + Ok(task_meta) + } +} diff --git a/runtimes/core/src/metadata/gce.rs b/runtimes/core/src/metadata/gce.rs new file mode 100644 index 0000000000..c778b9bcaf --- /dev/null +++ b/runtimes/core/src/metadata/gce.rs @@ -0,0 +1,115 @@ +//! GCE metadata client implementation based on golangs "cloud.google.com/go/compute/metadata" + +use std::time::Duration; +use tokio::sync::OnceCell; + +#[derive(Debug, thiserror::Error)] +pub enum GceMetadataError { + #[error("HTTP request failed: {0}")] + HttpRequest(#[from] reqwest::Error), + #[error("GCE metadata not defined (404)")] + NotDefined, + #[error("GCE metadata server temporarily unavailable (503)")] + ServiceUnavailable, + #[error("GCE metadata server returned error status: {status}")] + HttpStatus { status: reqwest::StatusCode }, + #[error("Failed to read response body: {0}")] + ResponseBody(reqwest::Error), +} + +// Default metadata server IP as documented by Google +const METADATA_IP: &str = "169.254.169.254"; +// Env to override metadata server host, if not set the METADATA_IP will be used +const METADATA_HOST_ENV: &str = "GCE_METADATA_HOST"; +const METADATA_FLAVOR_HEADER: &str = "Metadata-Flavor"; +const GOOGLE_HEADER_VALUE: &str = "Google"; +const USER_AGENT: &str = "encore-runtime/0.1.0"; +// Timeout and retry configuration +const REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const MAX_RETRIES: usize = 3; + +// Global cache for instance ID to ensure it's shared across all calls +static INSTANCE_ID_CACHE: OnceCell = OnceCell::const_new(); + +#[derive(Debug)] +pub struct GceMetadataClient { + http_client: reqwest::Client, +} + +impl GceMetadataClient { + pub fn new(http_client: reqwest::Client) -> Self { + Self { http_client } + } + + /// Build metadata URL, checking GCE_METADATA_HOST environment variable first + fn build_metadata_url(path: &str) -> String { + let host = std::env::var(METADATA_HOST_ENV).unwrap_or_else(|_| METADATA_IP.to_string()); + let path = path.trim_start_matches('/'); // Remove leading slashes like Go does + format!("http://{}/computeMetadata/v1/{}", host, path) + } + + /// Get the instance ID from GCE metadata server, with global caching + /// This ensures only one HTTP request is made even with concurrent calls + pub async fn instance_id(&self) -> Result { + let instance_id = INSTANCE_ID_CACHE + .get_or_try_init(|| async { self.fetch_metadata("instance/id").await }) + .await?; + + Ok(instance_id.clone()) + } + + /// Fetch metadata from the GCE metadata server + pub async fn fetch_metadata(&self, path: &str) -> Result { + let url = Self::build_metadata_url(path); + + for attempt in 1..=MAX_RETRIES { + let result = self.try_fetch(&url).await; + + match &result { + Ok(_) => { + return result; + } + Err(e) if attempt == MAX_RETRIES => { + log::error!( + "Failed to fetch GCE metadata after {} attempts: {}", + MAX_RETRIES, + e + ); + return result; + } + Err(e) => { + log::warn!("Attempt {} failed: {}, retrying...", attempt, e); + tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await; + } + } + } + + unreachable!("unexpected failure fetching metadata") + } + + async fn try_fetch(&self, url: &str) -> Result { + let response = self + .http_client + .get(url) + .header(METADATA_FLAVOR_HEADER, GOOGLE_HEADER_VALUE) + .header(http::header::USER_AGENT, USER_AGENT) + .timeout(REQUEST_TIMEOUT) + .send() + .await?; + + let status = response.status(); + if status.is_success() { + let text = response + .text() + .await + .map_err(GceMetadataError::ResponseBody)?; + Ok(text.trim().to_string()) + } else if status == reqwest::StatusCode::NOT_FOUND { + Err(GceMetadataError::NotDefined) + } else if status == reqwest::StatusCode::SERVICE_UNAVAILABLE { + Err(GceMetadataError::ServiceUnavailable) + } else { + Err(GceMetadataError::HttpStatus { status }) + } + } +} diff --git a/runtimes/core/src/metadata/mod.rs b/runtimes/core/src/metadata/mod.rs new file mode 100644 index 0000000000..fb444ce7b0 --- /dev/null +++ b/runtimes/core/src/metadata/mod.rs @@ -0,0 +1,157 @@ +use std::collections::HashMap; + +use crate::{ + encore::runtime::v1::{environment::Cloud, Environment}, + metadata::aws::AwsMetadataClient, +}; +use anyhow::Context; +use tokio::sync::OnceCell; + +mod aws; +mod gce; + +#[derive(Debug)] +pub struct ContainerMetaClient { + cell: OnceCell, + env: Environment, + http_client: reqwest::Client, + fallback: ContainerMetadata, +} + +impl ContainerMetaClient { + pub fn new(env: Environment, http_client: reqwest::Client) -> Self { + Self { + cell: OnceCell::new(), + fallback: ContainerMetadata { + env_name: env.env_name.clone(), + ..Default::default() + }, + env, + http_client, + } + } + + pub async fn collect(&self) -> anyhow::Result<&ContainerMetadata> { + self.cell + .get_or_try_init(|| async { + ContainerMetadata::collect(&self.env, &self.http_client).await + }) + .await + } + + pub fn fallback(&self) -> &ContainerMetadata { + &self.fallback + } +} + +#[derive(Debug, Clone, Default)] +pub struct ContainerMetadata { + pub service_id: String, + pub revision_id: String, + pub instance_id: String, + pub env_name: String, +} + +impl ContainerMetadata { + pub fn labels(&self) -> Vec<(String, String)> { + vec![ + ("service_id".to_string(), self.service_id.clone()), + ("revision_id".to_string(), self.revision_id.clone()), + ("instance_id".to_string(), self.instance_id.clone()), + ("env_name".to_string(), self.env_name.clone()), + ] + } + + pub async fn collect(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result { + match env.cloud() { + Cloud::Gcp | Cloud::Encore => Self::collect_gcp(env, http_client).await, + Cloud::Aws => Self::collect_aws(env, http_client).await, + Cloud::Azure | Cloud::Unspecified | Cloud::Local => anyhow::bail!( + "can't collect container meta in {}", + env.cloud().as_str_name() + ), + } + } + + async fn collect_aws(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result { + // Encore supports running on both ECS Fargate and EKS. + // For Fargate, we can get the metadata from the ECS metadata service. + // For EKS there doesn't appear to be a standard way to get the metadata, so skip it in that case. + let metadata_uri = std::env::var("ECS_CONTAINER_METADATA_URI_V4") + .map_err(|_| anyhow::anyhow!("unable to get ecs container metadata uri"))?; + + let client = AwsMetadataClient::new(http_client.clone(), metadata_uri); + let task_meta = client.fetch_task_meta().await?; + + let instance_id = task_meta + .task_arn + .get(task_meta.task_arn.len().saturating_sub(8)..) + .unwrap_or(&task_meta.task_arn) + .to_string(); + + Ok(Self { + service_id: task_meta.service_name, + revision_id: task_meta.revision, + instance_id, + env_name: env.env_name.clone(), + }) + } + + async fn collect_gcp(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result { + let service = std::env::var("K_SERVICE").map_err(|_| { + anyhow::anyhow!("unable to get service ID: env variable 'K_SERVICE' unset") + })?; + + let revision = std::env::var("K_REVISION").map_err(|_| { + anyhow::anyhow!("unable to get revision ID: env variable 'K_REVISION' unset") + })?; + + let revision = revision + .strip_prefix(&format!("{}-", service)) + .unwrap_or(&revision) + .to_string(); + + let instance_id = match std::env::var("K_POD") { + Ok(pod_id) => { + // If we have a K8s POD name, take the last part of it which is the random pod ID + // On GKE, the InstanceID appears to be the Node, so if the multiple replicas are running + // on the same InstanceID then we'd have a collision. This is unlikely, but possible - + // hence why we use the pod ID instead. + pod_id + .rsplit('-') + .next() + .ok_or_else(|| anyhow::anyhow!("invalid instance ID '{}'", pod_id))? + .to_string() + } + Err(_) => { + // If we don't have a K8s POD name, we're running on Cloud Run and can get the instance ID from the metadata server + let metadata_client = gce::GceMetadataClient::new(http_client.clone()); + + metadata_client + .instance_id() + .await + .context("failed to get instance ID from GCE metadata server")? + } + }; + + Ok(Self { + service_id: service, + revision_id: revision, + instance_id, + env_name: env.env_name.clone(), + }) + } +} + +/// Process environment variable substitution in labels +/// Replaces $ENV:VARIABLE_NAME with the actual environment variable value +pub fn process_env_substitution(labels: &mut HashMap) { + for (_, value) in labels.iter_mut() { + if value.starts_with("$ENV:") { + let env_var = &value[5..]; + if let Ok(env_value) = std::env::var(env_var) { + *value = env_value; + } + } + } +} diff --git a/runtimes/core/src/metrics/atomic.rs b/runtimes/core/src/metrics/atomic.rs new file mode 100644 index 0000000000..5531407da0 --- /dev/null +++ b/runtimes/core/src/metrics/atomic.rs @@ -0,0 +1,82 @@ +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; + +use crate::metrics::{CounterOps, GaugeOps}; + +impl CounterOps for AtomicU64 { + fn increment(&self, value: u64) { + self.fetch_add(value, Ordering::Release); + } + + fn get(&self) -> crate::metrics::MetricValue { + crate::metrics::MetricValue::CounterU64(self.load(Ordering::Acquire)) + } +} + +impl CounterOps for AtomicU64 { + fn increment(&self, value: i64) { + self.fetch_add(value as u64, Ordering::Release); + } + + fn get(&self) -> crate::metrics::MetricValue { + crate::metrics::MetricValue::CounterI64(self.load(Ordering::Acquire) as i64) + } +} + +impl CounterOps for Arc +where + AtomicU64: CounterOps, +{ + fn increment(&self, value: T) { + CounterOps::::increment(&(**self), value) + } + + fn get(&self) -> crate::metrics::MetricValue { + CounterOps::::get(&(**self)) + } +} + +impl GaugeOps for AtomicU64 { + fn set(&self, value: u64) { + self.swap(value, Ordering::AcqRel); + } + + fn get(&self) -> crate::metrics::MetricValue { + crate::metrics::MetricValue::GaugeU64(self.load(Ordering::Acquire)) + } +} + +impl GaugeOps for AtomicU64 { + fn set(&self, value: i64) { + self.swap(value as u64, Ordering::AcqRel); + } + + fn get(&self) -> crate::metrics::MetricValue { + crate::metrics::MetricValue::GaugeI64(self.load(Ordering::Acquire) as i64) + } +} + +impl GaugeOps for AtomicU64 { + fn set(&self, value: f64) { + self.swap(value.to_bits(), Ordering::AcqRel); + } + + fn get(&self) -> crate::metrics::MetricValue { + crate::metrics::MetricValue::GaugeF64(f64::from_bits(self.load(Ordering::Acquire))) + } +} + +impl GaugeOps for Arc +where + AtomicU64: GaugeOps, +{ + fn set(&self, value: T) { + GaugeOps::::set(&(**self), value) + } + + fn get(&self) -> crate::metrics::MetricValue { + GaugeOps::::get(&(**self)) + } +} diff --git a/runtimes/core/src/metrics/counter.rs b/runtimes/core/src/metrics/counter.rs new file mode 100644 index 0000000000..d85ced1a08 --- /dev/null +++ b/runtimes/core/src/metrics/counter.rs @@ -0,0 +1,214 @@ +use malachite::base::num::basic::traits::One; + +use crate::metrics; +use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; + +pub trait CounterOps { + fn increment(&self, value: T); + fn get(&self) -> crate::metrics::MetricValue; +} + +/// A typed counter that can be incremented +/// T must be compatible with CounterOps for type-safe operations +pub struct Counter { + atomic: Arc, + _phantom: PhantomData, +} + +impl Counter +where + Arc: CounterOps, + T: One, +{ + /// Create a new counter with the given atomic storage + /// This is typically called by Registry, not directly by users + pub(crate) fn new(atomic: Arc) -> Self { + Self { + atomic, + _phantom: PhantomData, + } + } + + /// Increment the counter by 1 + pub fn increment(&self) { + CounterOps::increment(&self.atomic, T::ONE); + } + + /// Get the current value of the counter + pub fn get(&self) -> metrics::MetricValue { + CounterOps::get(&self.atomic) + } +} + +/// A counter schema that defines static labels and required dynamic label keys +/// Validates dynamic labels at increment time and creates separate time series +/// for each unique combination of static + dynamic labels +#[derive(Clone, Debug)] +pub struct Schema { + name: String, + static_labels: Vec<(String, String)>, + required_dynamic_keys: HashSet, + registry: Arc, + _phantom: PhantomData, +} + +impl Schema +where + Arc: CounterOps, + T: One + Send + Sync + 'static, +{ + /// Create a new counter schema + pub(crate) fn new( + name: String, + static_labels: Vec<(String, String)>, + required_dynamic_keys: HashSet, + registry: Arc, + ) -> Self { + Self { + name, + static_labels, + required_dynamic_keys, + registry, + _phantom: PhantomData, + } + } + + pub fn with(&self, dynamic_labels: L) -> Counter + where + L: IntoIterator, + K: Into, + V: Into, + { + // Convert dynamic_labels to HashMap first + let dynamic_labels_map: HashMap = dynamic_labels + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + + // Validate required keys are present + let missing: Vec = self + .required_dynamic_keys + .iter() + .filter(|key| !dynamic_labels_map.contains_key(*key)) + .cloned() + .collect(); + + if !missing.is_empty() { + log::warn!( + "missing required dynamic labels for metric '{}': {:?}, required keys: {:?}", + self.name, + missing, + self.required_dynamic_keys + ); + } + + self.get_or_create_counter(dynamic_labels_map) + } + + /// Increment the counter with the given dynamic labels + pub fn increment(&self) + where + T: One, + { + if !self.required_dynamic_keys.is_empty() { + log::warn!( + "incrementing counter '{}' without required dynamic labels, required keys: {:?}", + self.name, + self.required_dynamic_keys + ); + } + + self.get_or_create_counter(HashMap::new()).increment(); + } + + /// Get or create a counter for the given dynamic labels + fn get_or_create_counter(&self, dynamic_labels: HashMap) -> Counter { + // Create merged labels (static + dynamic) + let mut merged_labels = self.static_labels.clone(); + for (key, value) in dynamic_labels { + merged_labels.push((key, value)); + } + + self.registry.get_or_create_counter( + &self.name, + merged_labels.iter().map(|(k, v)| (k.as_str(), v.as_str())), + ) + } +} + +/// Builder for creating counter schemas with static labels and required dynamic keys +pub struct CounterSchemaBuilder { + name: String, + static_labels: Vec<(String, String)>, + required_dynamic_keys: HashSet, + registry: Arc, + _phantom: std::marker::PhantomData, +} + +impl CounterSchemaBuilder +where + Arc: CounterOps, + T: One + Send + Sync + 'static, +{ + pub(crate) fn new(name: String, registry: Arc) -> Self { + Self { + name, + static_labels: Vec::new(), + required_dynamic_keys: HashSet::new(), + registry, + _phantom: std::marker::PhantomData, + } + } + + /// Add static labels that are set once when the schema is created + pub fn static_labels(mut self, labels: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + for (key, value) in labels { + self.static_labels + .push((key.as_ref().to_string(), value.as_ref().to_string())); + } + self + } + + /// Add a single static label + pub fn static_label(mut self, key: &str, value: &str) -> Self { + self.static_labels + .push((key.to_string(), value.to_string())); + self + } + + /// Specify required dynamic label keys that must be provided at increment time + pub fn require_dynamic_keys(mut self, keys: I) -> Self + where + I: IntoIterator, + K: AsRef, + { + for key in keys { + self.required_dynamic_keys.insert(key.as_ref().to_string()); + } + self + } + + /// Add a single required dynamic key + pub fn require_dynamic_key(mut self, key: &str) -> Self { + self.required_dynamic_keys.insert(key.to_string()); + self + } + + /// Build the counter schema + pub fn build(self) -> Schema { + Schema::new( + self.name, + self.static_labels, + self.required_dynamic_keys, + self.registry, + ) + } +} diff --git a/runtimes/core/src/metrics/exporter/aws.rs b/runtimes/core/src/metrics/exporter/aws.rs new file mode 100644 index 0000000000..13b544c619 --- /dev/null +++ b/runtimes/core/src/metrics/exporter/aws.rs @@ -0,0 +1,195 @@ +use crate::metadata::ContainerMetaClient; +use crate::metrics::exporter::Exporter; +use crate::metrics::{CollectedMetric, MetricValue}; +use anyhow::Context; +use aws_sdk_cloudwatch as cloudwatch; +use aws_sdk_cloudwatch::types::{Dimension, MetricDatum}; +use std::sync::Arc; +use std::time::SystemTime; + +#[derive(Debug)] +pub struct Aws { + client: Arc, + namespace: String, + container_meta_client: ContainerMetaClient, + container_dims: tokio::sync::OnceCell>>, +} + +#[derive(Debug)] +struct LazyCloudWatchClient { + cell: tokio::sync::OnceCell>, +} + +impl LazyCloudWatchClient { + fn new() -> Self { + Self { + cell: tokio::sync::OnceCell::new(), + } + } + + async fn get(&self) -> &anyhow::Result { + self.cell + .get_or_init(|| async { + let config = aws_config::defaults(aws_config::BehaviorVersion::v2025_08_07()) + .load() + .await; + + Ok(cloudwatch::Client::new(&config)) + }) + .await + } +} + +impl Aws { + pub fn new(namespace: String, container_meta_client: ContainerMetaClient) -> Self { + Self { + client: Arc::new(LazyCloudWatchClient::new()), + namespace, + container_meta_client, + container_dims: tokio::sync::OnceCell::new(), + } + } + + async fn export_metrics(&self, metrics: Vec) -> anyhow::Result<()> { + if metrics.is_empty() { + return Ok(()); + } + + let client = match self.client.get().await { + Ok(client) => client, + Err(e) => { + log::error!("failed to get CloudWatch client: {}", e); + return Err(anyhow::anyhow!("failed to get CloudWatch client: {}", e)); + } + }; + + log::trace!( + "Exporting {} metrics to AWS CloudWatch namespace {}", + metrics.len(), + self.namespace + ); + + let metric_data = self.get_metric_data(metrics).await; + + // Send metrics in batches (CloudWatch allows up to 1000 metrics per request) + for batch in metric_data.chunks(1000) { + if let Err(e) = self.send_metric_batch(client, batch.to_vec()).await { + log::error!("failed to export metrics batch: {}", e); + } + } + + Ok(()) + } + + async fn container_dimensions(&self) -> Arc> { + self.container_dims + .get_or_try_init(|| async { + let container_metadata = self.container_meta_client.collect().await?; + anyhow::Ok(Arc::new( + container_metadata + .labels() + .into_iter() + .map(|(key, value)| Dimension::builder().name(key).value(value).build()) + .collect(), + )) + }) + .await + .map(Arc::clone) + .unwrap_or_else(|e| { + log::warn!("failed fetching container metadata: {e}, using fallback"); + Arc::new( + self.container_meta_client + .fallback() + .labels() + .into_iter() + .map(|(key, value)| Dimension::builder().name(key).value(value).build()) + .collect(), + ) + }) + } + + async fn get_metric_data(&self, collected: Vec) -> Vec { + let now = SystemTime::now(); + let mut data: Vec = Vec::with_capacity(collected.len()); + + let container_dims = self.container_dimensions().await; + let container_dims_len = container_dims.len(); + + for metric in collected { + let metric_name = metric.key.name().to_string(); + + let mut dimensions: Vec = + Vec::with_capacity(container_dims_len + metric.key.labels().count()); + + // Add container metadata dimensions + dimensions.extend(container_dims.iter().cloned()); + + for label in metric.key.labels() { + let value = label.value(); + if value.is_empty() { + log::warn!( + "Skipping empty label '{}' for metric '{}' - CloudWatch does not support empty dimension values", + label.key(), + metric_name + ); + continue; + } + + dimensions.push(Dimension::builder().name(label.key()).value(value).build()); + } + + let value = match metric.value { + MetricValue::CounterU64(val) => val as f64, + MetricValue::CounterI64(val) => val as f64, + MetricValue::GaugeF64(val) => val, + MetricValue::GaugeU64(val) => val as f64, + MetricValue::GaugeI64(val) => val as f64, + }; + + let mut datum_builder = MetricDatum::builder() + .metric_name(metric_name) + .timestamp(aws_smithy_types::DateTime::from(now)) + .value(value) + .set_dimensions(Some(dimensions)); + + // For cumulative counters, include the start time + if matches!( + metric.value, + MetricValue::CounterU64(_) | MetricValue::CounterI64(_) + ) { + // CloudWatch uses storage resolution to determine how data is aggregated + // For counters, we use high resolution (1 second) to better track cumulative values + datum_builder = datum_builder.storage_resolution(1); + } + + data.push(datum_builder.build()) + } + + data + } + + async fn send_metric_batch( + &self, + client: &cloudwatch::Client, + metric_data: Vec, + ) -> Result<(), anyhow::Error> { + client + .put_metric_data() + .namespace(&self.namespace) + .set_metric_data(Some(metric_data)) + .send() + .await + .context("send metrics to CloudWatch")?; + + Ok(()) + } +} + +#[async_trait::async_trait] +impl Exporter for Aws { + async fn export(&self, metrics: Vec) { + if let Err(err) = self.export_metrics(metrics).await { + log::error!("Failed to export metrics to AWS CloudWatch: {}", err); + } + } +} diff --git a/runtimes/core/src/metrics/exporter/datadog.rs b/runtimes/core/src/metrics/exporter/datadog.rs new file mode 100644 index 0000000000..3cea3b4d29 --- /dev/null +++ b/runtimes/core/src/metrics/exporter/datadog.rs @@ -0,0 +1,243 @@ +use crate::{ + encore::runtime::v1 as pb, + metadata::ContainerMetaClient, + metrics::{exporter::Exporter, CollectedMetric, MetricValue}, + secrets, +}; +use anyhow::Context; +use dashmap::DashMap; +use datadog_api_client::datadog; +use datadog_api_client::datadogV2::api_metrics::{MetricsAPI, SubmitMetricsOptionalParams}; +use datadog_api_client::datadogV2::model::{ + MetricIntakeType, MetricPayload, MetricPoint, MetricSeries, +}; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; + +pub struct Datadog { + client: Arc, + container_meta_client: ContainerMetaClient, + last_export: Arc>, + last_value: Arc>, + container_tags: tokio::sync::OnceCell>>, +} + +#[derive(Clone)] +struct DatadogClient { + config: datadog::Configuration, +} + +impl DatadogClient { + async fn send_metrics(&self, metric_series: Vec) -> Result<(), anyhow::Error> { + let api = MetricsAPI::with_config(self.config.clone()); + let payload = MetricPayload::new(metric_series); + + api.submit_metrics(payload, SubmitMetricsOptionalParams::default()) + .await + .context("submit metrics to Datadog")?; + + Ok(()) + } +} + +struct LazyDatadogClient { + cell: tokio::sync::OnceCell, + site: String, + api_key: String, +} + +impl LazyDatadogClient { + fn new(site: String, api_key: String) -> Self { + Self { + cell: tokio::sync::OnceCell::new(), + site, + api_key, + } + } + + async fn get(&self) -> &DatadogClient { + self.cell + .get_or_init(|| async { + // Create Datadog API client configuration + let mut configuration = datadog::Configuration::new(); + configuration + .server_variables + .insert("site".to_string(), self.site.clone()); + configuration.set_auth_key( + "apiKeyAuth", + datadog::APIKey { + key: self.api_key.clone(), + prefix: String::new(), + }, + ); + + DatadogClient { + config: configuration, + } + }) + .await + } +} + +impl Datadog { + pub fn new( + provider_cfg: &pb::metrics_provider::Datadog, + secrets: &secrets::Manager, + container_meta_client: ContainerMetaClient, + ) -> anyhow::Result { + let api_key = match &provider_cfg.api_key { + Some(data) => { + let secret = secrets.load(data.clone()); + let api_key_bytes = secret.get().context("failed to resolve Datadog API key")?; + std::str::from_utf8(api_key_bytes) + .context("Datadog API key is not valid UTF-8")? + .to_string() + } + None => { + return Err(anyhow::anyhow!("Datadog API key not provided")); + } + }; + + Ok(Self { + client: Arc::new(LazyDatadogClient::new(provider_cfg.site.clone(), api_key)), + container_meta_client, + last_export: Arc::new(Mutex::new( + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("system time before Unix epoch") + .as_secs() as i64, + )), + last_value: Arc::new(DashMap::new()), + container_tags: tokio::sync::OnceCell::new(), + }) + } + + async fn export_metrics(&self, metrics: Vec) -> anyhow::Result<()> { + if metrics.is_empty() { + return Ok(()); + } + + let client = self.client.get().await; + + log::trace!( + "Exporting {} metrics to Datadog site {}", + metrics.len(), + self.client.site + ); + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("system time before Unix epoch") + .as_secs() as i64; + + let metric_series = self.get_metric_data(metrics, now).await; + + if !metric_series.is_empty() { + client.send_metrics(metric_series).await?; + } + + // Update last export time + if let Ok(mut last_export) = self.last_export.lock() { + *last_export = now; + } + + Ok(()) + } + + async fn container_tags_vec(&self) -> Arc> { + self.container_tags + .get_or_try_init(|| async { + let container_metadata = self.container_meta_client.collect().await?; + anyhow::Ok(Arc::new( + container_metadata + .labels() + .into_iter() + .map(|(key, value)| format!("{}:{}", key, value)) + .collect(), + )) + }) + .await + .map(Arc::clone) + .unwrap_or_else(|e| { + log::warn!("failed fetching container metadata: {e}, using fallback"); + Arc::new( + self.container_meta_client + .fallback() + .labels() + .into_iter() + .map(|(key, value)| format!("{}:{}", key, value)) + .collect(), + ) + }) + } + + async fn get_metric_data( + &self, + collected: Vec, + now: i64, + ) -> Vec { + let mut data: Vec = Vec::with_capacity(collected.len()); + + let container_tags = self.container_tags_vec().await; + let container_tags_len = container_tags.len(); + + let last_export = self.last_export.lock().ok().map(|t| *t).unwrap_or(now); + let interval = now - last_export; + + for metric in collected { + let metric_name = metric.key.name().to_string(); + + // Build tags: container metadata + metric labels + let mut tags: Vec = + Vec::with_capacity(container_tags_len + metric.key.labels().count()); + // Add container metadata tags + tags.extend(container_tags.iter().cloned()); + + for label in metric.key.labels() { + tags.push(format!("{}:{}", label.key(), label.value())); + } + + let (metric_type, value) = match metric.value { + MetricValue::CounterU64(val) => { + let value = val as f64; + let key = metric.key.get_hash(); + let last_val = self.last_value.get(&key).map(|v| *v).unwrap_or(0.0); + self.last_value.insert(key, value); + let delta = value - last_val; + (MetricIntakeType::COUNT, delta) + } + MetricValue::CounterI64(val) => { + let value = val as f64; + let key = metric.key.get_hash(); + let last_val = self.last_value.get(&key).map(|v| *v).unwrap_or(0.0); + self.last_value.insert(key, value); + let delta = value - last_val; + (MetricIntakeType::COUNT, delta) + } + MetricValue::GaugeF64(val) => (MetricIntakeType::GAUGE, val), + MetricValue::GaugeU64(val) => (MetricIntakeType::GAUGE, val as f64), + MetricValue::GaugeI64(val) => (MetricIntakeType::GAUGE, val as f64), + }; + + let point = MetricPoint::new().timestamp(now).value(value); + + let series = MetricSeries::new(metric_name, vec![point]) + .type_(metric_type) + .interval(interval) + .tags(tags); + + data.push(series); + } + + data + } +} + +#[async_trait::async_trait] +impl Exporter for Datadog { + async fn export(&self, metrics: Vec) { + if let Err(err) = self.export_metrics(metrics).await { + log::error!("Failed to export metrics to Datadog: {}", err); + } + } +} diff --git a/runtimes/core/src/metrics/exporter/gcp.rs b/runtimes/core/src/metrics/exporter/gcp.rs new file mode 100644 index 0000000000..799e0d1696 --- /dev/null +++ b/runtimes/core/src/metrics/exporter/gcp.rs @@ -0,0 +1,256 @@ +use crate::metadata::ContainerMetaClient; +use crate::metrics::exporter::Exporter; +use crate::metrics::{CollectedMetric, MetricValue}; +use anyhow::Context; +use google_cloud_api::model::metric_descriptor::{MetricKind, ValueType}; +use google_cloud_api::model::{Metric, MonitoredResource}; +use google_cloud_monitoring_v3::client::MetricService; +use google_cloud_monitoring_v3::model::{Point, TimeInterval, TimeSeries, TypedValue}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::sync::OnceCell; + +type LabelPairs = Vec<(String, String)>; + +#[derive(Debug)] +pub struct Gcp { + client: Arc, + project_id: String, + monitored_resource_type: String, + monitored_resource_labels: HashMap, + metric_names: HashMap, + container_meta_client: Arc, + container_labels: Arc>>, +} + +#[derive(Debug)] +struct LazyMonitoringClient { + cell: OnceCell>, +} + +impl LazyMonitoringClient { + fn new() -> Self { + Self { + cell: OnceCell::new(), + } + } + + async fn get(&self) -> &anyhow::Result { + self.cell + .get_or_init(|| async { + MetricService::builder() + .build() + .await + .inspect_err(|e| log::error!("Failed to create GCP monitoring client: {e:?}")) + .context("create monitoring client") + }) + .await + } +} + +impl Gcp { + pub fn new( + project_id: String, + monitored_resource_type: String, + monitored_resource_labels: HashMap, + metric_names: HashMap, + container_meta_client: ContainerMetaClient, + ) -> Self { + Self { + client: Arc::new(LazyMonitoringClient::new()), + project_id, + monitored_resource_type, + monitored_resource_labels, + metric_names, + container_meta_client: Arc::new(container_meta_client), + container_labels: Arc::new(OnceCell::new()), + } + } + + async fn export_metrics(&self, metrics: Vec) -> Result<(), anyhow::Error> { + if metrics.is_empty() { + return Ok(()); + } + + let client = match self.client.get().await { + Ok(client) => client, + Err(e) => { + log::error!("Failed to get monitoring client: {}", e); + return Err(anyhow::anyhow!("Failed to get monitoring client: {}", e)); + } + }; + + log::trace!( + "Exporting {} metrics to GCP project {}", + metrics.len(), + self.project_id + ); + + let time_series = self.get_metric_data(metrics).await; + + // Send metrics in batches (Google Cloud allows up to 200 time series per request) + for batch in time_series.chunks(200) { + if let Err(e) = self.send_time_series_batch(client, batch.to_vec()).await { + log::error!("Failed to export metrics batch: {}", e); + } + } + + Ok(()) + } + + async fn container_labels(&self) -> Arc> { + self.container_labels + .get_or_try_init(|| async { + let container_metadata = self.container_meta_client.collect().await?; + anyhow::Ok(Arc::new(container_metadata.labels())) + }) + .await + .map(Arc::clone) + .unwrap_or_else(|e| { + log::warn!("failed fetching container metadata: {e}, using fallback"); + Arc::new(self.container_meta_client.fallback().labels()) + }) + } + + async fn get_metric_data(&self, collected: Vec) -> Vec { + let end_time = SystemTime::now(); + let ts_end_time: google_cloud_wkt::Timestamp = end_time.try_into().unwrap_or_default(); + + let mut data: Vec = Vec::with_capacity(collected.len()); + + let container_labels = self.container_labels().await; + let container_labels_len = container_labels.len(); + + let instance_id = &self + .container_meta_client + .collect() + .await + .unwrap_or(self.container_meta_client.fallback()) + .instance_id; + + for metric in collected { + let cloud_metric_name = match self.metric_names.get(metric.key.name()) { + Some(name) => name, + None => { + log::warn!( + "Skipping metric '{}' - no cloud metric name configured", + metric.key.name() + ); + continue; + } + }; + + let mut labels = + HashMap::with_capacity(container_labels_len + metric.key.labels().len()); + labels.extend(container_labels.iter().cloned()); + labels.extend( + metric + .key + .labels() + .map(|label| (label.key().to_string(), label.value().to_string())), + ); + + let (kind, value_type, typed_value, interval) = match metric.value { + MetricValue::CounterU64(val) => { + let start_time: google_cloud_wkt::Timestamp = + metric.registered_at.try_into().unwrap_or_default(); + + ( + MetricKind::Cumulative, + ValueType::Int64, + TypedValue::new().set_int64_value(val as i64), + TimeInterval::new() + .set_start_time(start_time) + .set_end_time(ts_end_time), + ) + } + MetricValue::CounterI64(val) => { + let start_time: google_cloud_wkt::Timestamp = + metric.registered_at.try_into().unwrap_or_default(); + + ( + MetricKind::Cumulative, + ValueType::Int64, + TypedValue::new().set_int64_value(val), + TimeInterval::new() + .set_start_time(start_time) + .set_end_time(ts_end_time), + ) + } + MetricValue::GaugeF64(val) => ( + MetricKind::Gauge, + ValueType::Double, + TypedValue::new().set_double_value(val), + TimeInterval::new().set_end_time(ts_end_time), + ), + MetricValue::GaugeU64(val) => ( + MetricKind::Gauge, + ValueType::Int64, + TypedValue::new().set_int64_value(val as i64), + TimeInterval::new().set_end_time(ts_end_time), + ), + MetricValue::GaugeI64(val) => ( + MetricKind::Gauge, + ValueType::Int64, + TypedValue::new().set_int64_value(val), + TimeInterval::new().set_end_time(ts_end_time), + ), + }; + + // Add container instance ID to node_id if present + let mut monitored_resource_labels = self.monitored_resource_labels.clone(); + if let Some(node_id) = monitored_resource_labels.get("node_id") { + monitored_resource_labels.insert( + "node_id".to_string(), + format!("{}-{}", node_id, instance_id), + ); + } + + let mut mr = MonitoredResource::new().set_type(&self.monitored_resource_type); + mr.labels = monitored_resource_labels; + + data.push( + TimeSeries::new() + .set_metric_kind(kind) + .set_metric( + Metric::new() + .set_type(format!("custom.googleapis.com/{}", cloud_metric_name)) + .set_labels(labels), + ) + .set_resource(mr) + .set_value_type(value_type) + .set_points(vec![Point::new() + .set_interval(interval) + .set_value(typed_value)]), + ); + } + + data + } + + async fn send_time_series_batch( + &self, + client: &MetricService, + time_series: Vec, + ) -> Result<(), anyhow::Error> { + client + .create_time_series() + .set_name(format!("projects/{}", self.project_id)) + .set_time_series(time_series) + .send() + .await + .map_err(anyhow::Error::new)?; + + Ok(()) + } +} + +#[async_trait::async_trait] +impl Exporter for Gcp { + async fn export(&self, metrics: Vec) { + if let Err(err) = self.export_metrics(metrics).await { + log::error!("Failed to export metrics to GCP: {}", err); + } + } +} diff --git a/runtimes/core/src/metrics/exporter/mod.rs b/runtimes/core/src/metrics/exporter/mod.rs new file mode 100644 index 0000000000..6328149b87 --- /dev/null +++ b/runtimes/core/src/metrics/exporter/mod.rs @@ -0,0 +1,13 @@ +mod aws; +mod datadog; +mod gcp; +mod prometheus; +pub use aws::Aws; +pub use datadog::Datadog; +pub use gcp::Gcp; +pub use prometheus::Prometheus; + +#[async_trait::async_trait] +pub trait Exporter: Send + Sync { + async fn export(&self, metrics: Vec); +} diff --git a/runtimes/core/src/metrics/exporter/prometheus.rs b/runtimes/core/src/metrics/exporter/prometheus.rs new file mode 100644 index 0000000000..05917bc334 --- /dev/null +++ b/runtimes/core/src/metrics/exporter/prometheus.rs @@ -0,0 +1,214 @@ +use crate::encore::runtime::v1 as pb; +use crate::metadata::ContainerMetaClient; +use crate::metrics::exporter::Exporter; +use crate::metrics::{CollectedMetric, MetricValue}; +use crate::secrets; +use anyhow::Context; +use prost::Message; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::sync::OnceCell; +use url::Url; + +#[derive(Debug)] +pub struct Prometheus { + client: reqwest::Client, + remote_write_url: Url, + container_meta_client: ContainerMetaClient, + container_labels: OnceCell>>, +} + +impl Prometheus { + pub fn new( + provider_cfg: &pb::metrics_provider::PrometheusRemoteWrite, + secrets: &secrets::Manager, + container_meta_client: ContainerMetaClient, + ) -> anyhow::Result { + let remote_write_url = match &provider_cfg.remote_write_url { + Some(data) => { + let secret = secrets.load(data.clone()); + let url_bytes = secret + .get() + .context("failed to resolve Prometheus Remote Write Url")?; + let s = std::str::from_utf8(url_bytes) + .context("Prometheus Remote Write Url is not valid UTF-8")?; + Url::parse(s).context("Prometheus Remote Write Url not valid")? + } + None => { + return Err(anyhow::anyhow!("Prometheus Remote Write Url not provided")); + } + }; + + Ok(Self { + client: reqwest::Client::new(), + remote_write_url, + container_meta_client, + container_labels: OnceCell::new(), + }) + } + + async fn export_metrics(&self, metrics: Vec) -> anyhow::Result<()> { + if metrics.is_empty() { + return Ok(()); + } + + log::trace!( + "Exporting {} metrics to Prometheus remote write host {}", + metrics.len(), + self.remote_write_url.host_str().unwrap_or_default() + ); + + let time_series = self.get_metric_data(metrics).await; + + // Create WriteRequest with the time series + let write_request = prompb::WriteRequest { + timeseries: time_series, + metadata: vec![], + }; + + // Serialize to protobuf + let mut proto_buf = Vec::new(); + write_request + .encode(&mut proto_buf) + .context("marshal metrics into Protobuf")?; + + // Compress with Snappy + let mut encoder = snap::raw::Encoder::new(); + let encoded = encoder + .compress_vec(&proto_buf) + .context("compress metrics with Snappy")?; + + // Send HTTP request + let response = self + .client + .post(self.remote_write_url.clone()) + .header("Content-Type", "application/x-protobuf") + .header("Content-Encoding", "snappy") + .header("User-Agent", "encore") + .header("X-Prometheus-Remote-Write-Version", "0.1.0") + .body(encoded) + .send() + .await + .context("send metrics to Prometheus remote write destination")?; + + if !response.status().is_success() { + let status = response.status(); + let body = response + .text() + .await + .unwrap_or_else(|_| "".to_string()); + anyhow::bail!( + "Prometheus remote write returned non-success status {}: {}", + status, + body + ); + } + + Ok(()) + } + + async fn container_labels(&self) -> Arc> { + self.container_labels + .get_or_try_init(|| async { + let container_metadata = self.container_meta_client.collect().await?; + anyhow::Ok(Arc::new( + container_metadata + .labels() + .into_iter() + .map(|(name, value)| prompb::Label { name, value }) + .collect(), + )) + }) + .await + .map(Arc::clone) + .unwrap_or_else(|e| { + log::warn!("failed fetching container metadata: {e}, using fallback"); + Arc::new( + self.container_meta_client + .fallback() + .labels() + .into_iter() + .map(|(name, value)| prompb::Label { name, value }) + .collect(), + ) + }) + } + + async fn get_metric_data(&self, collected: Vec) -> Vec { + let now = SystemTime::now(); + let timestamp = from_time(now); + let mut data: Vec = Vec::with_capacity(collected.len()); + + let container_labels = self.container_labels().await; + let container_labels_len = container_labels.len(); + + for metric in collected { + let metric_name = metric.key.name().to_string(); + + // Build labels: container metadata + metric labels + __name__ + let mut labels: Vec = + Vec::with_capacity(container_labels_len + metric.key.labels().len() + 1); + + // Add container metadata labels + labels.extend(container_labels.iter().cloned()); + + // Add metric-specific labels + for label in metric.key.labels() { + labels.push(prompb::Label { + name: label.key().to_string(), + value: label.value().to_string(), + }); + } + + // Add __name__ label for the metric name + labels.push(prompb::Label { + name: "__name__".to_string(), + value: metric_name, + }); + + // Convert metric value to float64 + let value = match metric.value { + MetricValue::CounterU64(val) => val as f64, + MetricValue::CounterI64(val) => val as f64, + MetricValue::GaugeF64(val) => val, + MetricValue::GaugeU64(val) => val as f64, + MetricValue::GaugeI64(val) => val as f64, + }; + + data.push(prompb::TimeSeries { + labels, + samples: vec![prompb::Sample { value, timestamp }], + exemplars: vec![], + histograms: vec![], + }); + } + + data + } +} + +#[async_trait::async_trait] +impl Exporter for Prometheus { + async fn export(&self, metrics: Vec) { + if let Err(err) = self.export_metrics(metrics).await { + log::error!("Failed to export metrics to Prometheus: {}", err); + } + } +} + +/// Convert SystemTime to Prometheus timestamp (milliseconds since Unix epoch) +fn from_time(t: SystemTime) -> i64 { + match t.duration_since(SystemTime::UNIX_EPOCH) { + Ok(duration) => { + let secs = duration.as_secs() as i64; + let nanos = duration.subsec_nanos() as i64; + secs * 1000 + nanos / 1_000_000 + } + Err(_) => 0, // If time is before Unix epoch, return 0 + } +} + +#[allow(dead_code)] +mod prompb { + include!(concat!(env!("OUT_DIR"), "/prometheus.rs")); +} diff --git a/runtimes/core/src/metrics/gauge.rs b/runtimes/core/src/metrics/gauge.rs new file mode 100644 index 0000000000..9fa937f3b7 --- /dev/null +++ b/runtimes/core/src/metrics/gauge.rs @@ -0,0 +1,209 @@ +use crate::metrics; +use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; + +pub trait GaugeOps { + fn set(&self, value: T); + fn get(&self) -> crate::metrics::MetricValue; +} + +/// A typed gauge that can be set, incremented, or decremented +/// T must be compatible with GaugeOps for type-safe operations +pub struct Gauge { + atomic: Arc, + _phantom: PhantomData, +} + +impl Gauge +where + Arc: GaugeOps, +{ + /// Create a new gauge with the given atomic storage + /// This is typically called by Registry, not directly by users + pub(crate) fn new(atomic: Arc) -> Self { + Self { + atomic, + _phantom: PhantomData, + } + } + + /// Set the gauge to the specified value + pub fn set(&self, value: T) { + GaugeOps::set(&self.atomic, value); + } + + /// Get the current value of the gauge + pub fn get(&self) -> metrics::MetricValue { + GaugeOps::get(&self.atomic) + } +} + +/// A gauge schema that defines static labels and required dynamic label keys +/// Validates dynamic labels at set/add/sub time and creates separate time series +/// for each unique combination of static + dynamic labels +#[derive(Clone, Debug)] +pub struct Schema { + name: String, + static_labels: Vec<(String, String)>, + required_dynamic_keys: HashSet, + registry: Arc, + _phantom: PhantomData, +} + +impl Schema +where + Arc: GaugeOps, + T: Send + Sync + 'static, +{ + /// Create a new gauge schema + pub(crate) fn new( + name: String, + static_labels: Vec<(String, String)>, + required_dynamic_keys: HashSet, + registry: Arc, + ) -> Self { + Self { + name, + static_labels, + required_dynamic_keys, + registry, + _phantom: PhantomData, + } + } + + /// Set the gauge value directly without dynamic labels + pub fn set(&self, value: T) { + if !self.required_dynamic_keys.is_empty() { + log::warn!( + "setting gauge '{}' without required dynamic labels, required keys: {:?}", + self.name, + self.required_dynamic_keys + ); + } + + self.get_or_create_gauge(HashMap::new()).set(value); + } + + // Set the dynamic label values and return a completed Gauge + pub fn with(&self, dynamic_labels: L) -> Gauge + where + L: IntoIterator, + K: Into, + V: Into, + { + // Convert dynamic_labels to HashMap first + let dynamic_labels_map: HashMap = dynamic_labels + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + + // Validate required keys are present + let missing: Vec = self + .required_dynamic_keys + .iter() + .filter(|key| !dynamic_labels_map.contains_key(*key)) + .cloned() + .collect(); + + if !missing.is_empty() { + log::warn!( + "missing required dynamic labels for metric '{}': {:?}, required keys: {:?}", + self.name, + missing, + self.required_dynamic_keys + ); + } + + self.get_or_create_gauge(dynamic_labels_map) + } + + /// Get or create a gauge for the given dynamic labels + fn get_or_create_gauge(&self, dynamic_labels: HashMap) -> Gauge { + // Create merged labels (static + dynamic) + let mut merged_labels = self.static_labels.clone(); + for (key, value) in dynamic_labels { + merged_labels.push((key, value)); + } + + self.registry.get_or_create_gauge( + &self.name, + merged_labels.iter().map(|(k, v)| (k.as_str(), v.as_str())), + ) + } +} + +/// Builder for creating gauge schemas with static labels and required dynamic keys +pub struct GaugeSchemaBuilder { + name: String, + static_labels: Vec<(String, String)>, + required_dynamic_keys: HashSet, + registry: Arc, + _phantom: std::marker::PhantomData, +} + +impl GaugeSchemaBuilder +where + Arc: GaugeOps, + T: Send + Sync + 'static, +{ + pub(crate) fn new(name: String, registry: Arc) -> Self { + Self { + name, + static_labels: Vec::new(), + required_dynamic_keys: HashSet::new(), + registry, + _phantom: std::marker::PhantomData, + } + } + + /// Add static labels that are set once when the schema is created + pub fn static_labels(mut self, labels: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + for (key, value) in labels { + self.static_labels + .push((key.as_ref().to_string(), value.as_ref().to_string())); + } + self + } + + /// Add a single static label + pub fn static_label(mut self, key: &str, value: &str) -> Self { + self.static_labels + .push((key.to_string(), value.to_string())); + self + } + + /// Specify required dynamic label keys that must be provided at set/add/sub time + pub fn require_dynamic_keys(mut self, keys: I) -> Self + where + I: IntoIterator, + K: AsRef, + { + for key in keys { + self.required_dynamic_keys.insert(key.as_ref().to_string()); + } + self + } + + /// Add a single required dynamic key + pub fn require_dynamic_key(mut self, key: &str) -> Self { + self.required_dynamic_keys.insert(key.to_string()); + self + } + + /// Build the gauge schema + pub fn build(self) -> Schema { + Schema::new( + self.name, + self.static_labels, + self.required_dynamic_keys, + self.registry, + ) + } +} diff --git a/runtimes/core/src/metrics/manager.rs b/runtimes/core/src/metrics/manager.rs new file mode 100644 index 0000000000..39e9a06f76 --- /dev/null +++ b/runtimes/core/src/metrics/manager.rs @@ -0,0 +1,217 @@ +use crate::{ + encore::runtime::v1::{self as pb, Environment}, + metadata::{process_env_substitution, ContainerMetaClient}, + metrics::{ + exporter::{self, Exporter}, + registry::Registry, + }, + secrets, +}; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Debug, Clone)] +enum ProviderType { + Gcp(pb::metrics_provider::GcpCloudMonitoring), + EncoreCloud(pb::metrics_provider::GcpCloudMonitoring), + Aws(pb::metrics_provider::AwsCloudWatch), + Datadog(pb::metrics_provider::Datadog), + Prometheus(pb::metrics_provider::PrometheusRemoteWrite), +} + +impl ProviderType { + fn from_config(provider: &pb::MetricsProvider) -> Option { + match &provider.provider { + Some(pb::metrics_provider::Provider::Gcp(config)) => Some(Self::Gcp(config.clone())), + Some(pb::metrics_provider::Provider::EncoreCloud(config)) => { + Some(Self::EncoreCloud(config.clone())) + } + Some(pb::metrics_provider::Provider::Aws(config)) => Some(Self::Aws(config.clone())), + Some(pb::metrics_provider::Provider::Datadog(config)) => { + Some(Self::Datadog(config.clone())) + } + Some(pb::metrics_provider::Provider::PromRemoteWrite(config)) => { + Some(Self::Prometheus(config.clone())) + } + None => { + log::warn!("no metrics provider configured"); + None + } + } + } + + fn create_exporter( + &self, + env: &Environment, + secrets: &secrets::Manager, + http_client: &reqwest::Client, + ) -> anyhow::Result> { + match self { + Self::Gcp(config) | Self::EncoreCloud(config) => { + Ok(Self::create_gcp_exporter(config, env, http_client)) + } + Self::Aws(config) => Ok(Self::create_aws_exporter(config, env, http_client)), + Self::Datadog(config) => { + Self::create_datadog_exporter(config, secrets, env, http_client) + } + Self::Prometheus(config) => { + Self::create_prometheus_exporter(config, secrets, env, http_client) + } + } + } + + fn create_prometheus_exporter( + provider_cfg: &pb::metrics_provider::PrometheusRemoteWrite, + secrets: &secrets::Manager, + env: &Environment, + http_client: &reqwest::Client, + ) -> anyhow::Result> { + let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone()); + Ok(Arc::new(exporter::Prometheus::new( + provider_cfg, + secrets, + container_meta_client, + )?)) + } + + fn create_datadog_exporter( + provider_cfg: &pb::metrics_provider::Datadog, + secrets: &secrets::Manager, + env: &Environment, + http_client: &reqwest::Client, + ) -> anyhow::Result> { + let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone()); + Ok(Arc::new(exporter::Datadog::new( + provider_cfg, + secrets, + container_meta_client, + )?)) + } + + fn create_aws_exporter( + provider_cfg: &pb::metrics_provider::AwsCloudWatch, + env: &Environment, + http_client: &reqwest::Client, + ) -> Arc { + let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone()); + Arc::new(exporter::Aws::new( + provider_cfg.namespace.clone(), + container_meta_client, + )) + } + + fn create_gcp_exporter( + provider_cfg: &pb::metrics_provider::GcpCloudMonitoring, + env: &Environment, + http_client: &reqwest::Client, + ) -> Arc { + let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone()); + + let mut labels = provider_cfg.monitored_resource_labels.clone(); + process_env_substitution(&mut labels); + + Arc::new(exporter::Gcp::new( + provider_cfg.project_id.clone(), + provider_cfg.monitored_resource_type.clone(), + labels, + provider_cfg.metric_names.clone(), + container_meta_client, + )) + } +} + +#[derive(Clone)] +pub struct Manager { + exporter: Option>, + registry: Arc, +} + +impl Manager { + pub fn new() -> Self { + let registry = Arc::new(Registry::new()); + + Self { + exporter: None, + registry, + } + } + + pub fn registry(&self) -> &Arc { + &self.registry + } + + pub fn from_runtime_config( + observability: &pb::Observability, + environment: &pb::Environment, + secrets: &secrets::Manager, + http_client: &reqwest::Client, + runtime_handle: tokio::runtime::Handle, + ) -> Self { + let mut manager = Self::new(); + + for metrics_provider in &observability.metrics { + if let Some(provider_type) = ProviderType::from_config(metrics_provider) { + match provider_type.create_exporter(environment, secrets, http_client) { + Ok(exporter) => { + manager.exporter = Some(exporter); + break; // Take the first valid provider + } + Err(err) => { + log::error!("Failed to create metrics exporter: {}", err); + } + } + } + } + + // Start collection if we have an exporter + if manager.exporter.is_some() { + let collection_interval = observability + .metrics + .first() + .and_then(|p| p.collection_interval.as_ref()) + .and_then(|d| Duration::try_from(d.clone()).ok()) + .unwrap_or(Duration::from_secs(60)); // Default to 1 minute + + manager.start_collection_loop(runtime_handle, collection_interval); + } + + manager + } + + pub fn with_exporter(mut self, exporter: std::sync::Arc) -> Self { + self.exporter = Some(exporter); + self + } + + pub async fn collect_and_export(&self) { + let metrics = self.registry.collect(); + if let Some(ref exporter) = self.exporter { + exporter.export(metrics).await; + } + } + + pub fn collect_metrics(&self) -> Vec { + self.registry.collect() + } + + pub fn start_collection_loop( + &self, + runtime_handle: tokio::runtime::Handle, + interval: std::time::Duration, + ) { + let manager = self.clone(); + runtime_handle.spawn(async move { + let mut interval_timer = tokio::time::interval(interval); + loop { + interval_timer.tick().await; + manager.collect_and_export().await; + } + }); + } +} + +impl Default for Manager { + fn default() -> Self { + Self::new() + } +} diff --git a/runtimes/core/src/metrics/mod.rs b/runtimes/core/src/metrics/mod.rs new file mode 100644 index 0000000000..812fa9c8ae --- /dev/null +++ b/runtimes/core/src/metrics/mod.rs @@ -0,0 +1,39 @@ +mod atomic; +mod exporter; +mod manager; +mod registry; +mod system; + +pub mod counter; +pub mod gauge; + +#[cfg(test)] +mod test; + +use std::sync::Arc; + +pub use counter::{Counter, CounterOps}; +pub use gauge::{Gauge, GaugeOps}; +pub use manager::Manager; +pub use registry::{CollectedMetric, MetricValue, Registry}; +pub use system::SystemMetricsCollector; + +/// Create a requests counter schema +pub fn requests_total_counter( + registry: &Arc, + service: &str, + endpoint: &str, +) -> counter::Schema { + registry + .counter_schema::("e_requests_total") + .static_labels([("service", service), ("endpoint", endpoint)]) + .require_dynamic_key("code") + .build() +} + +/// Create a memory usage gauge schema +pub fn memory_usage_gauge_schema(registry: &Arc) -> gauge::Schema { + registry + .gauge_schema::("e_sys_memory_used_bytes") + .build() +} diff --git a/runtimes/core/src/metrics/registry.rs b/runtimes/core/src/metrics/registry.rs new file mode 100644 index 0000000000..d1e0f1bb34 --- /dev/null +++ b/runtimes/core/src/metrics/registry.rs @@ -0,0 +1,182 @@ +use crate::metrics::counter::{CounterOps, CounterSchemaBuilder}; +use crate::metrics::gauge::{GaugeOps, GaugeSchemaBuilder}; + +use super::system::SystemMetricsCollector; +use super::{Counter, Gauge}; +use dashmap::DashMap; +use malachite::base::num::basic::traits::One; +use metrics::{Key, Label}; +use std::sync::atomic::AtomicU64; +use std::sync::Arc; +use std::time::SystemTime; + +struct MetricStorage { + atomic: Arc, + getter: Box MetricValue + Send + Sync>, + registered_at: SystemTime, +} + +impl std::fmt::Debug for MetricStorage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MetricStorage") + .field("atomic", &self.atomic) + .field("registered_at", &self.registered_at) + .finish() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum MetricValue { + // Counter variants + CounterU64(u64), + CounterI64(i64), + + // Gauge variants + GaugeU64(u64), + GaugeI64(i64), + GaugeF64(f64), +} + +#[derive(Debug, Clone)] +pub struct CollectedMetric { + pub key: Key, + pub value: MetricValue, + pub registered_at: SystemTime, +} + +#[derive(Debug)] +pub struct Registry { + counters: DashMap, + gauges: DashMap, + system_metrics: SystemMetricsCollector, +} + +impl Registry { + pub fn new() -> Self { + Self { + counters: DashMap::new(), + gauges: DashMap::new(), + system_metrics: SystemMetricsCollector::new(), + } + } + + /// Create a counter with the given name and labels + pub fn get_or_create_counter<'a, T>( + &self, + name: &str, + labels: impl IntoIterator, + ) -> Counter + where + Arc: CounterOps, + T: One + Send + Sync + 'static, + { + let labels_vec: Vec