diff --git a/Cargo.lock b/Cargo.lock index 7998a15..30d9718 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,28 +123,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "async-stream" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.32", -] - [[package]] name = "async-trait" version = "0.1.69" @@ -162,18 +140,67 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ce4f10ea3abcd6617873bae9f91d1c5332b4a778bd9ce34d0cd517474c1de82" -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.68" @@ -201,12 +228,6 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" -[[package]] -name = "binascii" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" - [[package]] name = "bit-set" version = "0.5.3" @@ -382,17 +403,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "cookie" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -453,39 +463,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "devise" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" -dependencies = [ - "devise_codegen", - "devise_core", -] - -[[package]] -name = "devise_codegen" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" -dependencies = [ - "devise_core", - "quote", -] - -[[package]] -name = "devise_core" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" -dependencies = [ - "bitflags 2.3.3", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.32", -] - [[package]] name = "difflib" version = "0.4.0" @@ -569,20 +546,6 @@ dependencies = [ "instant", ] -[[package]] -name = "figment" -version = "0.10.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" -dependencies = [ - "atomic", - "pear", - "serde", - "toml", - "uncased", - "version_check", -] - [[package]] name = "filetime" version = "0.2.21" @@ -637,12 +600,13 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -651,9 +615,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -661,37 +625,60 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -700,19 +687,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -740,12 +714,6 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "h2" version = "0.3.20" @@ -757,7 +725,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.9", "indexmap 1.9.3", "slab", "tokio", @@ -765,6 +733,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap 2.0.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -800,6 +787,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -807,7 +805,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.9", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -833,9 +854,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.20", + "http 0.2.9", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -847,6 +868,41 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.2", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", + "pin-project-lite", + "socket2 0.5.5", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.59" @@ -870,6 +926,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + [[package]] name = "ident_case" version = "1.0.1" @@ -904,7 +966,6 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", - "serde", ] [[package]] @@ -913,12 +974,6 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - [[package]] name = "instant" version = "0.1.12" @@ -945,17 +1000,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "is-terminal" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" -dependencies = [ - "hermit-abi", - "rustix 0.38.2", - "windows-sys", -] - [[package]] name = "iso8601" version = "0.5.1" @@ -1036,12 +1080,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" -[[package]] -name = "linux-raw-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" - [[package]] name = "lock_api" version = "0.4.10" @@ -1059,28 +1097,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] -name = "loom" -version = "0.5.6" +name = "matchit" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "serde", - "serde_json", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata", -] +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "md5" @@ -1135,26 +1155,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "multer" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "log", - "memchr", - "mime", - "spin", - "tokio", - "tokio-util", - "version_check", -] - [[package]] name = "nom" version = "7.1.3" @@ -1294,14 +1294,16 @@ version = "0.3.0" dependencies = [ "anyhow", "assert_cmd", + "axum", "cached", "chrono", "clap", + "futures", "itertools", "jsonschema", "lazy_static", - "log", "md5", + "mime", "pest", "pest_derive", "predicates 2.1.5", @@ -1309,8 +1311,6 @@ dependencies = [ "pyo3", "rand 0.8.5", "regex", - "rocket", - "rocket_prometheus", "serde", "serde_json", "sha1", @@ -1320,6 +1320,12 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", + "tracing-capture", + "tracing-subscriber", "url", "walkdir", ] @@ -1353,29 +1359,6 @@ dependencies = [ "windows-targets 0.48.1", ] -[[package]] -name = "pear" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec95680a7087503575284e5063e14b694b7a9c0b065e5dceec661e0497127e8" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi 0.5.1", -] - -[[package]] -name = "pear_codegen" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9661a3a53f93f09f2ea882018e4d7c88f6ff2956d809a276060476fd8c879d3c" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.32", -] - [[package]] name = "percent-encoding" version = "2.3.0" @@ -1426,6 +1409,26 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1495,19 +1498,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "606c4ba35817e2922a308af55ad51bab3645b59eae5c570d4a6cf07e36bd493b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.32", - "version_check", - "yansi 0.5.1", -] - [[package]] name = "prometheus" version = "0.13.3" @@ -1684,26 +1674,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "ref-cast" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43faa91b1c8b36841ee70e97188a869d37ae21759da6846d4be66de5bf7b12c" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2275aab483050ab2a7364c1a46604865ee7d6906684e08db0f090acf74f9e7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.32", -] - [[package]] name = "regex" version = "1.8.4" @@ -1712,7 +1682,7 @@ checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.2", + "regex-syntax", ] [[package]] @@ -1720,15 +1690,6 @@ name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" @@ -1756,10 +1717,10 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.20", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "ipnet", "js-sys", "log", @@ -1779,98 +1740,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "rocket" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e7bb57ccb26670d73b6a47396c83139447b9e7878cab627fdfe9ea8da489150" -dependencies = [ - "async-stream", - "async-trait", - "atomic", - "binascii", - "bytes", - "either", - "figment", - "futures", - "indexmap 2.0.0", - "log", - "memchr", - "multer", - "num_cpus", - "parking_lot", - "pin-project-lite", - "rand 0.8.5", - "ref-cast", - "rocket_codegen", - "rocket_http", - "serde", - "serde_json", - "state", - "tempfile", - "time", - "tokio", - "tokio-stream", - "tokio-util", - "ubyte", - "version_check", - "yansi 1.0.0-rc.1", -] - -[[package]] -name = "rocket_codegen" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2238066abf75f21be6cd7dc1a09d5414a671f4246e384e49fe3f8a4936bd04c" -dependencies = [ - "devise", - "glob", - "indexmap 2.0.0", - "proc-macro2", - "quote", - "rocket_http", - "syn 2.0.32", - "unicode-xid", - "version_check", -] - -[[package]] -name = "rocket_http" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a1663694d059fe5f943ea5481363e48050acedd241d46deb2e27f71110389e" -dependencies = [ - "cookie", - "either", - "futures", - "http", - "hyper", - "indexmap 2.0.0", - "log", - "memchr", - "pear", - "percent-encoding", - "pin-project-lite", - "ref-cast", - "serde", - "smallvec", - "stable-pattern", - "state", - "time", - "tokio", - "uncased", -] - -[[package]] -name = "rocket_prometheus" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18faabccdfcd08d4501768f5b6936f6332af10496f1ea8107eb48a7766e03439" -dependencies = [ - "prometheus", - "rocket", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1887,20 +1756,7 @@ dependencies = [ "errno", "io-lifetimes", "libc", - "linux-raw-sys 0.3.8", - "windows-sys", -] - -[[package]] -name = "rustix" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabcb0461ebd01d6b79945797c27f8529082226cb630a9865a71870ff63532a4" -dependencies = [ - "bitflags 2.3.3", - "errno", - "libc", - "linux-raw-sys 0.4.3", + "linux-raw-sys", "windows-sys", ] @@ -1925,12 +1781,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.1.0" @@ -1969,11 +1819,12 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "0.6.3" +name = "serde_path_to_error" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ + "itoa", "serde", ] @@ -2020,15 +1871,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.8" @@ -2064,30 +1906,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable-pattern" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" -dependencies = [ - "memchr", -] - -[[package]] -name = "state" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" -dependencies = [ - "loom", -] - [[package]] name = "strsim" version = "0.10.0" @@ -2116,6 +1934,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tar" version = "0.4.38" @@ -2153,7 +1977,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix 0.37.22", + "rustix", "windows-sys", ] @@ -2199,7 +2023,6 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ - "itoa", "serde", "time-core", "time-macros", @@ -2247,7 +2070,6 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "signal-hook-registry", "socket2 0.5.5", "tokio-macros", "windows-sys", @@ -2264,22 +2086,11 @@ dependencies = [ "syn 2.0.32", ] -[[package]] -name = "tokio-stream" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -2290,38 +2101,46 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.7.5" +name = "tower" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "toml_datetime" -version = "0.6.3" +name = "tower-http" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e" dependencies = [ - "serde", + "bitflags 2.3.3", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", + "uuid", ] [[package]] -name = "toml_edit" -version = "0.19.11" +name = "tower-layer" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" -dependencies = [ - "indexmap 2.0.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" @@ -2335,6 +2154,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2351,6 +2171,19 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "tracing-capture" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3b1b84b84d7f95504091cb9518dea662eacba7a3bc23f23e98fe5fafede344" +dependencies = [ + "id-arena", + "predicates 2.1.5", + "tracing-core", + "tracing-subscriber", + "tracing-tunnel", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2363,33 +2196,39 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ - "matchers", "nu-ansi-term", - "once_cell", - "regex", "sharded-slab", "smallvec", "thread_local", - "tracing", "tracing-core", "tracing-log", ] +[[package]] +name = "tracing-tunnel" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507bbab1fc2c9e606543558f5656b62e8014ba7e0ffa68b9484511b6871019c5" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "try-lock" version = "0.2.4" @@ -2402,31 +2241,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "ubyte" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c81f0dae7d286ad0d9366d7679a77934cfc3cf3a8d67e82669794412b2368fe6" -dependencies = [ - "serde", -] - [[package]] name = "ucd-trie" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" -[[package]] -name = "uncased" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" -dependencies = [ - "serde", - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2448,12 +2268,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "unindent" version = "0.2.3" @@ -2479,9 +2293,12 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", +] [[package]] name = "valuable" @@ -2636,15 +2453,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.1", -] - [[package]] name = "windows-core" version = "0.52.0" @@ -2777,15 +2585,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" -[[package]] -name = "winnow" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.10.1" @@ -2803,18 +2602,3 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" dependencies = [ "libc", ] - -[[package]] -name = "yansi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - -[[package]] -name = "yansi" -version = "1.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" -dependencies = [ - "is-terminal", -] diff --git a/Cargo.toml b/Cargo.toml index 0da5c5a..8adf60b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ rust-version = "1.70" crate-type = ["rlib", "cdylib"] [dependencies] -rocket = { version = "0.5.0", features = ["json"] } regex = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1" @@ -26,10 +25,16 @@ clap = { version = "4.4.8", features = ["derive"] } anyhow = "1.0.75" thiserror = "1.0.50" pyo3 = { version = "0.20.0", features = ["extension-module", "abi3-py38"], optional = true } -rocket_prometheus = "0.10.0" prometheus = "0.13.3" -log = "0.4.20" -tokio = "1.35.1" +tokio = { version = "1.35.1", features = ["fs", "rt-multi-thread"] } +axum = "0.7.4" +tracing-subscriber = "0.3.18" +tracing = "0.1.40" +tower-http = { version = "0.5.1", features = ["trace", "catch-panic", "request-id", "util"] } +tokio-util = { version = "0.7.10", features = ["io"] } +futures = "0.3.30" +tower = "0.4.13" +mime = "0.3.17" [dev-dependencies] assert_cmd = "2.0.6" @@ -40,6 +45,7 @@ tempdir = "0.3.7" tar = "0.4.38" chrono = "0.4.33" rand = "0.8.5" +tracing-capture = "0.1.0" [features] python = [ "dep:pyo3" ] diff --git a/Dockerfile b/Dockerfile index 0cddca9..5452b3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,6 @@ RUN apt-get -yq update && \ apt-get -yqq install openssh-client git COPY --from=builder /usr/local/cargo/bin/* /usr/local/bin/ -COPY --from=builder /usr/src/outpack_server/Rocket.toml . COPY start-with-wait /usr/local/bin EXPOSE 8000 ENTRYPOINT ["start-with-wait"] diff --git a/Rocket.toml b/Rocket.toml deleted file mode 100644 index d42993d..0000000 --- a/Rocket.toml +++ /dev/null @@ -1,4 +0,0 @@ -[global] -address = "0.0.0.0" -port = 8000 -log_level = "normal" diff --git a/src/api.rs b/src/api.rs index 6a140e1..00f1c11 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,23 +1,27 @@ use anyhow::{bail, Context}; -use rocket::fs::TempFile; -use rocket::serde::json::{Error, Json}; -use rocket::serde::{Deserialize, Serialize}; -use rocket::State; -use rocket::{catch, catchers, routes, Build, Request, Rocket}; +use axum::extract::rejection::JsonRejection; +use axum::extract::{self, Query, State}; +use axum::response::IntoResponse; +use axum::response::Response; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use std::any::Any; use std::io::ErrorKind; use std::path::{Path, PathBuf}; +use tower_http::catch_panic::CatchPanicLayer; +use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer}; +use tower_http::trace::TraceLayer; use crate::config; use crate::hash; use crate::location; use crate::metadata; use crate::metrics; -use crate::responses; use crate::store; -use rocket_prometheus::PrometheusMetrics; use crate::outpack_file::OutpackFile; -use responses::{FailResponse, OutpackError, OutpackSuccess}; +use crate::responses::{OutpackError, OutpackSuccess}; +use crate::upload::{Upload, UploadLayer}; type OutpackResult = Result, OutpackError>; @@ -29,142 +33,136 @@ pub struct ApiRoot { pub schema_version: String, } -#[catch(500)] -fn internal_error(_req: &Request) -> Json { - Json(FailResponse::from(OutpackError { +fn internal_error(_err: Box) -> Response { + OutpackError { error: String::from("UNKNOWN_ERROR"), detail: String::from("Something went wrong"), kind: Some(ErrorKind::Other), - })) + } + .into_response() } -#[catch(404)] -fn not_found(_req: &Request) -> Json { - Json(FailResponse::from(OutpackError { +async fn not_found() -> OutpackError { + OutpackError { error: String::from("NOT_FOUND"), detail: String::from("This route does not exist"), kind: Some(ErrorKind::NotFound), - })) -} - -#[catch(400)] -fn bad_request(_req: &Request) -> Json { - Json(FailResponse::from(OutpackError { - error: String::from("BAD_REQUEST"), - detail: String::from( - "The request could not be understood by the server due to malformed syntax", - ), - kind: Some(ErrorKind::InvalidInput), - })) + } } -#[rocket::get("/")] -fn index() -> OutpackResult { - Ok(ApiRoot { +async fn index() -> OutpackResult { + Ok(OutpackSuccess::from(ApiRoot { schema_version: String::from("0.1.1"), - } - .into()) + })) } -#[rocket::get("/metadata/list")] -fn list_location_metadata(root: &State) -> OutpackResult> { - location::read_locations(root) +async fn list_location_metadata( + root: State, +) -> OutpackResult> { + location::read_locations(&root) .map_err(OutpackError::from) .map(OutpackSuccess::from) } -#[rocket::get("/packit/metadata?")] -fn get_metadata( - root: &State, +#[derive(Deserialize)] +struct KnownSince { known_since: Option, +} +async fn get_metadata_since( + root: State, + query: Query, ) -> OutpackResult> { - metadata::get_packit_metadata_from_date(root, known_since) + metadata::get_packit_metadata_from_date(&root, query.known_since) .map_err(OutpackError::from) .map(OutpackSuccess::from) } -#[rocket::get("/metadata//json")] -fn get_metadata_by_id(root: &State, id: &str) -> OutpackResult { - metadata::get_metadata_by_id(root, id) +async fn get_metadata_by_id( + root: State, + id: extract::Path, +) -> OutpackResult { + metadata::get_metadata_by_id(&root, &id) .map_err(OutpackError::from) .map(OutpackSuccess::from) } -#[rocket::get("/metadata//text")] -fn get_metadata_raw(root: &State, id: &str) -> Result { - metadata::get_metadata_text(root, id).map_err(OutpackError::from) +async fn get_metadata_raw( + root: State, + id: extract::Path, +) -> Result { + metadata::get_metadata_text(&root, &id).map_err(OutpackError::from) } -#[rocket::get("/file/")] -async fn get_file(root: &State, hash: &str) -> Result { - let path = store::file_path(root, hash); +async fn get_file( + root: State, + hash: extract::Path, +) -> Result { + let path = store::file_path(&root, &hash); OutpackFile::open(hash.to_owned(), path?) .await .map_err(OutpackError::from) } -#[rocket::get("/checksum?")] -async fn get_checksum(root: &State, alg: Option) -> OutpackResult { - metadata::get_ids_digest(root, alg) +#[derive(Deserialize)] +struct Algorithm { + alg: Option, +} + +async fn get_checksum(root: State, query: Query) -> OutpackResult { + metadata::get_ids_digest(&root, query.0.alg) .map_err(OutpackError::from) .map(OutpackSuccess::from) } -#[rocket::post("/packets/missing", format = "json", data = "")] async fn get_missing_packets( - root: &State, - ids: Result, Error<'_>>, + root: State, + ids: Result, JsonRejection>, ) -> OutpackResult> { let ids = ids?; - metadata::get_missing_ids(root, &ids.ids, ids.unpacked) + metadata::get_missing_ids(&root, &ids.ids, ids.unpacked) .map_err(OutpackError::from) .map(OutpackSuccess::from) } -#[rocket::post("/files/missing", format = "json", data = "")] async fn get_missing_files( - root: &State, - hashes: Result, Error<'_>>, + root: State, + hashes: Result, JsonRejection>, ) -> OutpackResult> { let hashes = hashes?; - store::get_missing_files(root, &hashes.hashes) + store::get_missing_files(&root, &hashes.hashes) .map_err(OutpackError::from) .map(OutpackSuccess::from) } -#[rocket::post("/file/", format = "binary", data = "")] async fn add_file( - root: &State, - hash: &str, - file: TempFile<'_>, + root: State, + hash: extract::Path, + file: Upload, ) -> Result, OutpackError> { - store::put_file(root, file, hash) + store::put_file(&root, file, &hash) .await .map_err(OutpackError::from) .map(OutpackSuccess::from) } -#[rocket::post("/packet/", format = "plain", data = "")] async fn add_packet( - root: &State, - hash: &str, - packet: &str, + root: State, + hash: extract::Path, + packet: String, ) -> Result, OutpackError> { let hash = hash.parse::().map_err(OutpackError::from)?; - metadata::add_packet(root, packet, &hash) + metadata::add_packet(&root, &packet, &hash) .map_err(OutpackError::from) .map(OutpackSuccess::from) } #[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] struct Ids { ids: Vec, unpacked: bool, } #[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] struct Hashes { hashes: Vec, } @@ -205,35 +203,68 @@ pub fn preflight(root: &Path) -> anyhow::Result<()> { Ok(()) } -fn api_build(root: &Path) -> Rocket { - let prometheus = PrometheusMetrics::new(); - metrics::register(prometheus.registry(), root).expect("metrics registered"); - rocket::build() - .manage(root.to_owned()) - .attach(prometheus.clone()) - .mount("/metrics", prometheus) - .register("/", catchers![internal_error, not_found, bad_request]) - .mount( - "/", - routes![ - index, - list_location_metadata, - get_metadata, - get_metadata_by_id, - get_metadata_raw, - get_file, - get_checksum, - get_missing_packets, - get_missing_files, - add_file, - add_packet - ], - ) +fn make_request_span(request: &axum::extract::Request) -> tracing::span::Span { + let request_id = String::from_utf8_lossy(request.headers()["x-request-id"].as_bytes()); + tracing::span!( + tracing::Level::DEBUG, + "request", + method = tracing::field::display(request.method()), + uri = tracing::field::display(request.uri()), + version = tracing::field::debug(request.version()), + request_id = tracing::field::display(request_id) + ) } -pub fn api(root: &Path) -> anyhow::Result> { +pub fn api(root: &Path) -> anyhow::Result { + use axum::routing::{get, post}; + + let registry = prometheus::Registry::new(); + + metrics::RepositoryMetrics::register(®istry, root).expect("repository metrics registered"); + let http_metrics = metrics::HttpMetrics::register(®istry).expect("http metrics registered"); + preflight(root)?; - Ok(api_build(root)) + + let routes = Router::new() + .route("/", get(index)) + .route("/metadata/list", get(list_location_metadata)) + .route("/metadata/:id/json", get(get_metadata_by_id)) + .route("/metadata/:id/text", get(get_metadata_raw)) + .route("/checksum", get(get_checksum)) + .route("/packets/missing", post(get_missing_packets)) + .route("/files/missing", post(get_missing_files)) + .route("/packit/metadata", get(get_metadata_since)) + .route("/file/:hash", get(get_file).post(add_file)) + .route("/packet/:hash", post(add_packet)) + .route("/metrics", get(|| async move { metrics::render(registry) })) + .fallback(not_found) + .with_state(root.to_owned()); + + Ok(routes + .layer(UploadLayer::new(root.join(".outpack").join("files"))) + .layer(TraceLayer::new_for_http().make_span_with(make_request_span)) + .layer(PropagateRequestIdLayer::x_request_id()) + .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) + .layer(CatchPanicLayer::custom(internal_error)) + .layer(http_metrics.layer())) +} + +pub fn serve(root: &Path) -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); + + let app = api(root)?; + + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()? + .block_on(async { + let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await?; + tracing::info!("listening on {}", listener.local_addr().unwrap()); + axum::serve(listener, app).await?; + Ok(()) + }) } #[cfg(test)] diff --git a/src/bin/outpack/main.rs b/src/bin/outpack/main.rs index f954577..de7d4d4 100644 --- a/src/bin/outpack/main.rs +++ b/src/bin/outpack/main.rs @@ -28,8 +28,7 @@ fn main() -> anyhow::Result<()> { } Command::StartServer { root } => { - let server = outpack::api::api(&root)?; - rocket::execute(server.launch())?; + outpack::api::serve(&root)?; } } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 2b755a6..c252f0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,4 +14,5 @@ mod metrics; mod outpack_file; mod responses; mod store; +mod upload; mod utils; diff --git a/src/metrics.rs b/src/metrics.rs index eaf365e..0711fe6 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -1,12 +1,20 @@ use crate::metadata; use crate::store; -use prometheus::{core::Collector, core::Desc, IntGauge, Opts, Registry}; +use axum::extract::{MatchedPath, Request, State}; +use axum::middleware::Next; +use axum::response::{IntoResponse, Response}; +use futures::future::{BoxFuture, FutureExt}; +use prometheus::{ + core::Collector, core::Desc, Encoder, HistogramOpts, HistogramVec, IntCounterVec, IntGauge, + IntGaugeVec, Opts, Registry, +}; use std::path::{Path, PathBuf}; +use std::time::Instant; /// A prometheus collector with metrics for the state of the repository. /// /// The metrics are collected lazily whenever the metrics endpoint is called. -struct RepositoryCollector { +pub struct RepositoryMetrics { root: PathBuf, metadata_total: IntGauge, packets_total: IntGauge, @@ -15,8 +23,12 @@ struct RepositoryCollector { descs: Vec, } -impl RepositoryCollector { - pub fn new(root: impl Into) -> RepositoryCollector { +impl RepositoryMetrics { + pub fn register(registry: &Registry, root: &Path) -> prometheus::Result<()> { + registry.register(Box::new(RepositoryMetrics::new(root))) + } + + pub fn new(root: impl Into) -> RepositoryMetrics { let namespace = "outpack_server"; let make_opts = |name: &str, help: &str| Opts::new(name, help).namespace(namespace); @@ -49,7 +61,7 @@ impl RepositoryCollector { descs.extend(packets_total.desc().into_iter().cloned()); descs.extend(files_total.desc().into_iter().cloned()); descs.extend(file_size_bytes_total.desc().into_iter().cloned()); - RepositoryCollector { + RepositoryMetrics { root: root.into(), metadata_total, packets_total, @@ -79,7 +91,7 @@ impl RepositoryCollector { } } -impl Collector for RepositoryCollector { +impl Collector for RepositoryMetrics { fn desc(&self) -> Vec<&prometheus::core::Desc> { self.descs.iter().collect() } @@ -87,7 +99,7 @@ impl Collector for RepositoryCollector { fn collect(&self) -> Vec { let mut metrics = Vec::new(); if let Err(e) = self.update() { - log::error!("error while collecting repository metrics: {}", e); + tracing::error!("error while collecting repository metrics: {}", e); } else { metrics.extend(self.metadata_total.collect()); metrics.extend(self.packets_total.collect()); @@ -98,9 +110,118 @@ impl Collector for RepositoryCollector { } } -pub fn register(registry: &Registry, root: &Path) -> prometheus::Result<()> { - registry.register(Box::new(RepositoryCollector::new(root)))?; - Ok(()) +#[derive(Clone)] +pub struct HttpMetrics { + requests_total: IntCounterVec, + requests_duration_seconds: HistogramVec, + requests_in_flight: IntGaugeVec, +} + +// The type returned by `HttpMetrics::layer()`. Unfortunately it is a might of a mouthful. +pub type HttpMetricsLayer = axum::middleware::FromFnLayer< + fn(State, Request, Next) -> BoxFuture<'static, Response>, + HttpMetrics, + (State, Request), +>; + +impl HttpMetrics { + /// Create and register HTTP metrics. + /// + /// The returned object should be used to add a layer to axum router, using the `layer` method. + pub fn register(registry: &Registry) -> prometheus::Result { + let metrics = HttpMetrics::new(); + registry.register(Box::new(metrics.requests_total.clone()))?; + registry.register(Box::new(metrics.requests_duration_seconds.clone()))?; + registry.register(Box::new(metrics.requests_in_flight.clone()))?; + Ok(metrics) + } + + pub fn new() -> HttpMetrics { + HttpMetrics { + requests_total: IntCounterVec::new( + Opts::new("requests_total", "Total number of HTTP requests").namespace("http"), + &["endpoint", "method", "status"], + ) + .unwrap(), + + requests_duration_seconds: HistogramVec::new( + HistogramOpts::new( + "requests_duration_seconds", + "HTTP request duration in seconds for all requests", + ) + .namespace("http"), + &["endpoint", "method", "status"], + ) + .unwrap(), + + requests_in_flight: IntGaugeVec::new( + Opts::new( + "requests_in_flight", + "Number of HTTP requests currently in-flight", + ) + .namespace("http"), + &["endpoint", "method"], + ) + .unwrap(), + } + } + + /// Create a `Layer` that can be added to an Axum router to record request metrics. + pub fn layer(&self) -> HttpMetricsLayer { + axum::middleware::from_fn_with_state(self.clone(), |State(metrics), request, next| { + metrics.track(request, next).boxed() + }) + } + + /// Execute a request and record associated metrics. + async fn track(self, req: Request, next: Next) -> Response { + let start = Instant::now(); + + // We only record metrics for paths that matched a route, using the endpoint string with + // placeholders. If we were to use the full path we'd be at risk of blowing up the metrics' + // cardinality by creating a set of metric for every possible request URL. + // TODO(mrc-5003): at some point we should record unmatched paths too using a catch-all + // metric. + let Some(path) = req.extensions().get::().cloned() else { + return next.run(req).await; + }; + + let method = req.method().clone(); + + self.requests_in_flight + .with_label_values(&[path.as_str(), method.as_ref()]) + .inc(); + + let response = next.run(req).await; + + self.requests_in_flight + .with_label_values(&[path.as_str(), method.as_ref()]) + .dec(); + + let duration = start.elapsed().as_secs_f64(); + let status = response.status().as_u16().to_string(); + + self.requests_total + .with_label_values(&[path.as_str(), method.as_ref(), &status]) + .inc(); + + self.requests_duration_seconds + .with_label_values(&[path.as_str(), method.as_ref(), &status]) + .observe(duration); + + response + } +} + +/// Render the metrics from a `prometheus::Registry` into an HTTP response. +pub fn render(registry: Registry) -> impl IntoResponse { + let mut buffer = vec![]; + let encoder = prometheus::TextEncoder::new(); + let metrics = registry.gather(); + encoder.encode(&metrics, &mut buffer).unwrap(); + + let headers = [(axum::http::header::CONTENT_TYPE, prometheus::TEXT_FORMAT)]; + (headers, buffer) } #[cfg(test)] @@ -112,10 +233,17 @@ mod tests { use crate::store::put_file; use crate::test_utils::tests::{get_empty_outpack_root, start_packet}; + use axum::body::Body; + use axum::http::StatusCode; + use axum::Router; + use std::sync::Arc; + use tokio::sync::Barrier; + use tower::Service; + #[test] fn repository_collector_empty_repo() { let root = get_empty_outpack_root(); - let collector = RepositoryCollector::new(root); + let collector = RepositoryMetrics::new(root); assert_eq!(collector.metadata_total.get(), 0); assert_eq!(collector.packets_total.get(), 0); @@ -126,7 +254,7 @@ mod tests { #[tokio::test] async fn repository_collector_files() { let root = get_empty_outpack_root(); - let collector = RepositoryCollector::new(&root); + let collector = RepositoryMetrics::new(&root); let data1 = b"Testing 123"; let hash1 = hash_data(data1, HashAlgorithm::Sha256).to_string(); @@ -147,7 +275,7 @@ mod tests { #[test] fn repository_collector_packets() { let root = get_empty_outpack_root(); - let collector = RepositoryCollector::new(&root); + let collector = RepositoryMetrics::new(&root); // Create two different packets. // One of them is actually added to the repository. @@ -162,4 +290,92 @@ mod tests { assert_eq!(collector.metadata_total.get(), 2); assert_eq!(collector.packets_total.get(), 1); } + + #[tokio::test] + async fn http_metrics() { + use axum::routing::{get, post}; + let metrics = HttpMetrics::new(); + + let mut router = Router::<()>::new() + .route("/", get(())) + .route("/error", get(StatusCode::BAD_REQUEST)) + .route("/upload", post(())) + .route("/match/:id", get(())) + .layer(metrics.layer()); + + let mut send = |method: &str, path: &str| { + let request = Request::builder() + .method(method) + .uri(path) + .body(Body::empty()) + .unwrap(); + router.call(request) + }; + + let get_metric = |labels| metrics.requests_total.with_label_values(labels).get(); + + send("GET", "/").await.unwrap(); + send("GET", "/").await.unwrap(); + send("GET", "/error").await.unwrap(); + send("POST", "/upload").await.unwrap(); + send("GET", "/match/1234").await.unwrap(); + send("GET", "/match/5678").await.unwrap(); + + assert_eq!(get_metric(&["/", "GET", "200"]), 2); + assert_eq!(get_metric(&["/error", "GET", "400"]), 1); + assert_eq!(get_metric(&["/upload", "POST", "200"]), 1); + assert_eq!(get_metric(&["/match/:id", "GET", "200"]), 2); + } + + #[tokio::test] + async fn http_in_flight_metric() { + // Testing the in-flight metric needs a bit of coordination, since we need to read the + // value while the request handlers are all executing. + // + // We use a pair of barriers: the first barrier is used to wait for all the request handlers + // to be executing, and the second barrier is used to stop the barriers from exiting. In + // between those two barriers, the main task can read the metric and get an accurate value + // out of it. + + use axum::routing::get; + let request_count = 4; + let metrics = HttpMetrics::new(); + let barriers = Arc::new(( + Barrier::new(request_count + 1), + Barrier::new(request_count + 1), + )); + + let barriers_ = barriers.clone(); + let endpoint = || async move { + barriers_.0.wait().await; + barriers_.1.wait().await; + }; + + let mut router = Router::new() + .route("/:count", get(endpoint)) + .layer(metrics.layer()); + + let metric = metrics + .requests_in_flight + .with_label_values(&["/:count", "GET"]); + + assert_eq!(metric.get(), 0); + + let mut requests = Vec::new(); + for i in 0..request_count { + let path = format!("/{i}"); + let f = tokio::spawn(router.call(Request::get(&path).body(Body::empty()).unwrap())); + requests.push(f); + } + + barriers.0.wait().await; + assert_eq!(metric.get(), request_count as i64); + barriers.1.wait().await; + + for r in requests { + r.await.unwrap().unwrap(); + } + + assert_eq!(metric.get(), 0); + } } diff --git a/src/outpack_file.rs b/src/outpack_file.rs index dbe8d1f..58164a2 100644 --- a/src/outpack_file.rs +++ b/src/outpack_file.rs @@ -1,11 +1,10 @@ -use rocket::http::ContentType; -use rocket::tokio::fs::File; +use axum::body::Body; +use axum::response::Response; use std::io; use std::io::ErrorKind; use std::path::Path; - -use rocket::response::{Responder, Result}; -use rocket::Request; +use tokio::fs::File; +use tokio_util::io::ReaderStream; pub struct OutpackFile { hash: String, @@ -28,18 +27,17 @@ impl OutpackFile { } } -impl<'r> Responder<'r, 'static> for OutpackFile { - fn respond_to(self, request: &'r Request<'_>) -> Result<'static> { - use rocket::http::hyper::header::*; - - let content_type = ContentType::Binary.to_string(); +impl axum::response::IntoResponse for OutpackFile { + fn into_response(self) -> Response { + use axum::http::header::*; + let stream = ReaderStream::new(self.file); let content_disposition = format!("attachment; filename=\"{}\"", self.hash); - let mut response = self.file.respond_to(request)?; - response.set_raw_header(CONTENT_TYPE.as_str(), content_type); - response.set_raw_header(CONTENT_DISPOSITION.as_str(), content_disposition); - response.set_raw_header(CONTENT_LENGTH.as_str(), self.size.to_string()); - - Ok(response) + Response::builder() + .header(CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM.as_ref()) + .header(CONTENT_DISPOSITION, content_disposition) + .header(CONTENT_LENGTH, self.size) + .body(Body::from_stream(stream)) + .unwrap() } } diff --git a/src/responses.rs b/src/responses.rs index 171a5f3..6f9f147 100644 --- a/src/responses.rs +++ b/src/responses.rs @@ -1,18 +1,17 @@ -use rocket::http::{ContentType, Status}; -use rocket::response::Responder; -use rocket::serde::json::{json, Json}; -use rocket::serde::{json, Deserialize, Serialize}; -use rocket::{Request, Response}; +use axum::extract::rejection::JsonRejection; +use axum::http::StatusCode; +use serde::{Deserialize, Serialize}; use std::io; use std::io::ErrorKind; use crate::hash; -#[derive(Responder)] -#[response(status = 200, content_type = "json")] -pub struct OutpackSuccess { - inner: Json>, - header: ContentType, +pub struct OutpackSuccess(T); + +impl From for OutpackSuccess { + fn from(inner: T) -> Self { + Self(inner) + } } #[derive(Serialize, Deserialize, Debug)] @@ -46,32 +45,16 @@ impl From for OutpackError { } } -impl From> for OutpackError { - fn from(e: json::Error) -> Self { - match e { - json::Error::Io(err) => OutpackError::from(err), - json::Error::Parse(_str, err) => OutpackError::from(io::Error::from(err)), +impl From for OutpackError { + fn from(e: JsonRejection) -> Self { + OutpackError { + error: e.to_string(), + detail: e.body_text(), + kind: Some(std::io::ErrorKind::InvalidInput), } } } -impl<'r> Responder<'r, 'static> for OutpackError { - fn respond_to(self, req: &'r Request<'_>) -> rocket::response::Result<'static> { - let kind = self.kind; - let json = FailResponse::from(self); - let status = match kind { - Some(ErrorKind::NotFound) => Status::NotFound, - Some(ErrorKind::InvalidInput) => Status::BadRequest, - Some(ErrorKind::UnexpectedEof) => Status::BadRequest, - _ => Status::InternalServerError, - }; - Response::build_from(json!(json).respond_to(req).unwrap()) - .status(status) - .header(ContentType::JSON) - .ok() - } -} - #[derive(Serialize, Deserialize, Debug)] pub struct SuccessResponse { pub status: String, @@ -86,25 +69,32 @@ pub struct FailResponse { pub errors: Option>, } -impl From for FailResponse { - fn from(e: OutpackError) -> Self { - FailResponse { - status: String::from("failure"), - data: None, - errors: Some(Vec::from([e])), - } +impl axum::response::IntoResponse for OutpackSuccess { + fn into_response(self) -> axum::http::Response { + axum::Json(SuccessResponse { + status: String::from("success"), + data: self.0, + errors: None, + }) + .into_response() } } -impl From for OutpackSuccess { - fn from(obj: T) -> Self { - OutpackSuccess { - inner: Json(SuccessResponse { - status: String::from("success"), - data: obj, - errors: None, - }), - header: ContentType::JSON, - } +impl axum::response::IntoResponse for OutpackError { + fn into_response(self) -> axum::http::Response { + let status = match self.kind { + Some(ErrorKind::NotFound) => StatusCode::NOT_FOUND, + Some(ErrorKind::InvalidInput) => StatusCode::BAD_REQUEST, + Some(ErrorKind::UnexpectedEof) => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + + let body = axum::Json(FailResponse { + status: "failure".to_owned(), + data: None, + errors: Some(vec![self]), + }); + + (status, body).into_response() } } diff --git a/src/store.rs b/src/store.rs index 87b3262..d8465c0 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,65 +1,10 @@ -use rocket::fs::TempFile; use std::path::{Path, PathBuf}; use std::{fs, io}; use tempfile::tempdir_in; use walkdir::{DirEntry, WalkDir}; use crate::hash; - -// Workaround for https://github.com/rwf2/Rocket/pull/2668 -// `TempFile::copy_to` has a bug where the function returns before the file has -// been written. The function below is a copy of the merged fixed. -// Once the fix is released we can remove this function. -async fn copy_to(file: &mut TempFile<'_>, path: impl AsRef) -> io::Result<()> { - match *file { - TempFile::File { .. } => { - // This code path is fine even in the original implementation, so we can delegate to - // that. - file.copy_to(path).await - } - TempFile::Buffered { content } => { - let path = path.as_ref(); - tokio::fs::write(&path, &content).await?; - *file = TempFile::File { - file_name: None, - content_type: None, - path: rocket::Either::Right(path.to_path_buf()), - len: content.len() as u64, - }; - Ok(()) - } - } -} - -/// A conversion trait for Rocket's `TempFile` type. -/// -/// The trait allows us to write methods that accept a `TempFile` object while allowing the method -/// be called concisely from tests that don't care about rocket. -/// -/// It is implemented for either `TempFile` or byte slices. -pub trait IntoTempFile<'a> { - fn into_temp_file(self) -> TempFile<'a>; -} - -impl<'a> IntoTempFile<'a> for TempFile<'a> { - fn into_temp_file(self) -> TempFile<'a> { - self - } -} - -impl<'a> IntoTempFile<'a> for &'a [u8] { - fn into_temp_file(self) -> TempFile<'a> { - TempFile::Buffered { content: self } - } -} - -impl<'a, const N: usize> IntoTempFile<'a> for &'a [u8; N] { - fn into_temp_file(self) -> TempFile<'a> { - TempFile::Buffered { - content: self.as_ref(), - } - } -} +use crate::upload::Upload; pub fn file_path(root: &Path, hash: &str) -> io::Result { let parsed: hash::Hash = hash.parse().map_err(hash::hash_error_to_io_error)?; @@ -87,12 +32,11 @@ pub fn get_missing_files(root: &Path, wanted: &[String]) -> io::Result, hash: &str) -> io::Result<()> { - let mut file = file.into_temp_file(); +pub async fn put_file(root: &Path, file: impl Into, hash: &str) -> io::Result<()> { let temp_dir = tempdir_in(root)?; let temp_path = temp_dir.path().join("data"); - copy_to(&mut file, &temp_path).await?; + file.into().persist(&temp_path).await?; hash::validate_hash_file(&temp_path, hash).map_err(hash::hash_error_to_io_error)?; let path = file_path(root, hash)?; diff --git a/src/upload.rs b/src/upload.rs new file mode 100644 index 0000000..2493d81 --- /dev/null +++ b/src/upload.rs @@ -0,0 +1,164 @@ +use crate::responses::OutpackError; +use axum::body::Bytes; +use axum::extract::{FromRequest, FromRequestParts, Request}; +use axum::Extension; +use futures::{Stream, TryStreamExt}; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tempfile::{NamedTempFile, TempPath}; +use tokio::io::AsyncWriteExt; +use tokio_util::io::StreamReader; +use tower::Layer; + +#[derive(Clone)] +pub struct UploadConfig { + directory: Arc, +} + +#[derive(Clone)] +pub struct UploadLayer { + config: UploadConfig, +} + +impl UploadLayer { + /// Create an axum layer that configures the upload directory. + pub fn new(path: impl Into) -> UploadLayer { + UploadLayer { + config: UploadConfig { + directory: Arc::new(path.into()), + }, + } + } +} + +/// An axum `Extractor` that stores the request body as a temporary file. +/// +/// The extractor can be configured by adding an `UploadLayer` to the axum Router. Request bodies +/// are stored in the configured directory. If no directory is configured, the system's default +/// temporary directory is used. +/// +/// To aid in testing, an `Upload` object can also be created from an in-memory buffer. +/// +/// This mimicks [Rocket's TempFile] type. +/// +/// [Rocket's TempFile]: https://api.rocket.rs/v0.5/rocket/fs/enum.TempFile.html +pub enum Upload { + Buffered(&'static [u8]), + File(TempPath), +} + +impl Upload { + /// Persist the temporary file to the given path. + /// + /// The file is moved to the destination path. That path must be located on the same filesystem + /// as the configured upload directory. + pub async fn persist(self, destination: &Path) -> std::io::Result<()> { + match self { + Upload::Buffered(data) => { + tokio::fs::write(destination, &data).await?; + } + Upload::File(path) => { + let destination = destination.to_owned(); + tokio::task::spawn_blocking(move || path.persist(destination).unwrap()).await? + } + } + Ok(()) + } +} + +#[axum::async_trait] +impl FromRequest for Upload +where + S: Send + Sync, +{ + type Rejection = OutpackError; + + async fn from_request(request: Request, state: &S) -> Result { + let (mut parts, body) = request.into_parts(); + + let config = Extension::::from_request_parts(&mut parts, state) + .await + .ok(); + + let file = if let Some(config) = config { + NamedTempFile::new_in(&*config.directory)? + } else { + NamedTempFile::new()? + }; + + stream_to_file(file.path(), body.into_data_stream()).await?; + + Ok(Upload::File(file.into_temp_path())) + } +} + +impl Layer for UploadLayer { + type Service = axum::middleware::AddExtension; + fn layer(&self, inner: S) -> Self::Service { + Extension(self.config.clone()).layer(inner) + } +} + +/// Stream a request body to an on-disk file. +async fn stream_to_file(path: &Path, stream: S) -> std::io::Result<()> +where + S: Stream> + Unpin, +{ + let stream = stream.map_err(|err| io::Error::new(io::ErrorKind::Other, err)); + let mut reader = StreamReader::new(stream); + + let mut file = tokio::fs::File::create(path).await?; + tokio::io::copy(&mut reader, &mut file).await?; + file.flush().await?; + + Ok(()) +} + +impl From<&'static [u8]> for Upload { + fn from(data: &'static [u8]) -> Upload { + Upload::Buffered(data) + } +} + +impl From<&'static [u8; N]> for Upload { + fn from(data: &'static [u8; N]) -> Upload { + Upload::Buffered(data) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::Body; + + #[tokio::test] + async fn upload_from_body() { + let root = tempfile::tempdir().unwrap(); + let upload_dir = root.as_ref().join("uploads"); + std::fs::create_dir_all(&upload_dir).unwrap(); + + let data: &[u8] = b"Hello, World!"; + let request = Request::get("/") + .extension(UploadConfig { + directory: Arc::new(upload_dir.clone()), + }) + .body(Body::from(data)) + .unwrap(); + + let upload = Upload::from_request(request, &()).await.unwrap(); + + match upload { + Upload::Buffered(..) => panic!("Unexpected variant"), + Upload::File(ref path) => { + assert!(path.starts_with(&upload_dir), "{:?} {:?}", path, upload_dir); + } + } + + let destination = root.as_ref().join("hello.txt"); + upload.persist(&destination).await.unwrap(); + + let contents = tokio::fs::read(&destination).await.unwrap(); + assert_eq!(contents, data); + } +} diff --git a/tests/test_api.rs b/tests/test_api.rs index b24e52a..e76839e 100644 --- a/tests/test_api.rs +++ b/tests/test_api.rs @@ -1,22 +1,26 @@ +use axum::body::Body; +use axum::extract::Request; +use axum::http::header::CONTENT_TYPE; +use axum::http::StatusCode; +use axum::response::Response; use jsonschema::{Draft, JSONSchema, SchemaResolverError}; -use rocket::http::{ContentType, Status}; -use rocket::local::blocking::Client; -use rocket::serde::{Deserialize, Serialize}; -use rocket::{Build, Rocket}; +use serde::{Deserialize, Serialize}; use serde_json::Value; use sha2::{Digest, Sha256}; use std::fs; use std::fs::File; - use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::sync::Once; use tar::Archive; use tar::Builder; use tempdir::TempDir; +use tower::Service; +use tracing::instrument::WithSubscriber; +use tracing_capture::{CaptureLayer, SharedStorage}; +use tracing_subscriber::{layer::SubscriberExt, Registry}; use url::Url; -use std::sync::Once; - static INIT: Once = Once::new(); pub fn initialize() { @@ -35,67 +39,151 @@ fn get_test_dir() -> PathBuf { tmp_dir.into_path().join("example") } -fn get_test_rocket() -> Rocket { - let root = get_test_dir(); - outpack::api::api(&root).unwrap() +/// A wrapper around the Axum router to provide simpler helper functions. +/// +/// These functions are designed to keep the test code clear and concise. As an effect, they do not +/// propagate errors and instead panic when they occur. This is acceptable in tests, but should not +/// be copied over to production code. +struct TestClient(axum::Router); + +impl TestClient { + fn new(root: impl Into) -> TestClient { + let api = outpack::api::api(&root.into()).unwrap(); + TestClient(api) + } + + async fn request(&mut self, request: Request) -> Response { + self.0.call(request).await.unwrap() + } + + async fn get(&mut self, path: impl AsRef) -> Response { + let request = Request::get(path.as_ref()).body(Body::empty()).unwrap(); + self.request(request).await + } + + async fn post( + &mut self, + path: impl AsRef, + content_type: mime::Mime, + data: impl Into, + ) -> Response { + let request = Request::post(path.as_ref()) + .header(CONTENT_TYPE, content_type.as_ref()) + .body(data.into()) + .unwrap(); + self.request(request).await + } + + async fn post_json(&mut self, path: impl AsRef, data: &T) -> Response { + self.post( + path, + mime::APPLICATION_JSON, + serde_json::to_vec(data).unwrap(), + ) + .await + } } -#[test] -fn can_get_index() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client.get("/").dispatch(); +fn get_default_client() -> TestClient { + TestClient::new(get_test_dir()) +} + +/// An extension trait implemented on the `Response` type for concise decoding. +/// +/// Decoding errors are not propagated, and these method panic instead. +#[axum::async_trait] +trait ResponseExt { + fn content_type(&self) -> mime::Mime; + async fn to_bytes(self) -> axum::body::Bytes; + async fn to_string(self) -> String; + async fn to_json Deserialize<'a>>(self) -> T; +} - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); +#[axum::async_trait] +impl ResponseExt for Response { + fn content_type(&self) -> mime::Mime { + let value = self + .headers() + .get(CONTENT_TYPE) + .expect("content type header"); - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); - validate_success("server", "root.json", &body); + value + .to_str() + .expect("Non-printable header value") + .parse() + .expect("Invalid mime type") + } + + async fn to_bytes(self) -> axum::body::Bytes { + let body = self.into_body(); + axum::body::to_bytes(body, usize::MAX).await.unwrap() + } + + async fn to_string(self) -> String { + let bytes = self.to_bytes().await; + std::str::from_utf8(&bytes) + .expect("Invalid utf-8 response") + .to_owned() + } + + async fn to_json Deserialize<'a>>(self) -> T { + serde_json::from_slice(&self.to_bytes().await).expect("Invalid json response") + } } #[test] -fn error_if_cant_get_index() { +fn error_if_invalid_root() { let res = outpack::api::api(Path::new("bad-root")); assert_eq!( res.unwrap_err().to_string(), - String::from("Outpack root not found at 'bad-root'") + "Outpack root not found at 'bad-root'" ); } -#[test] -fn can_get_checksum() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client.get("/checksum").dispatch(); +#[tokio::test] +async fn can_get_index() { + let mut client = get_default_client(); + let response = client.get("/").await; + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.to_json().await; + validate_success("server", "root.json", &body); +} - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); +#[tokio::test] +async fn can_get_checksum() { + let mut client = get_default_client(); + + let response = client.get("/checksum").await; + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); + + let body = response.to_json().await; validate_success("outpack", "hash.json", &body); - let response = client.get("/checksum?alg=md5").dispatch(); + let response = client.get("/checksum?alg=md5").await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let response_string = &response.into_string().unwrap(); - let body = serde_json::from_str(response_string).unwrap(); + let body = response.to_json().await; validate_success("outpack", "hash.json", &body); - assert!(response_string.contains("md5")) + + let hash = body["data"].as_str().unwrap(); + assert!(hash.starts_with("md5:")); } -#[test] -fn can_list_location_metadata() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client.get("/metadata/list").dispatch(); +#[tokio::test] +async fn can_list_location_metadata() { + let mut client = get_default_client(); + let response = client.get("/metadata/list").await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); - print!("{}", body); + let body = response.to_json().await; validate_success("server", "locations.json", &body); let entries = body.get("data").unwrap().as_array().unwrap(); @@ -128,28 +216,26 @@ fn can_list_location_metadata() { ); } -#[test] -fn handles_location_metadata_errors() { - let rocket = outpack::api::api(Path::new("tests/bad-example")).unwrap(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client.get("/metadata/list").dispatch(); - assert_eq!(response.status(), Status::InternalServerError); - assert_eq!(response.content_type(), Some(ContentType::JSON)); - - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); +#[tokio::test] +async fn handles_location_metadata_errors() { + let mut client = TestClient::new("tests/bad-example"); + let response = client.get("/metadata/list").await; + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); + + let body = response.to_json().await; validate_error(&body, Some("missing field `packet`")); } -#[test] -fn can_list_metadata() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client.get("/packit/metadata").dispatch(); +#[tokio::test] +async fn can_list_metadata() { + let mut client = get_default_client(); + let response = client.get("/packit/metadata").await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; print!("{}", body); validate_success("server", "list.json", &body); @@ -202,18 +288,15 @@ fn can_list_metadata() { ); } -#[test] -fn can_list_metadata_from_date() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client - .get("/packit/metadata?known_since=1662480556") - .dispatch(); +#[tokio::test] +async fn can_list_metadata_from_date() { + let mut client = get_default_client(); + let response = client.get("/packit/metadata?known_since=1662480556").await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; print!("{}", body); validate_success("server", "list.json", &body); @@ -225,95 +308,86 @@ fn can_list_metadata_from_date() { ); } -#[test] -fn handles_metadata_errors() { - let rocket = outpack::api::api(Path::new("tests/bad-example")).unwrap(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client.get("/packit/metadata").dispatch(); - assert_eq!(response.status(), Status::InternalServerError); - assert_eq!(response.content_type(), Some(ContentType::JSON)); - - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); +#[tokio::test] +async fn handles_metadata_errors() { + let mut client = TestClient::new("tests/bad-example"); + let response = client.get("/packit/metadata").await; + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); + + let body = response.to_json().await; validate_error(&body, Some("missing field `name`")); } -#[test] -fn can_get_metadata_json() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client - .get("/metadata/20180818-164043-7cdcde4b/json") - .dispatch(); +#[tokio::test] +async fn can_get_metadata_json() { + let mut client = get_default_client(); + let response = client.get("/metadata/20180818-164043-7cdcde4b/json").await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_success("outpack", "metadata.json", &body); } -#[test] -fn can_get_metadata_text() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client - .get("/metadata/20180818-164043-7cdcde4b/text") - .dispatch(); +#[tokio::test] +async fn can_get_metadata_text() { + let mut client = get_default_client(); + let response = client.get("/metadata/20180818-164043-7cdcde4b/text").await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::Text)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::TEXT_PLAIN_UTF_8); let expected = fs::File::open(Path::new( "tests/example/.outpack/metadata/20180818-164043-7cdcde4b", )) .unwrap(); - let result: Value = serde_json::from_str(&response.into_string().unwrap()[..]).unwrap(); + + let result: Value = response.to_json().await; let expected: Value = serde_json::from_reader(expected).unwrap(); assert_eq!(result, expected); } -#[test] -fn returns_404_if_packet_not_found() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client.get("/metadata/bad-id/json").dispatch(); +#[tokio::test] +async fn returns_404_if_packet_not_found() { + let mut client = get_default_client(); + let response = client.get("/metadata/bad-id/json").await; - assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_error(&body, Some("packet with id 'bad-id' does not exist")) } -#[test] -fn can_get_file() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn can_get_file() { + let mut client = get_default_client(); let hash = "sha256:b189579a9326f585d308304bd9e03326be5d395ac71b31df359ab8bac408d248"; - let response = client.get(format!("/file/{}", hash)).dispatch(); + let response = client.get(format!("/file/{}", hash)).await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::Binary)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_OCTET_STREAM); let path = Path::new("tests/example/.outpack/files/sha256/b1/") .join("89579a9326f585d308304bd9e03326be5d395ac71b31df359ab8bac408d248"); let expected = fs::read(path).unwrap(); - assert_eq!(response.into_bytes().unwrap(), expected); + assert_eq!(response.to_bytes().await, expected); } -#[test] -fn returns_404_if_file_not_found() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn returns_404_if_file_not_found() { + let mut client = get_default_client(); let hash = "sha256:123456"; - let response = client.get(format!("/file/{}", hash)).dispatch(); + let response = client.get(format!("/file/{}", hash)).await; - assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_error(&body, Some("hash 'sha256:123456' not found")) } @@ -323,48 +397,50 @@ struct Ids { unpacked: bool, } -#[test] -fn can_get_missing_ids() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn can_get_missing_ids() { + let mut client = get_default_client(); let response = client - .post("/packets/missing") - .json(&Ids { - ids: vec![ - "20180818-164043-7cdcde4b".to_string(), - "20170818-164830-33e0ab01".to_string(), - ], - unpacked: false, - }) - .dispatch(); - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); - - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + .post_json( + "/packets/missing", + &Ids { + ids: vec![ + "20180818-164043-7cdcde4b".to_string(), + "20170818-164830-33e0ab01".to_string(), + ], + unpacked: false, + }, + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); + + let body = response.to_json().await; validate_success("server", "ids.json", &body); let entries = body.get("data").unwrap().as_array().unwrap(); assert_eq!(entries.len(), 0); } -#[test] -fn can_get_missing_unpacked_ids() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn can_get_missing_unpacked_ids() { + let mut client = get_default_client(); let response = client - .post("/packets/missing") - .json(&Ids { - ids: vec![ - "20170818-164847-7574883b".to_string(), - "20170818-164830-33e0ab02".to_string(), - ], - unpacked: true, - }) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); - - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + .post_json( + "/packets/missing", + &Ids { + ids: vec![ + "20170818-164847-7574883b".to_string(), + "20170818-164830-33e0ab02".to_string(), + ], + unpacked: true, + }, + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); + + let body = response.to_json().await; validate_success("server", "ids.json", &body); let entries = body.get("data").unwrap().as_array().unwrap(); assert_eq!(entries.len(), 1); @@ -374,35 +450,34 @@ fn can_get_missing_unpacked_ids() { ); } -#[test] -fn missing_packets_propagates_errors() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn missing_packets_propagates_errors() { + let mut client = get_default_client(); let response = client - .post("/packets/missing") - .json(&Ids { - ids: vec!["badid".to_string()], - unpacked: true, - }) - .dispatch(); - - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + .post_json( + "/packets/missing", + &Ids { + ids: vec!["badid".to_string()], + unpacked: true, + }, + ) + .await; + + let body = response.to_json().await; validate_error(&body, Some("Invalid packet id")); } -#[test] -fn missing_packets_validates_request_body() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn missing_packets_validates_request_body() { + let mut client = get_default_client(); let response = client - .post("/packets/missing") - .header(ContentType::JSON) - .dispatch(); + .post("/packets/missing", mime::APPLICATION_JSON, Body::empty()) + .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_error(&body, Some("EOF while parsing a value at line 1 column 0")); } @@ -411,26 +486,27 @@ struct Hashes { hashes: Vec, } -#[test] -fn can_get_missing_files() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn can_get_missing_files() { + let mut client = get_default_client(); let response = client - .post("/files/missing") - .json(&Hashes { - hashes: vec![ - "sha256:b189579a9326f585d308304bd9e03326be5d395ac71b31df359ab8bac408d248" - .to_string(), - "sha256:a189579a9326f585d308304bd9e03326be5d395ac71b31df359ab8bac408d247" - .to_string(), - ], - }) - .dispatch(); - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); - - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + .post_json( + "/files/missing", + &Hashes { + hashes: vec![ + "sha256:b189579a9326f585d308304bd9e03326be5d395ac71b31df359ab8bac408d248" + .to_string(), + "sha256:a189579a9326f585d308304bd9e03326be5d395ac71b31df359ab8bac408d247" + .to_string(), + ], + }, + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); + + let body = response.to_json().await; validate_success("server", "hashes.json", &body); let entries = body.get("data").unwrap().as_array().unwrap(); assert_eq!(entries.len(), 1); @@ -440,57 +516,56 @@ fn can_get_missing_files() { ); } -#[test] -fn missing_files_propagates_errors() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn missing_files_propagates_errors() { + let mut client = get_default_client(); let response = client - .post("/files/missing") - .json(&Hashes { - hashes: vec!["badhash".to_string()], - }) - .dispatch(); - - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + .post_json( + "/files/missing", + &Hashes { + hashes: vec!["badhash".to_string()], + }, + ) + .await; + + let body = response.to_json().await; validate_error(&body, Some("Invalid hash format 'badhash'")); } -#[test] -fn missing_files_validates_request_body() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn missing_files_validates_request_body() { + let mut client = get_default_client(); let response = client - .post("/files/missing") - .header(ContentType::JSON) - .dispatch(); + .post("/files/missing", mime::APPLICATION_JSON, Body::empty()) + .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body: Value = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_error(&body, Some("EOF while parsing a value at line 1 column 0")); } -#[test] -fn can_post_file() { - let root = get_test_dir(); - let rocket = outpack::api::api(&root).unwrap(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn can_post_file() { + let mut client = get_default_client(); let content = "test"; let hash = format!( "sha256:{:x}", Sha256::new().chain_update(content).finalize() ); let response = client - .post(format!("/file/{}", hash)) - .body(content) - .header(ContentType::Binary) - .dispatch(); + .post( + format!("/file/{}", hash), + mime::APPLICATION_OCTET_STREAM, + content, + ) + .await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_success("server", "null-response.json", &body); body.get("data") @@ -499,38 +574,32 @@ fn can_post_file() { .expect("Null data"); // check file now exists on server - let get_file_response = client.get(format!("/file/{}", hash)).dispatch(); - assert_eq!(get_file_response.status(), Status::Ok); - assert_eq!(get_file_response.into_string().unwrap(), "test"); + let get_file_response = client.get(format!("/file/{}", hash)).await; + assert_eq!(get_file_response.status(), StatusCode::OK); + assert_eq!(get_file_response.to_string().await, "test"); } -#[test] -fn file_post_handles_errors() { - let root = get_test_dir(); - let rocket = outpack::api::api(&root).unwrap(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn file_post_handles_errors() { + let mut client = get_default_client(); let content = "test"; let response = client - .post("/file/md5:bad4a54".to_string()) - .body(content) - .header(ContentType::Binary) - .dispatch(); + .post("/file/md5:bad4a54", mime::APPLICATION_OCTET_STREAM, content) + .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_error( &body, Some("Expected hash 'md5:bad4a54' but found 'md5:098f6bcd4621d373cade4e832627b4f6'"), ); } -#[test] -fn can_post_metadata() { - let root = get_test_dir(); - let rocket = outpack::api::api(&root).unwrap(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn can_post_metadata() { + let mut client = get_default_client(); let content = r#"{ "schema_version": "0.0.1", "name": "computed-resource", @@ -551,15 +620,13 @@ fn can_post_metadata() { Sha256::new().chain_update(content).finalize() ); let response = client - .post(format!("/packet/{}", hash)) - .body(content) - .header(ContentType::Text) - .dispatch(); + .post(format!("/packet/{}", hash), mime::TEXT_PLAIN_UTF_8, content) + .await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_success("server", "null-response.json", &body); body.get("data") @@ -568,43 +635,96 @@ fn can_post_metadata() { .expect("Null data"); // check packet now exists on server - let get_metadata_response = client - .get("/metadata/20230427-150828-68772cee/json") - .dispatch(); - assert_eq!(get_metadata_response.status(), Status::Ok); + let get_metadata_response = client.get("/metadata/20230427-150828-68772cee/json").await; + assert_eq!(get_metadata_response.status(), StatusCode::OK); } -#[test] -fn catches_arbitrary_404() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); - let response = client.get("/badurl").dispatch(); +#[tokio::test] +async fn catches_arbitrary_404() { + let mut client = get_default_client(); + let response = client.get("/badurl").await; - assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.content_type(), Some(ContentType::JSON)); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.content_type(), mime::APPLICATION_JSON); - let body = serde_json::from_str(&response.into_string().unwrap()).unwrap(); + let body = response.to_json().await; validate_error(&body, Some("This route does not exist")); } -#[test] -fn exposes_metrics_endpoint() { - let rocket = get_test_rocket(); - let client = Client::tracked(rocket).expect("valid rocket instance"); +#[tokio::test] +async fn exposes_metrics_endpoint() { + let mut client = get_default_client(); // Send at least one arbitrary request first so we don't get empty metrics. - client.get("/").dispatch(); + client.get("/").await; - let response = client.get("/metrics").dispatch(); + let response = client.get("/metrics").await; - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.content_type(), Some(ContentType::Text)); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.content_type(), "text/plain; version=0.0.4"); assert!(response - .into_string() - .unwrap() + .to_string() + .await .lines() - .any(|line| line.starts_with("rocket_http_requests_total"))); + .any(|line| line.starts_with("http_requests_total"))); +} + +#[tokio::test] +async fn generates_request_id() { + let mut client = get_default_client(); + let response = client.get("/").await; + assert!(!response.headers()["x-request-id"].is_empty()); +} + +#[tokio::test] +async fn propagates_request_id() { + let mut client = get_default_client(); + let request = Request::get("/") + .header("x-request-id", "foobar123") + .body(Body::empty()) + .unwrap(); + let response = client.request(request).await; + assert_eq!(response.headers()["x-request-id"], "foobar123"); +} + +#[tokio::test] +async fn request_id_is_logged() { + use predicates::ord::eq; + use tracing_capture::predicates::{message, name, ScanExt}; + + // tracing has a pretty obscure bug when exactly one subscriber exists, but other threads are + // calling trace macros without a subscriber. We can work around it by creating a dummy + // subscriber in the background. We need to assign it to a variable to ensure it does not get + // dropped and persists until the end of the test. + // See https://github.com/tokio-rs/tracing/issues/2874 + let _dont_drop_me = tracing::Dispatch::new(tracing::subscriber::NoSubscriber::new()); + + let storage = SharedStorage::default(); + let subscriber = Registry::default().with(CaptureLayer::new(&storage)); + + let f = async { + let mut client = get_default_client(); + let request = Request::get("/") + .header("x-request-id", "foobar123") + .body(Body::empty()) + .unwrap(); + + client.request(request).await + }; + + f.with_subscriber(subscriber).await; + + let storage = storage.lock(); + let span = storage.scan_spans().single(&name(eq("request"))); + assert!(span + .value("request_id") + .unwrap() + .is_debug(&tracing::field::display("foobar123"))); + span.scan_events() + .single(&message(eq("started processing request"))); + span.scan_events() + .single(&message(eq("finished processing request"))); } fn validate_success(schema_group: &str, schema_name: &str, instance: &Value) { @@ -624,7 +744,7 @@ fn validate_error(instance: &Value, message: Option<&str>) { let status = instance.get("status").expect("Status property present"); assert_eq!(status, "failure"); - if message.is_some() { + if let Some(message) = message { let err = instance .get("errors") .expect("Status property present") @@ -636,7 +756,7 @@ fn validate_error(instance: &Value, message: Option<&str>) { .expect("Error detail") .to_string(); - assert!(err.contains(message.unwrap()), "Error was: {}", err); + assert!(err.contains(message), "Error was: {}", err); } }