From 6d401dd44a88b59a387fdb407c8bbca4e64d9ec3 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 28 Aug 2026 09:44:20 -0400 Subject: [PATCH] Add trace components Components that log each wasi:http call at the TRACE level, with the 'componentized-trace' context: * trace-types: wasi:http/types * trace-client: wasi:http/types and wasi:http/client * trace-handler: wasi:http/types and wasi:http/handler * trace: wasi:http/types, wasi:http/client and wasi:http/handler * trace-componentized-client: componentized:http/client Each component exports only the interfaces it traces, so it imports nothing more from the host than it needs. Traced client and handler calls take the traced request and response resources, so they can't be composed from trace-types with wac. Instead, the four wasi:http components compile the shared sources in crates/trace with cargo features that select the world. Resources are logged by the last four hex digits of their upstream handle, prefixed with `&`, e.g. `SELF=&002a`. Tests run each component in wasmtime with wasi:http from wasmtime-wasi-http. Requests sent upstream are recorded and answered with a canned response, without the network. Body streams passed through from the host can't be read by the harness, so the tests check status and headers. Signed-off-by: Scott Andrews Co-Authored-By: Claude --- Cargo.lock | 2496 +++++++++++++++-- Cargo.toml | 14 + Makefile | 13 +- README.md | 39 +- components/trace-client/Cargo.toml | 23 + components/trace-client/README.md | 13 + components/trace-client/tests/client.rs | 116 + components/trace-client/tests/handles.rs | 55 + .../trace-componentized-client/Cargo.toml | 16 + .../trace-componentized-client/README.md | 9 + .../trace-componentized-client/src/lib.rs | 142 + .../tests/client.rs | 110 + components/trace-handler/Cargo.toml | 23 + components/trace-handler/README.md | 13 + components/trace-handler/tests/handler.rs | 116 + components/trace-handler/tests/handles.rs | 55 + components/trace-types/Cargo.toml | 23 + components/trace-types/README.md | 11 + components/trace-types/tests/handles.rs | 214 ++ components/trace-types/tests/types.rs | 339 +++ components/trace/Cargo.toml | 23 + components/trace/README.md | 15 + components/trace/tests/trace.rs | 94 + .../deps/wasi-logging-0.1.0-draft/package.wit | 36 + components/wit/worlds.wit | 29 + components/wkg.lock | 9 + crates/test-harness/Cargo.toml | 15 + crates/test-harness/src/lib.rs | 800 ++++++ crates/trace/src/client.rs | 21 + crates/trace/src/handler.rs | 21 + crates/trace/src/lib.rs | 102 + crates/trace/src/types.rs | 620 ++++ 32 files changed, 5401 insertions(+), 224 deletions(-) create mode 100644 components/trace-client/Cargo.toml create mode 100644 components/trace-client/README.md create mode 100644 components/trace-client/tests/client.rs create mode 100644 components/trace-client/tests/handles.rs create mode 100644 components/trace-componentized-client/Cargo.toml create mode 100644 components/trace-componentized-client/README.md create mode 100644 components/trace-componentized-client/src/lib.rs create mode 100644 components/trace-componentized-client/tests/client.rs create mode 100644 components/trace-handler/Cargo.toml create mode 100644 components/trace-handler/README.md create mode 100644 components/trace-handler/tests/handler.rs create mode 100644 components/trace-handler/tests/handles.rs create mode 100644 components/trace-types/Cargo.toml create mode 100644 components/trace-types/README.md create mode 100644 components/trace-types/tests/handles.rs create mode 100644 components/trace-types/tests/types.rs create mode 100644 components/trace/Cargo.toml create mode 100644 components/trace/README.md create mode 100644 components/trace/tests/trace.rs create mode 100644 components/wit/deps/wasi-logging-0.1.0-draft/package.wit create mode 100644 crates/test-harness/Cargo.toml create mode 100644 crates/test-harness/src/lib.rs create mode 100644 crates/trace/src/client.rs create mode 100644 crates/trace/src/handler.rs create mode 100644 crates/trace/src/lib.rs create mode 100644 crates/trace/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index bdb604a..f3c8f29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,139 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + [[package]] name = "client" version = "0.1.0" @@ -22,6 +143,278 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658e8623bea025d142fb11c299b8c6572ffe9fd663b52887c4dc124735cc130b" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b12b8c55c31c3d8598c2de9a657c3b978f905f6886c43e02216e0b15fe45a3" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "894b984f82c5a36f05fdd4973fbf4d37d69cd5cdf8c16e8ffe3af4a9cdfe193c" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1e4c34abe94193ada09f821f474be87ac8a1fbbacd87058571ef04c1bf4009" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63826cef0bf70e61d56cc930aa4ee423638f66dc3cfc10d683e73138a6d177b8" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26de7849112293681a19f7525c689e1d73dc6eb2687355170fb7458c4da8d353" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df315a8b1cfd6e620971e3d4249bd2c4528167717e8f48a6317a87c55891bcb8" + +[[package]] +name = "cranelift-control" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3fff7610fb17f228f16598e1a044db79a12806ece7496ff2c69cf1a27a79a5" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6158b1ac381792ad11e670abbcd15db8b1737e84bfa8a7f2bd7eb41388a92530" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099361a0919067086dcde624a281ce719834cebf8544d6c24646f5e924107234" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2f30ea3fd5a11f7d3473976d3b62b5b3f2b62fa943c85e0331d88bf505082c" + +[[package]] +name = "cranelift-native" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397508a8d817dc74019603ac5a4c4e40b4df627e5aed4f2f8088345d4c01d401" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4944ef05513e1c1a85a945cf8f29d95ddea8bcbb19e0bfd4e71063450967420" + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -33,12 +426,78 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.2.0" @@ -54,6 +513,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "futures" version = "0.3.34" @@ -108,36 +578,192 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "fxprof-processed-profile" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" dependencies = [ - "foldhash", + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", ] [[package]] -name = "heck" -version = "0.5.0" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] [[package]] -name = "icu_collections" -version = "2.2.0" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "displaydoc", - "potential_utf", + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", "utf8_iter", "yoke", "zerofrom", @@ -198,314 +824,1694 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] -name = "icu_provider" -version = "2.2.0" +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.5", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b66b7448cb92d709f2924de98b5ba5b5ce4ea08e6d6b9c3b403fcd88d122ca2" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185b9b4cd91f85110f48f8c0314757dfb2bb48c40f5d91055ea666cbbcbf317b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test-harness" +version = "0.1.0" +dependencies = [ + "bytes", + "http", + "http-body-util", + "tokio", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-http", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "trace" +version = "0.1.0" +dependencies = [ + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "trace-client" +version = "0.1.0" +dependencies = [ + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "trace-componentized-client" +version = "0.1.0" +dependencies = [ + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "trace-handler" +version = "0.1.0" +dependencies = [ + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "trace-types" +version = "0.1.0" +dependencies = [ + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27dc71f96d21a8b9153da9005c603f4f20d5781b7e6d2675ea0ca5def8713b75" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64765e43c000fae008a48db8b288e175e237fc9ce20a26586836878c1bee89f" +dependencies = [ + "leb128fmt", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasm-encoder" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" +dependencies = [ + "leb128fmt", + "wasmparser 0.259.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2608e8bb6d67fd68f5a8d0eb1363d6e7bcbc1f8ded5a0bd3a1e382462b876b22" +dependencies = [ + "leb128fmt", + "wasmparser 0.261.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e10808eb27efde692f1b1706089fe19296728bf872291f5b4e0d50fba88f2f" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasm-metadata" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.259.0", + "wasmparser 0.259.0", +] + +[[package]] +name = "wasmparser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a457ba8670a3db3d357c1aeac3ed4fe378a76054a221789f0df0ea507e292b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasmtime" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1801b9c9f91e16e352ed77656824e7c4556a7538dbfc1a50b37243f0e12ea045" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "bytes", + "cc", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wat", + "windows-sys 0.61.2", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-environ" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f397969b8daf208e45120036be74956e99364ce916d5b5d67f9e9f0fe40e5477" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf564a08f96acec55a335e3185d1df98ba3feee47e178f5bc2429e723d30d0f" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47c428b868b59d51a9f77e24bff02cc321f8ec0faeba5c40f7e4c1cddd43a64" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fe14bd5ea6444a09f6baa3865559c93c91158930e7fe6d127429a9372f6165" + +[[package]] +name = "wasmtime-internal-core" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2053fd1d90080e502ca4ea39d86c3528587a40220b9572a874c8b742c219a00c" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e96d6c55401e68e40e3f74078ec96f92fe9b8a7e90e885fb87265e69eea559" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.21", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b913875bab1b78fada489e55d5e0a10b53286fb4bb09f5657212f549c9af36c" +dependencies = [ + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0356a1a09dee1239904e09e962456d9f6edfa5da61c5e3e1b0e4669710f48fba" +dependencies = [ + "cc", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dead9a2d7b5d835698a2e1a32006e7d0b5711f4697b24bab1564bb2488e3d7a" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ffac8fc893de2958b263d9d7274cf4c531ba9ff582558348d4e28d97b8a0f2" +dependencies = [ + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a20f0dcb6c6a006363d3f85b5c633675730487b0dc23726b2d40495872407fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80758f73f5f5daacd8a6d1a8513336a4f9a58d26c4f1c73eb80502cb143e2626" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-component 0.258.3", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-wasi" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac947c8478d8ea84ef4120d04f984012a514e493fe16fa5bb72906d7e55addb" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-primitives", + "futures", + "rand", + "rustix", + "rustix-linux-procfs", + "thiserror 2.0.21", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "wasmtime-wasi-http" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4cd327c70de1309b0c04d49df3633a2ffeae9b29b526d421320b565e198175" +dependencies = [ + "async-trait", + "bytes", + "futures", + "http", + "http-body", + "http-body-util", + "hyper", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-io", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "e65d70efbe70631acbb4c6cf46eea543c75908f01e2fd09550e5a89631693304" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "wast" +version = "35.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] [[package]] -name = "idna" -version = "1.1.0" +name = "wast" +version = "261.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "776443145731a4062e5b0d392892a2005909b6ab72d9fdc3cad53dd1a714e44a" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.261.0", ] [[package]] -name = "idna_adapter" -version = "1.2.2" +name = "wat" +version = "1.261.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "a7b4d1a49ea73a8f3326e74e3a05db667001b16bd1035ed3356fc1a0ed05ca7f" dependencies = [ - "icu_normalizer", - "icu_properties", + "wast 261.0.0", ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "wiggle" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "7f7c3a2e8ec4789bb82e4bf04b0dba44b02ce05beba6cbc6eb992b8f0b93c9cd" dependencies = [ - "equivalent", - "hashbrown", - "serde", - "serde_core", + "bitflags", + "thiserror 2.0.21", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "wiggle-generate" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "761bf518bf5972dd0ddb8c3c693b6a52ed826e25d10bfae89b1368327c86030d" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-environ", + "witx", +] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "wiggle-macro" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "0bdaa435787bc4bb662020aea37538745fc60b4629d7f37864a09195cec0e1eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "wiggle-generate", +] [[package]] -name = "litemap" -version = "0.8.2" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] [[package]] -name = "log" -version = "0.4.33" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "macro-string" -version = "0.3.0" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.5", + "windows-sys 0.61.2", ] [[package]] -name = "memchr" -version = "2.8.3" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] [[package]] -name = "potential_utf" -version = "0.1.5" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "zerovec", + "windows-targets 0.53.5", ] [[package]] -name = "prettyplease" -version = "0.3.0" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "proc-macro2", - "syn 3.0.5", + "windows-link", ] [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "unicode-ident", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] -name = "quote" -version = "1.0.47" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "proc-macro2", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] -name = "semver" -version = "1.0.28" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "serde" -version = "1.0.229" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", -] +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] -name = "serde_core" -version = "1.0.229" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "serde_derive" -version = "1.0.229" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.5", -] +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] -name = "serde_json" -version = "1.0.151" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "slab" -version = "0.4.12" +name = "windows_i686_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] -name = "smallvec" -version = "1.15.2" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] -name = "syn" -version = "2.0.119" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "syn" -version = "3.0.5" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] -name = "synstructure" -version = "0.13.2" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "tinystr" -version = "0.8.3" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "url" -version = "2.5.8" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wasm-encoder" -version = "0.259.0" +name = "windows_x86_64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" -dependencies = [ - "leb128fmt", - "wasmparser", -] +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "wasm-metadata" -version = "0.259.0" +name = "winnow" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] -name = "wasmparser" -version = "0.259.0" +name = "winx" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ "bitflags", - "hashbrown", - "indexmap", - "semver", + "windows-sys 0.59.0", ] [[package]] @@ -527,7 +2533,7 @@ checksum = "72ad22a37ecbc0e1fdca34d2b670ba41368cb0920735643e00fdf39a16ee5431" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.259.0", ] [[package]] @@ -541,9 +2547,9 @@ dependencies = [ "indexmap", "prettyplease", "syn 3.0.5", - "wasm-metadata", + "wasm-metadata 0.259.0", "wit-bindgen-core", - "wit-component", + "wit-component 0.259.0", ] [[package]] @@ -562,6 +2568,25 @@ dependencies = [ "wit-bindgen-rust", ] +[[package]] +name = "wit-component" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fca88950af0c1f42bc77e17315e48798b8d68340171b6d204f62bc19a878a6" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.258.3", + "wasm-metadata 0.258.3", + "wasmparser 0.258.3", + "wit-parser 0.258.3", +] + [[package]] name = "wit-component" version = "0.259.0" @@ -575,10 +2600,29 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.259.0", + "wasm-metadata 0.259.0", + "wasmparser 0.259.0", + "wit-parser 0.259.0", +] + +[[package]] +name = "wit-parser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2127663dcf3ee16614fb0e3c33d5c6c66fec9dad317a8b0fdd3869487a533" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.258.3", ] [[package]] @@ -588,7 +2632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6390f4f02493ce678d509c859cf1698e38929fb75c8012a6ee2f7d6b6cb66cd7" dependencies = [ "anyhow", - "hashbrown", + "hashbrown 0.17.1", "id-arena", "indexmap", "log", @@ -597,7 +2641,19 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser", + "wasmparser 0.259.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", ] [[package]] @@ -688,3 +2744,31 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 7603948..791c9db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,23 @@ resolver = "2" members = [ "components/client", + "components/trace", + "components/trace-client", + "components/trace-handler", + "components/trace-componentized-client", + "components/trace-types", + "crates/test-harness", ] [workspace.dependencies] +bytes = "1" +http = "1" +http-body-util = "0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +test-harness = { path = "./crates/test-harness" } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] } +wasmtime = { version = "49", features = ["component-model-async"] } +wasmtime-wasi = { version = "49", features = ["p3"] } +wasmtime-wasi-http = { version = "49", default-features = false, features = ["p3", "component-model-async"] } wit-bindgen = "0.62.0" diff --git a/Makefile b/Makefile index 9c2a844..2ce997e 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,16 @@ SHELL := /bin/bash -WKG_CONFIG := $(CURDIR)/.config/wasm-pkg/config.toml +# prefer paths are relative to the root of the repository, where make runs, to keep the logs readable + +WKG_CONFIG := .config/wasm-pkg/config.toml export RUST_BACKTRACE ?= 1 export WASMTIME_BACKTRACE_DETAILS ?= 1 -COMPONENTS_DIR := $(abspath target/components) -TOOLS_DIR := $(abspath target/tools) -export PATH := $(TOOLS_DIR)/bin:$(PATH) +COMPONENTS_DIR := target/components +TOOLS_DIR := target/tools +# absolute, tools also run from other directories, e.g. `cd components && wkg fetch` +export PATH := $(abspath $(TOOLS_DIR))/bin:$(PATH) # cargo binstall downloads prebuilt binaries, without it the tools are built with cargo install CARGO_INSTALL := $(if $(shell command -v cargo-binstall 2> /dev/null),cargo binstall --no-confirm --disable-telemetry,cargo install) @@ -125,7 +128,7 @@ wit/deps: wkg.toml $(shell find wit -type f -name "*.wit" -not -path "deps") | $ wkg fetch --config $(WKG_CONFIG) components/wit/deps: wit/deps components/wkg.toml $(shell find components/wit -type f -name "*.wit" -not -path "deps") | $(call tool,wkg) - ( cd components && wkg fetch --config $(WKG_CONFIG) ) + ( cd components && wkg fetch --config ../$(WKG_CONFIG) ) # sign published components with cosign, `SIGN=false` to push without signing, e.g. to a local registry SIGN ?= true diff --git a/README.md b/README.md index 9e52bea..9abb05d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ A collection of utility components that remix wasi:http types and interfaces. - [Components](#components) + - [Choosing a component variant](#choosing-a-component-variant) - [Build](#build) - [Community](#community) - [Code of Conduct](#code-of-conduct) @@ -15,15 +16,35 @@ A collection of utility components that remix wasi:http types and interfaces. ## Components - [`client`](./components/client/) +- [`trace`](./components/trace/) +- [`trace-client`](./components/trace-client/) +- [`trace-handler`](./components/trace-handler/) +- [`trace-componentized-client`](./components/trace-componentized-client/) +- [`trace-types`](./components/trace-types/) -## Build +### Choosing a component variant + +Due to resource types being unique to the instance that defines them in the Component Model, fine grain composition of the `wasi:http` interfaces can be persnickety. Pick the most specific component that covers the interfaces the target component imports. While the base component is more universal, a larger surface area asks the host for capabilities the target component doesn't use. + +For example, with the `trace-*` components: + +| The target component imports | Use | +| ------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `wasi:http/types` | [`trace-types`](./components/trace-types/) | +| `wasi:http/types` and `wasi:http/client` | [`trace-client`](./components/trace-client/) | +| `wasi:http/types` and `wasi:http/handler` | [`trace-handler`](./components/trace-handler/) | +| `wasi:http/types`, `wasi:http/client` and `wasi:http/handler` | [`trace`](./components/trace/) | +| `componentized:http/client` | [`trace-componentized-client`](./components/trace-componentized-client/) | -A [dev container](https://containers.dev) is available that contains the necessary tools and configuration out of the box. +`trace-client`, `trace-handler` and `trace` also trace `wasi:http/types`, don't combine them with `trace-types`. + +The `wasi:http/handler` exported by `trace-handler` and `trace` takes requests created with their exported `wasi:http/types`. Use them in front of a component that forwards requests with `wasi:http/handler`, a host serving incoming requests can't call them directly. + +## Build Prereqs: - a rust toolchain -- [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools) -- [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools) +- [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall), optional, to download prebuilt tools instead of building them ```sh make components @@ -31,15 +52,7 @@ make components The build creates each component in [`components`](./components) into `target/components`, e.g. the client at `target/components/client/client.wasm`, along with `target/components/interface.wasm`, the `componentized:http` WIT package. Each component is also built with debug info, e.g. `target/components/client/client.debug.wasm`. -```sh -make test -``` - -The WIT dependencies in each `wit/deps` directory are fetched rather than committed, pinned by the `wkg.lock` files. The make targets fetch them as needed. To fetch or update them directly, e.g. before building the Rust components with `cargo`, whose bindings are generated from the WIT: - -```sh -make wit -``` +The cli tools the build uses, [`static-config`](https://github.com/componentized/static-config), [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools), [`wac`](https://github.com/bytecodealliance/wac) and [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools), are pinned in [`tools/Cargo.toml`](./tools/Cargo.toml) and installed into `target/tools` as needed, or ahead of time with `make tools`. Dependabot bumps the pinned versions. ## Community diff --git a/components/trace-client/Cargo.toml b/components/trace-client/Cargo.toml new file mode 100644 index 0000000..1ed8596 --- /dev/null +++ b/components/trace-client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "trace-client" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other trace components +path = "../../crates/trace/src/lib.rs" + +[features] +default = ["client"] +client = [] +handler = [] + +[dependencies] +wit-bindgen = { workspace = true, features = ["async-spawn"] } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/trace-client/README.md b/components/trace-client/README.md new file mode 100644 index 0000000..db7d739 --- /dev/null +++ b/components/trace-client/README.md @@ -0,0 +1,13 @@ +# `trace-client` + +Virtualizes the wasi:http/types and wasi:http/client interfaces logging all method calls at the TRACE level and 'componentized-trace' component. + +Built from the sources shared with the other trace components in [`crates/trace`](../../crates/trace/src/lib.rs). + +## The `trace-client` World + +- exports `wasi:http/types` +- exports `wasi:http/client` +- imports `wasi:http/types` +- imports `wasi:http/client` +- imports `wasi:logging/logging` diff --git a/components/trace-client/tests/client.rs b/components/trace-client/tests/client.rs new file mode 100644 index 0000000..3445a7f --- /dev/null +++ b/components/trace-client/tests/client.rs @@ -0,0 +1,116 @@ +//! Tests for the `wasi:http/client` operations traced by `trace-client`. + +use test_harness::{ + ErrorCode, Harness, LogEntry, Method, ResourceAny, Scheme, SentRequest, UPSTREAM_HEADER, + UPSTREAM_STATUS, +}; + +fn traced(message: &str) -> LogEntry { + LogEntry::trace("componentized-trace", message) +} + +#[tokio::test(flavor = "multi_thread")] +async fn exports_types_and_client() -> wasmtime::Result<()> { + let trace = Harness::new("trace-client").build().await?; + assert!(trace.exports().exports_types()); + assert!(trace.exports().exports_client()); + assert!(!trace.exports().exports_handler()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn send_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-client").build().await?; + let (status, header) = trace + .run(async |accessor, trace| { + let request = trace + .new_request( + accessor, + Method::Get, + Scheme::Https, + "example.com", + "/hello", + ) + .await?; + let response: ResourceAny = trace + .wasi_http_client() + .call_send(accessor, request) + .await? + .expect("send"); + // the response is a traced resource + let types = trace.wasi_http_types(); + let status = types + .response() + .call_get_status_code(accessor, response) + .await?; + let headers = types + .response() + .call_get_headers(accessor, response) + .await?; + let header = types + .fields() + .call_get(accessor, headers, UPSTREAM_HEADER.0.to_string()) + .await?; + Ok((status, header)) + }) + .await?; + // the upstream response is passed through + assert_eq!(status, UPSTREAM_STATUS); + assert_eq!(header, vec![UPSTREAM_HEADER.1.as_bytes().to_vec()]); + assert_eq!( + trace.recorder().requests(), + vec![SentRequest { + method: "GET".to_string(), + uri: "https://example.com/hello".to_string(), + }] + ); + assert_eq!( + trace.recorder().masked_logs(), + vec![ + traced("OPERATION=wasi:http/types#fields.new"), + traced("OPERATION=wasi:http/types#request.new"), + traced("OPERATION=wasi:http/types#request.set-method SELF=&---- METHOD=get"), + traced("OPERATION=wasi:http/types#request.set-scheme SELF=&---- SCHEME=some"), + traced( + "OPERATION=wasi:http/types#request.set-authority SELF=&---- AUTHORITY=some" + ), + traced( + "OPERATION=wasi:http/types#request.set-path-with-query SELF=&---- PATH-WITH-QUERY=some" + ), + traced("OPERATION=wasi:http/client#send REQUEST=&----"), + traced("OPERATION=wasi:http/types#response.get-status-code SELF=&----"), + traced("OPERATION=wasi:http/types#response.get-headers SELF=&----"), + traced("OPERATION=wasi:http/types#fields.get SELF=&---- NAME=x-upstream"), + ] + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn send_error_passed_through() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-client").build().await?; + let result = trace + .run(async |accessor, trace| { + let scheme = Scheme::Other("gopher".to_string()); + let request = trace + .new_request(accessor, Method::Get, scheme, "example.com", "/") + .await?; + let result = trace + .wasi_http_client() + .call_send(accessor, request) + .await?; + Ok(result.map(|_| ())) + }) + .await?; + // the upstream error is passed through, the request never left + assert!( + matches!(result, Err(ErrorCode::HttpProtocolError)), + "unsupported scheme: {result:?}" + ); + assert_eq!(trace.recorder().requests(), vec![]); + assert_eq!( + trace.recorder().masked_logs().last(), + Some(&traced("OPERATION=wasi:http/client#send REQUEST=&----")) + ); + Ok(()) +} diff --git a/components/trace-client/tests/handles.rs b/components/trace-client/tests/handles.rs new file mode 100644 index 0000000..b6629bd --- /dev/null +++ b/components/trace-client/tests/handles.rs @@ -0,0 +1,55 @@ +//! The handle logged for a resource must be the same in every message about it, including when +//! it is passed to `wasi:http/client#send`. + +use test_harness::{ErrorCode, Harness, Method, Scheme, ready}; + +fn same_handle(values: Vec, resource: &str) -> String { + let first = values + .first() + .unwrap_or_else(|| panic!("no handle logged for the {resource}")) + .clone(); + assert!( + values.iter().all(|value| *value == first), + "the {resource} was logged with different handles: {values:?}" + ); + first +} + +#[tokio::test(flavor = "multi_thread")] +async fn send_logs_the_request_handle() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-client").build().await?; + trace + .run(async |accessor, trace| { + let request = trace + .new_request(accessor, Method::Get, Scheme::Https, "example.com", "/") + .await?; + let response = trace + .wasi_http_client() + .call_send(accessor, request) + .await? + .expect("send"); + let responses = trace.wasi_http_types().response(); + responses.call_get_status_code(accessor, response).await?; + responses.call_get_headers(accessor, response).await?; + let res = ready(accessor, Ok::<(), ErrorCode>(()))?; + responses.call_consume_body(accessor, response, res).await?; + Ok(()) + }) + .await?; + let recorder = trace.recorder(); + let request = same_handle( + recorder.handle_values("wasi:http/types#request."), + "request", + ); + assert_eq!( + recorder.handle_values("wasi:http/client#send"), + vec![request], + "send logs the handle of the request it was passed" + ); + // the response from upstream is a new resource, its handle is stable too + same_handle( + recorder.handle_values("wasi:http/types#response."), + "response", + ); + Ok(()) +} diff --git a/components/trace-componentized-client/Cargo.toml b/components/trace-componentized-client/Cargo.toml new file mode 100644 index 0000000..c5f7503 --- /dev/null +++ b/components/trace-componentized-client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "trace-componentized-client" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/trace-componentized-client/README.md b/components/trace-componentized-client/README.md new file mode 100644 index 0000000..82dca1f --- /dev/null +++ b/components/trace-componentized-client/README.md @@ -0,0 +1,9 @@ +# `trace-componentized-client` + +Virtualizes the componentized:http/client interface logging all method calls at the TRACE level and 'componentized-trace' component. + +## The `trace-componentized-client` World + +- exports `componentized:http/client` +- imports `componentized:http/client` +- imports `wasi:logging/logging` diff --git a/components/trace-componentized-client/src/lib.rs b/components/trace-componentized-client/src/lib.rs new file mode 100644 index 0000000..2fec168 --- /dev/null +++ b/components/trace-componentized-client/src/lib.rs @@ -0,0 +1,142 @@ +use std::fmt::Display; + +use crate::{ + componentized::http::client::{self, ErrorCode, HttpResponse, Method, RequestOptions}, + exports::componentized::http::client::Guest, + wasi::logging::logging::{Level, log}, +}; +use wit_bindgen::StreamReader; + +#[macro_export] +macro_rules! trace { + ($dst:expr, $($arg:tt)*) => { + log(Level::Trace, "componentized-trace", &format!($dst, $($arg)*)); + }; + ($dst:expr) => { + log(Level::Trace, "componentized-trace", &format!($dst)); + }; +} + +struct TraceComponentizedClient; + +impl Guest for TraceComponentizedClient { + async fn request( + method: Method, + url: String, + headers: Vec<(String, String)>, + body: Option>, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#request URL={url}"); + client::request(method, url, headers, body, options).await + } + + async fn get( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#get URL={url}"); + client::get(url, headers, options).await + } + + async fn post( + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#post URL={url}"); + client::post(url, headers, body, options).await + } + + async fn put( + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#put URL={url}"); + client::put(url, headers, body, options).await + } + + async fn delete( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#delete URL={url}"); + client::delete(url, headers, options).await + } + + async fn patch( + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#patch URL={url}"); + client::patch(url, headers, body, options).await + } + + async fn head( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#head URL={url}"); + client::head(url, headers, options).await + } + + async fn options( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#options URL={url}"); + client::options(url, headers, options).await + } + + async fn trace( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#trace URL={url}"); + client::trace(url, headers, options).await + } + + async fn query( + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + trace!("OPERATION=componentized:http/client#query URL={url}"); + client::query(url, headers, body, options).await + } +} + +impl Display for Method { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Method::Get => f.write_str("get"), + Method::Post => f.write_str("post"), + Method::Put => f.write_str("put"), + Method::Delete => f.write_str("delete"), + Method::Patch => f.write_str("patch"), + Method::Head => f.write_str("head"), + Method::Options => f.write_str("options"), + Method::Trace => f.write_str("trace"), + Method::Query => f.write_str("query"), + } + } +} +wit_bindgen::generate!({ + path: "../wit", + world: "trace-componentized-client", + merge_structurally_equal_types: true, + generate_all, +}); + +export!(TraceComponentizedClient); diff --git a/components/trace-componentized-client/tests/client.rs b/components/trace-componentized-client/tests/client.rs new file mode 100644 index 0000000..399448e --- /dev/null +++ b/components/trace-componentized-client/tests/client.rs @@ -0,0 +1,110 @@ +//! Tests for the `componentized:http/client` operations traced by `trace-componentized-client`. + +use test_harness::{ + Harness, HttpClientMethod, LogEntry, SentRequest, UPSTREAM_HEADER, UPSTREAM_STATUS, +}; + +fn traced(message: &str) -> LogEntry { + LogEntry::trace("componentized-trace", message) +} + +#[tokio::test(flavor = "multi_thread")] +async fn exports_only_http_client() -> wasmtime::Result<()> { + let trace = Harness::new("trace-componentized-client").build().await?; + assert!(!trace.exports().exports_types()); + assert!(!trace.exports().exports_client()); + assert!(!trace.exports().exports_handler()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn request_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-componentized-client").build().await?; + let (status, headers) = trace + .run(async |accessor, trace| { + let response = trace + .componentized_http_client() + .call_request( + accessor, + HttpClientMethod::Delete, + "https://example.com/a".to_string(), + vec![], + None, + None, + ) + .await? + .expect("request"); + Ok((response.status, response.headers)) + }) + .await?; + // the upstream response is passed through + assert_eq!(status, UPSTREAM_STATUS); + assert_eq!( + headers, + vec![(UPSTREAM_HEADER.0.to_string(), UPSTREAM_HEADER.1.to_string())] + ); + assert_eq!( + trace.recorder().requests(), + vec![SentRequest { + method: "DELETE".to_string(), + uri: "https://example.com/a".to_string(), + }] + ); + assert_eq!( + trace.recorder().logs(), + vec![traced( + "OPERATION=componentized:http/client#request URL=https://example.com/a" + )] + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn methods_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-componentized-client").build().await?; + trace + .run(async |accessor, trace| { + let client = trace.componentized_http_client(); + let url = |path: &str| format!("https://example.com/{path}"); + client + .call_get(accessor, url("get"), vec![], None) + .await? + .expect("get"); + client + .call_delete(accessor, url("delete"), vec![], None) + .await? + .expect("delete"); + client + .call_head(accessor, url("head"), vec![], None) + .await? + .expect("head"); + client + .call_options(accessor, url("options"), vec![], None) + .await? + .expect("options"); + client + .call_trace(accessor, url("trace"), vec![], None) + .await? + .expect("trace"); + Ok(()) + }) + .await?; + let methods: Vec = trace + .recorder() + .requests() + .into_iter() + .map(|request| request.method) + .collect(); + assert_eq!(methods, vec!["GET", "DELETE", "HEAD", "OPTIONS", "TRACE"]); + assert_eq!( + trace.recorder().logs(), + vec![ + traced("OPERATION=componentized:http/client#get URL=https://example.com/get"), + traced("OPERATION=componentized:http/client#delete URL=https://example.com/delete"), + traced("OPERATION=componentized:http/client#head URL=https://example.com/head"), + traced("OPERATION=componentized:http/client#options URL=https://example.com/options"), + traced("OPERATION=componentized:http/client#trace URL=https://example.com/trace"), + ] + ); + Ok(()) +} diff --git a/components/trace-handler/Cargo.toml b/components/trace-handler/Cargo.toml new file mode 100644 index 0000000..9c685ea --- /dev/null +++ b/components/trace-handler/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "trace-handler" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other trace components +path = "../../crates/trace/src/lib.rs" + +[features] +default = ["handler"] +client = [] +handler = [] + +[dependencies] +wit-bindgen = { workspace = true, features = ["async-spawn"] } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/trace-handler/README.md b/components/trace-handler/README.md new file mode 100644 index 0000000..ce21052 --- /dev/null +++ b/components/trace-handler/README.md @@ -0,0 +1,13 @@ +# `trace-handler` + +Virtualizes the wasi:http/types and wasi:http/handler interfaces logging all method calls at the TRACE level and 'componentized-trace' component. + +Built from the sources shared with the other trace components in [`crates/trace`](../../crates/trace/src/lib.rs). + +## The `trace-handler` World + +- exports `wasi:http/types` +- exports `wasi:http/handler` +- imports `wasi:http/types` +- imports `wasi:http/handler` +- imports `wasi:logging/logging` diff --git a/components/trace-handler/tests/handler.rs b/components/trace-handler/tests/handler.rs new file mode 100644 index 0000000..5cf2b07 --- /dev/null +++ b/components/trace-handler/tests/handler.rs @@ -0,0 +1,116 @@ +//! Tests for the `wasi:http/handler` operations traced by `trace-handler`. + +use test_harness::{ + ErrorCode, Harness, LogEntry, Method, ResourceAny, Scheme, SentRequest, UPSTREAM_HEADER, + UPSTREAM_STATUS, +}; + +fn traced(message: &str) -> LogEntry { + LogEntry::trace("componentized-trace", message) +} + +#[tokio::test(flavor = "multi_thread")] +async fn exports_types_and_handler() -> wasmtime::Result<()> { + let trace = Harness::new("trace-handler").build().await?; + assert!(trace.exports().exports_types()); + assert!(!trace.exports().exports_client()); + assert!(trace.exports().exports_handler()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn handle_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-handler").build().await?; + let (status, header) = trace + .run(async |accessor, trace| { + let request = trace + .new_request( + accessor, + Method::Get, + Scheme::Https, + "example.com", + "/hello", + ) + .await?; + let response: ResourceAny = trace + .wasi_http_handler() + .call_handle(accessor, request) + .await? + .expect("handle"); + // the response is a traced resource + let types = trace.wasi_http_types(); + let status = types + .response() + .call_get_status_code(accessor, response) + .await?; + let headers = types + .response() + .call_get_headers(accessor, response) + .await?; + let header = types + .fields() + .call_get(accessor, headers, UPSTREAM_HEADER.0.to_string()) + .await?; + Ok((status, header)) + }) + .await?; + // the upstream response is passed through + assert_eq!(status, UPSTREAM_STATUS); + assert_eq!(header, vec![UPSTREAM_HEADER.1.as_bytes().to_vec()]); + assert_eq!( + trace.recorder().requests(), + vec![SentRequest { + method: "GET".to_string(), + uri: "https://example.com/hello".to_string(), + }] + ); + assert_eq!( + trace.recorder().masked_logs(), + vec![ + traced("OPERATION=wasi:http/types#fields.new"), + traced("OPERATION=wasi:http/types#request.new"), + traced("OPERATION=wasi:http/types#request.set-method SELF=&---- METHOD=get"), + traced("OPERATION=wasi:http/types#request.set-scheme SELF=&---- SCHEME=some"), + traced( + "OPERATION=wasi:http/types#request.set-authority SELF=&---- AUTHORITY=some" + ), + traced( + "OPERATION=wasi:http/types#request.set-path-with-query SELF=&---- PATH-WITH-QUERY=some" + ), + traced("OPERATION=wasi:http/handler#handle REQUEST=&----"), + traced("OPERATION=wasi:http/types#response.get-status-code SELF=&----"), + traced("OPERATION=wasi:http/types#response.get-headers SELF=&----"), + traced("OPERATION=wasi:http/types#fields.get SELF=&---- NAME=x-upstream"), + ] + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn handle_error_passed_through() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-handler").build().await?; + let result = trace + .run(async |accessor, trace| { + let scheme = Scheme::Other("gopher".to_string()); + let request = trace + .new_request(accessor, Method::Get, scheme, "example.com", "/") + .await?; + let result = trace + .wasi_http_handler() + .call_handle(accessor, request) + .await?; + Ok(result.map(|_| ())) + }) + .await?; + // the upstream error is passed through, the request never left + assert!( + matches!(result, Err(ErrorCode::HttpProtocolError)), + "unsupported scheme: {result:?}" + ); + assert_eq!(trace.recorder().requests(), vec![]); + assert_eq!( + trace.recorder().masked_logs().last(), + Some(&traced("OPERATION=wasi:http/handler#handle REQUEST=&----")) + ); + Ok(()) +} diff --git a/components/trace-handler/tests/handles.rs b/components/trace-handler/tests/handles.rs new file mode 100644 index 0000000..1c84118 --- /dev/null +++ b/components/trace-handler/tests/handles.rs @@ -0,0 +1,55 @@ +//! The handle logged for a resource must be the same in every message about it, including when +//! it is passed to `wasi:http/handler#handle`. + +use test_harness::{ErrorCode, Harness, Method, Scheme, ready}; + +fn same_handle(values: Vec, resource: &str) -> String { + let first = values + .first() + .unwrap_or_else(|| panic!("no handle logged for the {resource}")) + .clone(); + assert!( + values.iter().all(|value| *value == first), + "the {resource} was logged with different handles: {values:?}" + ); + first +} + +#[tokio::test(flavor = "multi_thread")] +async fn handle_logs_the_request_handle() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-handler").build().await?; + trace + .run(async |accessor, trace| { + let request = trace + .new_request(accessor, Method::Get, Scheme::Https, "example.com", "/") + .await?; + let response = trace + .wasi_http_handler() + .call_handle(accessor, request) + .await? + .expect("handle"); + let responses = trace.wasi_http_types().response(); + responses.call_get_status_code(accessor, response).await?; + responses.call_get_headers(accessor, response).await?; + let res = ready(accessor, Ok::<(), ErrorCode>(()))?; + responses.call_consume_body(accessor, response, res).await?; + Ok(()) + }) + .await?; + let recorder = trace.recorder(); + let request = same_handle( + recorder.handle_values("wasi:http/types#request."), + "request", + ); + assert_eq!( + recorder.handle_values("wasi:http/handler#handle"), + vec![request], + "handle logs the handle of the request it was passed" + ); + // the response from upstream is a new resource, its handle is stable too + same_handle( + recorder.handle_values("wasi:http/types#response."), + "response", + ); + Ok(()) +} diff --git a/components/trace-types/Cargo.toml b/components/trace-types/Cargo.toml new file mode 100644 index 0000000..4cdbbd8 --- /dev/null +++ b/components/trace-types/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "trace-types" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other trace components +path = "../../crates/trace/src/lib.rs" + +[features] +default = [] +client = [] +handler = [] + +[dependencies] +wit-bindgen = { workspace = true, features = ["async-spawn"] } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/trace-types/README.md b/components/trace-types/README.md new file mode 100644 index 0000000..3f00f94 --- /dev/null +++ b/components/trace-types/README.md @@ -0,0 +1,11 @@ +# `trace-types` + +Virtualizes the wasi:http/types interface logging all method calls at the TRACE level and 'componentized-trace' component. + +Built from the sources shared with the other trace components in [`crates/trace`](../../crates/trace/src/lib.rs). + +## The `trace-types` World + +- exports `wasi:http/types` +- imports `wasi:http/types` +- imports `wasi:logging/logging` diff --git a/components/trace-types/tests/handles.rs b/components/trace-types/tests/handles.rs new file mode 100644 index 0000000..3c3dbc9 --- /dev/null +++ b/components/trace-types/tests/handles.rs @@ -0,0 +1,214 @@ +//! The handle logged for a resource identifies it while debugging. The value depends on the +//! runtime, but must be the same in every message about the same resource, whatever name it is +//! logged under, e.g. `SELF` or `THIS`. + +use test_harness::{ErrorCode, Harness, Method, Scheme, ready}; + +/// The single value logged for a resource, panics if it was not logged or logged with different +/// values. +fn same_handle(values: Vec, resource: &str) -> String { + let first = values + .first() + .unwrap_or_else(|| panic!("no handle logged for the {resource}")) + .clone(); + assert!( + values.iter().all(|value| *value == first), + "the {resource} was logged with different handles: {values:?}" + ); + first +} + +#[tokio::test(flavor = "multi_thread")] +async fn fields_handle_is_stable() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + trace + .run(async |accessor, trace| { + let fields = trace.wasi_http_types().fields(); + let a = fields.call_constructor(accessor).await?; + let b = fields.call_constructor(accessor).await?; + fields.call_has(accessor, a, "x".to_string()).await?; + fields.call_has(accessor, b, "x".to_string()).await?; + fields.call_copy_all(accessor, a).await?; + fields.call_get(accessor, a, "x".to_string()).await?; + fields.call_copy_all(accessor, b).await?; + Ok(()) + }) + .await?; + let values = trace.recorder().handle_values("wasi:http/types#fields."); + let a = same_handle( + vec![values[0].clone(), values[2].clone(), values[3].clone()], + "fields", + ); + let b = same_handle(vec![values[1].clone(), values[4].clone()], "fields"); + assert_ne!(a, b, "each fields has its own handle"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn request_handle_is_stable() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + trace + .run(async |accessor, trace| { + let request = trace + .new_request(accessor, Method::Get, Scheme::Https, "example.com", "/") + .await?; + let requests = trace.wasi_http_types().request(); + requests.call_get_method(accessor, request).await?; + requests.call_get_headers(accessor, request).await?; + requests.call_get_options(accessor, request).await?; + let res = ready(accessor, Ok::<(), ErrorCode>(()))?; + requests.call_consume_body(accessor, request, res).await?; + Ok(()) + }) + .await?; + let handles = trace.recorder().handles(); + // logged as SELF by the methods and THIS by consume-body + assert!(handles.iter().any(|handle| handle.key == "SELF")); + assert!(handles.iter().any(|handle| handle.key == "THIS")); + same_handle( + trace.recorder().handle_values("wasi:http/types#request."), + "request", + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn request_options_handle_is_stable() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + trace + .run(async |accessor, trace| { + let options = trace.wasi_http_types().request_options(); + let opts = options.call_constructor(accessor).await?; + options + .call_set_connect_timeout(accessor, opts, Some(1)) + .await? + .expect("set-connect-timeout"); + options.call_get_connect_timeout(accessor, opts).await?; + let clone = options.call_clone(accessor, opts).await?; + options.call_get_first_byte_timeout(accessor, clone).await?; + options + .call_get_between_bytes_timeout(accessor, clone) + .await?; + options + .call_get_between_bytes_timeout(accessor, opts) + .await?; + Ok(()) + }) + .await?; + let values = trace + .recorder() + .handle_values("wasi:http/types#request-options."); + // set, get, clone and the last get are on the original, the others on the clone + let original = same_handle( + vec![ + values[0].clone(), + values[1].clone(), + values[2].clone(), + values[5].clone(), + ], + "request-options", + ); + let clone = same_handle( + vec![values[3].clone(), values[4].clone()], + "request-options clone", + ); + assert_ne!(original, clone, "the clone has its own handle"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn response_handle_is_stable() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + trace + .run(async |accessor, trace| { + let types = trace.wasi_http_types(); + let headers = types.fields().call_constructor(accessor).await?; + let trailers = ready(accessor, Ok(None))?; + let responses = types.response(); + let (response, _transmitted) = responses + .call_new(accessor, headers, None, trailers) + .await?; + responses.call_get_status_code(accessor, response).await?; + responses + .call_set_status_code(accessor, response, 201) + .await? + .expect("set-status-code"); + responses.call_get_headers(accessor, response).await?; + let res = ready(accessor, Ok::<(), ErrorCode>(()))?; + responses.call_consume_body(accessor, response, res).await?; + Ok(()) + }) + .await?; + let handles = trace.recorder().handles(); + // logged as SELF by the methods and THIS by consume-body + assert!(handles.iter().any(|handle| handle.key == "SELF")); + assert!(handles.iter().any(|handle| handle.key == "THIS")); + same_handle( + trace.recorder().handle_values("wasi:http/types#response."), + "response", + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn handles_are_hex() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + trace + .run(async |accessor, trace| { + let fields = trace.wasi_http_types().fields(); + // enough resources for handles past 9 + for _ in 0..32 { + let f = fields.call_constructor(accessor).await?; + fields.call_has(accessor, f, "x".to_string()).await?; + } + Ok(()) + }) + .await?; + let values = trace.recorder().handle_values("wasi:http/types#fields."); + assert_eq!(values.len(), 32); + for value in &values { + let digits = value + .strip_prefix('&') + .unwrap_or_else(|| panic!("prefixed with &: {value}")); + assert_eq!(digits.len(), 4, "{value}"); + assert!( + digits + .bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()), + "lowercase hex: {value}" + ); + } + assert!( + values + .iter() + .any(|value| value.bytes().any(|b| (b'a'..=b'f').contains(&b))), + "handles past 9 use hex digits: {values:?}" + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn values_are_not_mistaken_for_handles() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + trace + .run(async |accessor, trace| { + let fields = trace.wasi_http_types().fields(); + let headers = fields.call_constructor(accessor).await?; + // a name and value with the shape of a handle's digits + fields + .call_append(accessor, headers, "beef".to_string(), b"cafe".to_vec()) + .await? + .expect("append"); + Ok(()) + }) + .await?; + let handles = trace.recorder().handles(); + assert_eq!(handles.len(), 1, "{handles:?}"); + assert_eq!(handles[0].key, "SELF"); + let logs = trace.recorder().masked_logs(); + assert_eq!( + logs.last().map(|log| log.message.as_str()), + Some("OPERATION=wasi:http/types#fields.append SELF=&---- NAME=beef VALUE=cafe") + ); + Ok(()) +} diff --git a/components/trace-types/tests/types.rs b/components/trace-types/tests/types.rs new file mode 100644 index 0000000..44a07dd --- /dev/null +++ b/components/trace-types/tests/types.rs @@ -0,0 +1,339 @@ +//! Tests for the `wasi:http/types` operations traced by every trace component, run against +//! `trace-types`. + +use test_harness::{ErrorCode, Harness, LogEntry, Method, Scheme, ready}; + +fn traced(message: &str) -> LogEntry { + LogEntry::trace("componentized-trace", message) +} + +#[tokio::test(flavor = "multi_thread")] +async fn exports_only_types() -> wasmtime::Result<()> { + let trace = Harness::new("trace-types").build().await?; + assert!(trace.exports().exports_types()); + assert!(!trace.exports().exports_client()); + assert!(!trace.exports().exports_handler()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn fields_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + let results = trace + .run(async |accessor, trace| { + let fields = trace.wasi_http_types().fields(); + let headers = fields + .call_from_list( + accessor, + vec![("accept".to_string(), b"text/plain".to_vec())], + ) + .await? + .expect("from-list"); + fields + .call_append(accessor, headers, "x-a".to_string(), b"1".to_vec()) + .await? + .expect("append"); + fields + .call_set( + accessor, + headers, + "x-b".to_string(), + vec![b"2".to_vec(), b"3".to_vec()], + ) + .await? + .expect("set"); + let get = fields + .call_get(accessor, headers, "x-b".to_string()) + .await?; + let has = fields + .call_has(accessor, headers, "x-a".to_string()) + .await?; + fields + .call_delete(accessor, headers, "x-a".to_string()) + .await? + .expect("delete"); + let deleted = fields + .call_get_and_delete(accessor, headers, "x-b".to_string()) + .await? + .expect("get-and-delete"); + let copy = fields.call_copy_all(accessor, headers).await?; + let clone = fields.call_clone(accessor, headers).await?; + let cloned = fields.call_copy_all(accessor, clone).await?; + let empty = fields.call_constructor(accessor).await?; + let empty = fields.call_copy_all(accessor, empty).await?; + Ok((get, has, deleted, copy, cloned, empty)) + }) + .await?; + let (get, has, deleted, copy, cloned, empty) = results; + // the upstream values are passed through + assert_eq!(get, vec![b"2".to_vec(), b"3".to_vec()]); + assert!(has); + assert_eq!(deleted, vec![b"2".to_vec(), b"3".to_vec()]); + assert_eq!(copy, vec![("accept".to_string(), b"text/plain".to_vec())]); + assert_eq!(cloned, copy); + assert_eq!(empty, vec![]); + assert_eq!( + trace.recorder().masked_logs(), + vec![ + traced("OPERATION=wasi:http/types#fields.from-list ENTRIES-LEN=1"), + traced("OPERATION=wasi:http/types#fields.append SELF=&---- NAME=x-a VALUE=1"), + traced("OPERATION=wasi:http/types#fields.set SELF=&---- NAME=x-b VALUE-LEN=2"), + traced("OPERATION=wasi:http/types#fields.get SELF=&---- NAME=x-b"), + traced("OPERATION=wasi:http/types#fields.has SELF=&---- NAME=x-a"), + traced("OPERATION=wasi:http/types#fields.delete SELF=&---- NAME=x-a"), + traced("OPERATION=wasi:http/types#fields.get-and-delete SELF=&---- NAME=x-b"), + traced("OPERATION=wasi:http/types#fields.copy-all SELF=&----"), + traced("OPERATION=wasi:http/types#fields.clone SELF=&----"), + traced("OPERATION=wasi:http/types#fields.copy-all SELF=&----"), + traced("OPERATION=wasi:http/types#fields.new"), + traced("OPERATION=wasi:http/types#fields.copy-all SELF=&----"), + ] + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn fields_error_passed_through() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + let result = trace + .run(async |accessor, trace| { + let fields = trace.wasi_http_types().fields(); + let headers = fields.call_constructor(accessor).await?; + Ok(fields + .call_append(accessor, headers, "bad name".to_string(), b"1".to_vec()) + .await?) + }) + .await?; + assert!(result.is_err(), "invalid header name: {result:?}"); + assert_eq!( + trace.recorder().masked_logs(), + vec![ + traced("OPERATION=wasi:http/types#fields.new"), + traced("OPERATION=wasi:http/types#fields.append SELF=&---- NAME=bad name VALUE=1"), + ] + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn request_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + let results = trace + .run(async |accessor, trace| { + let request = trace + .new_request( + accessor, + Method::Post, + Scheme::Http, + "example.com", + "/a?b=c", + ) + .await?; + let types = trace.wasi_http_types(); + let requests = types.request(); + let method = requests.call_get_method(accessor, request).await?; + let scheme = requests.call_get_scheme(accessor, request).await?; + let authority = requests.call_get_authority(accessor, request).await?; + let path_with_query = requests.call_get_path_with_query(accessor, request).await?; + let options = requests.call_get_options(accessor, request).await?; + let headers = requests.call_get_headers(accessor, request).await?; + let headers = types.fields().call_copy_all(accessor, headers).await?; + let res = ready(accessor, Ok::<(), ErrorCode>(()))?; + let (_contents, _trailers) = requests.call_consume_body(accessor, request, res).await?; + Ok(( + method, + scheme, + authority, + path_with_query, + options.is_some(), + headers, + )) + }) + .await?; + let (method, scheme, authority, path_with_query, has_options, headers) = results; + // the upstream values are passed through + assert!(matches!(method, Method::Post), "{method:?}"); + assert!(matches!(scheme, Some(Scheme::Http)), "{scheme:?}"); + assert_eq!(authority.as_deref(), Some("example.com")); + assert_eq!(path_with_query.as_deref(), Some("/a?b=c")); + assert!(!has_options); + assert_eq!(headers, vec![]); + assert_eq!( + trace.recorder().masked_logs(), + vec![ + traced("OPERATION=wasi:http/types#fields.new"), + traced("OPERATION=wasi:http/types#request.new"), + traced("OPERATION=wasi:http/types#request.set-method SELF=&---- METHOD=post"), + traced("OPERATION=wasi:http/types#request.set-scheme SELF=&---- SCHEME=some"), + traced( + "OPERATION=wasi:http/types#request.set-authority SELF=&---- AUTHORITY=some" + ), + traced( + "OPERATION=wasi:http/types#request.set-path-with-query SELF=&---- PATH-WITH-QUERY=some" + ), + traced("OPERATION=wasi:http/types#request.get-method SELF=&----"), + traced("OPERATION=wasi:http/types#request.get-scheme SELF=&----"), + traced("OPERATION=wasi:http/types#request.get-authority SELF=&----"), + traced("OPERATION=wasi:http/types#request.get-path-with-query SELF=&----"), + traced("OPERATION=wasi:http/types#request.get-options SELF=&----"), + traced("OPERATION=wasi:http/types#request.get-headers SELF=&----"), + traced("OPERATION=wasi:http/types#fields.copy-all SELF=&----"), + traced("OPERATION=wasi:http/types#request.consume-body THIS=&----"), + ] + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn request_options_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + let results = trace + .run(async |accessor, trace| { + let options = trace.wasi_http_types().request_options(); + let opts = options.call_constructor(accessor).await?; + options + .call_set_connect_timeout(accessor, opts, Some(1_000)) + .await? + .expect("set-connect-timeout"); + options + .call_set_first_byte_timeout(accessor, opts, Some(2_000)) + .await? + .expect("set-first-byte-timeout"); + options + .call_set_between_bytes_timeout(accessor, opts, None) + .await? + .expect("set-between-bytes-timeout"); + let clone = options.call_clone(accessor, opts).await?; + let connect = options.call_get_connect_timeout(accessor, clone).await?; + let first_byte = options.call_get_first_byte_timeout(accessor, clone).await?; + let between_bytes = options + .call_get_between_bytes_timeout(accessor, clone) + .await?; + Ok((connect, first_byte, between_bytes)) + }) + .await?; + // the upstream values are passed through, including to the clone + assert_eq!(results, (Some(1_000), Some(2_000), None)); + assert_eq!( + trace.recorder().masked_logs(), + vec![ + traced("OPERATION=wasi:http/types#request-options.new"), + traced( + "OPERATION=wasi:http/types#request-options.set-connect-timeout SELF=&---- DURATION=some<1000>" + ), + traced( + "OPERATION=wasi:http/types#request-options.set-first-byte-timeout SELF=&---- DURATION=some<2000>" + ), + traced( + "OPERATION=wasi:http/types#request-options.set-between-bytes-timeout SELF=&---- DURATION=none" + ), + traced("OPERATION=wasi:http/types#request-options.clone SELF=&----"), + traced("OPERATION=wasi:http/types#request-options.get-connect-timeout SELF=&----"), + traced("OPERATION=wasi:http/types#request-options.get-first-byte-timeout SELF=&----"), + traced( + "OPERATION=wasi:http/types#request-options.get-between-bytes-timeout SELF=&----" + ), + ] + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn request_with_options_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + let connect = trace + .run(async |accessor, trace| { + let types = trace.wasi_http_types(); + let options = types.request_options(); + let opts = options.call_constructor(accessor).await?; + options + .call_set_connect_timeout(accessor, opts, Some(1_000)) + .await? + .expect("set-connect-timeout"); + let headers = types.fields().call_constructor(accessor).await?; + let trailers = ready(accessor, Ok(None))?; + let (request, _transmitted) = types + .request() + .call_new(accessor, headers, None, trailers, Some(opts)) + .await?; + let opts = types + .request() + .call_get_options(accessor, request) + .await? + .expect("options"); + Ok(options.call_get_connect_timeout(accessor, opts).await?) + }) + .await?; + // the options are unwrapped for the upstream request, and wrapped again when read back + assert_eq!(connect, Some(1_000)); + assert_eq!( + trace.recorder().masked_logs(), + vec![ + traced("OPERATION=wasi:http/types#request-options.new"), + traced( + "OPERATION=wasi:http/types#request-options.set-connect-timeout SELF=&---- DURATION=some<1000>" + ), + traced("OPERATION=wasi:http/types#fields.new"), + traced("OPERATION=wasi:http/types#request.new"), + traced("OPERATION=wasi:http/types#request.get-options SELF=&----"), + traced("OPERATION=wasi:http/types#request-options.get-connect-timeout SELF=&----"), + ] + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn response_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace-types").build().await?; + let results = trace + .run(async |accessor, trace| { + let types = trace.wasi_http_types(); + let headers = types + .fields() + .call_from_list(accessor, vec![("x-a".to_string(), b"1".to_vec())]) + .await? + .expect("from-list"); + let trailers = ready(accessor, Ok(None))?; + let responses = types.response(); + let (response, _transmitted) = responses + .call_new(accessor, headers, None, trailers) + .await?; + let default_status = responses.call_get_status_code(accessor, response).await?; + responses + .call_set_status_code(accessor, response, 404) + .await? + .expect("set-status-code"); + let status = responses.call_get_status_code(accessor, response).await?; + let invalid = responses + .call_set_status_code(accessor, response, 42) + .await?; + let headers = responses.call_get_headers(accessor, response).await?; + let headers = types.fields().call_copy_all(accessor, headers).await?; + let res = ready(accessor, Ok::<(), ErrorCode>(()))?; + let (_contents, _trailers) = + responses.call_consume_body(accessor, response, res).await?; + Ok((default_status, status, invalid, headers)) + }) + .await?; + let (default_status, status, invalid, headers) = results; + // the upstream values and errors are passed through + assert_eq!(default_status, 200); + assert_eq!(status, 404); + assert_eq!(invalid, Err(())); + assert_eq!(headers, vec![("x-a".to_string(), b"1".to_vec())]); + assert_eq!( + trace.recorder().masked_logs(), + vec![ + traced("OPERATION=wasi:http/types#fields.from-list ENTRIES-LEN=1"), + traced("OPERATION=wasi:http/types#response.new"), + traced("OPERATION=wasi:http/types#response.get-status-code SELF=&----"), + traced("OPERATION=wasi:http/types#response.set-status-code SELF=&---- STATUS-CODE=404"), + traced("OPERATION=wasi:http/types#response.get-status-code SELF=&----"), + traced("OPERATION=wasi:http/types#response.set-status-code SELF=&---- STATUS-CODE=42"), + traced("OPERATION=wasi:http/types#response.get-headers SELF=&----"), + traced("OPERATION=wasi:http/types#fields.copy-all SELF=&----"), + traced("OPERATION=wasi:http/types#response.consume-body THIS=&----"), + ] + ); + Ok(()) +} diff --git a/components/trace/Cargo.toml b/components/trace/Cargo.toml new file mode 100644 index 0000000..02e5734 --- /dev/null +++ b/components/trace/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "trace" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] +# shared with the other trace components +path = "../../crates/trace/src/lib.rs" + +[features] +default = ["client", "handler"] +client = [] +handler = [] + +[dependencies] +wit-bindgen = { workspace = true, features = ["async-spawn"] } + +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/trace/README.md b/components/trace/README.md new file mode 100644 index 0000000..849da90 --- /dev/null +++ b/components/trace/README.md @@ -0,0 +1,15 @@ +# `trace` + +Virtualizes the wasi:http/types, wasi:http/client and wasi:http/handler interfaces logging all method calls at the TRACE level and 'componentized-trace' component. + +Built from the sources shared with the other trace components in [`crates/trace`](../../crates/trace/src/lib.rs). + +## The `trace` World + +- exports `wasi:http/types` +- exports `wasi:http/client` +- exports `wasi:http/handler` +- imports `wasi:http/types` +- imports `wasi:http/client` +- imports `wasi:http/handler` +- imports `wasi:logging/logging` diff --git a/components/trace/tests/trace.rs b/components/trace/tests/trace.rs new file mode 100644 index 0000000..b417c90 --- /dev/null +++ b/components/trace/tests/trace.rs @@ -0,0 +1,94 @@ +//! Tests for `trace`, which traces `wasi:http/types`, `wasi:http/client` and `wasi:http/handler`. +//! Tests for the individual operations live with `trace-types`, `trace-client` and +//! `trace-handler`, which are built from the same sources. + +use test_harness::{Harness, LogEntry, Method, Scheme, SentRequest, UPSTREAM_STATUS}; + +fn traced(message: &str) -> LogEntry { + LogEntry::trace("componentized-trace", message) +} + +#[tokio::test(flavor = "multi_thread")] +async fn exports_types_client_and_handler() -> wasmtime::Result<()> { + let trace = Harness::new("trace").build().await?; + assert!(trace.exports().exports_types()); + assert!(trace.exports().exports_client()); + assert!(trace.exports().exports_handler()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn send_and_handle_traced() -> wasmtime::Result<()> { + let mut trace = Harness::new("trace").build().await?; + let statuses = trace + .run(async |accessor, trace| { + let types = trace.wasi_http_types(); + + let request = trace + .new_request( + accessor, + Method::Get, + Scheme::Https, + "example.com", + "/client", + ) + .await?; + let response = trace + .wasi_http_client() + .call_send(accessor, request) + .await? + .expect("send"); + let sent = types + .response() + .call_get_status_code(accessor, response) + .await?; + + let request = trace + .new_request( + accessor, + Method::Put, + Scheme::Https, + "example.com", + "/handler", + ) + .await?; + let response = trace + .wasi_http_handler() + .call_handle(accessor, request) + .await? + .expect("handle"); + let handled = types + .response() + .call_get_status_code(accessor, response) + .await?; + + Ok((sent, handled)) + }) + .await?; + assert_eq!(statuses, (UPSTREAM_STATUS, UPSTREAM_STATUS)); + assert_eq!( + trace.recorder().requests(), + vec![ + SentRequest { + method: "GET".to_string(), + uri: "https://example.com/client".to_string(), + }, + SentRequest { + method: "PUT".to_string(), + uri: "https://example.com/handler".to_string(), + }, + ] + ); + let logs = trace.recorder().masked_logs(); + // types, client and handler are all traced + assert!(logs.contains(&traced("OPERATION=wasi:http/types#request.new"))); + assert!(logs.contains(&traced("OPERATION=wasi:http/client#send REQUEST=&----"))); + assert!(logs.contains(&traced("OPERATION=wasi:http/handler#handle REQUEST=&----"))); + assert_eq!( + logs.iter() + .filter(|log| log.message.contains("response.get-status-code")) + .count(), + 2 + ); + Ok(()) +} diff --git a/components/wit/deps/wasi-logging-0.1.0-draft/package.wit b/components/wit/deps/wasi-logging-0.1.0-draft/package.wit new file mode 100644 index 0000000..164cb5b --- /dev/null +++ b/components/wit/deps/wasi-logging-0.1.0-draft/package.wit @@ -0,0 +1,36 @@ +package wasi:logging@0.1.0-draft; + +/// WASI Logging is a logging API intended to let users emit log messages with +/// simple priority levels and context values. +interface logging { + /// A log level, describing a kind of message. + enum level { + /// Describes messages about the values of variables and the flow of + /// control within a program. + trace, + /// Describes messages likely to be of interest to someone debugging a + /// program. + debug, + /// Describes messages likely to be of interest to someone monitoring a + /// program. + info, + /// Describes messages indicating hazardous situations. + warn, + /// Describes messages indicating serious errors. + error, + /// Describes messages indicating fatal errors. + critical, + } + + /// Emit a log message. + /// + /// A log message has a `level` describing what kind of message is being + /// sent, a context, which is an uninterpreted string meant to help + /// consumers group similar messages, and a string containing the message + /// text. + log: func(level: level, context: string, message: string); +} + +world imports { + import logging; +} diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 037bbc6..dc14a55 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -4,3 +4,32 @@ world client { import wasi:http/client@0.3.0; export componentized:http/client@0.1.0-dev; } + +world trace-componentized-client { + import componentized:http/client@0.1.0-dev; + import wasi:logging/logging@0.1.0-draft; + export componentized:http/client@0.1.0-dev; +} + +world trace-types { + import wasi:http/types@0.3.0; + import wasi:logging/logging@0.1.0-draft; + export wasi:http/types@0.3.0; +} + +world trace-handler { + include trace-types; + import wasi:http/handler@0.3.0; + export wasi:http/handler@0.3.0; +} + +world trace-client { + include trace-types; + import wasi:http/client@0.3.0; + export wasi:http/client@0.3.0; +} + +world trace { + include trace-client; + include trace-handler; +} diff --git a/components/wkg.lock b/components/wkg.lock index 3912411..dccfcbf 100644 --- a/components/wkg.lock +++ b/components/wkg.lock @@ -24,3 +24,12 @@ digest = "sha256:92cd8f3730c00226dc15626a2e7b21834dd187fc221f09818720d228585bbbf requirement = "=0.3.1" version = "0.3.1" digest = "sha256:fac6e40c9b8101cb0a32c76ae63271ffb7c9a66007214d57f3ae8048844d2b5f" + +[[packages]] +name = "wasi:logging" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.1.0-draft" +version = "0.1.0-draft" +digest = "sha256:09621a45b12b0a9cddc798517f778aac0e5ae4bd234077b3d70758d6cf625580" diff --git a/crates/test-harness/Cargo.toml b/crates/test-harness/Cargo.toml new file mode 100644 index 0000000..507f60c --- /dev/null +++ b/crates/test-harness/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "test-harness" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +publish = false + +[dependencies] +bytes = { workspace = true } +http = { workspace = true } +http-body-util = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } +wasmtime-wasi-http = { workspace = true } diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs new file mode 100644 index 0000000..49c79d5 --- /dev/null +++ b/crates/test-harness/src/lib.rs @@ -0,0 +1,800 @@ +//! Test harness for the trace components. +//! +//! A [`Harness`] instantiates a component (`target/components/*/*.wasm`) with `wasi:http` from +//! wasmtime-wasi-http and captured `wasi:logging` output. Requests the component sends upstream, +//! with either `wasi:http/client` or `wasi:http/handler`, never reach the network, they are +//! recorded and answered with a canned response. + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::process::Command; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use bytes::Bytes; +use http_body_util::{BodyExt, Empty}; +use tokio::sync::{mpsc, oneshot}; +use wasmtime::component::{ + Accessor, Component, FutureConsumer, FutureReader, HasData, HasSelf, Lift, Linker, Lower, + Resource, ResourceTable, Source, StreamConsumer, StreamReader, StreamResult, +}; +use wasmtime::error::Context as _; +use wasmtime::{Config, Engine, Result, Store, StoreContextMut, bail, format_err}; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; +use wasmtime_wasi_http::p3::bindings::http::client::HostWithStore as _; +use wasmtime_wasi_http::{ + RequestOptions, WasiBody, WasiHttp, WasiHttpCtx, WasiHttpCtxView, WasiHttpHooks, WasiHttpView, +}; + +use crate::bindings::componentized::http::client as upstream_client; +use crate::bindings::exports::componentized::http::client as http_client; +use crate::bindings::exports::wasi::http::{client, handler, types}; +use crate::bindings::wasi::logging::logging; + +pub mod bindings { + wasmtime::component::bindgen!({ + path: "../../components/wit", + inline: " + package componentized:test-harness; + + world harness { + import wasi:logging/logging@0.1.0-draft; + import componentized:http/client@0.1.0-dev; + export componentized:http/client@0.1.0-dev; + export wasi:http/types@0.3.0; + export wasi:http/client@0.3.0; + export wasi:http/handler@0.3.0; + } + ", + world: "componentized:test-harness/harness", + imports: { + "componentized:http/client": async | store, + }, + exports: { default: async | store }, + with: { + "wasi:clocks": wasmtime_wasi::p3::bindings::clocks, + }, + }); +} + +pub use bindings::exports::componentized::http::client::{ + ErrorCode as HttpClientErrorCode, Method as HttpClientMethod, +}; +pub use bindings::exports::wasi::http::types::{ErrorCode, Method, Scheme}; +pub use bindings::wasi::logging::logging::Level; +pub use wasmtime::component::ResourceAny; + +/// The status of the canned response to requests sent upstream. +pub const UPSTREAM_STATUS: u16 = 200; + +/// A header of the canned response to requests sent upstream, `(name, value)`. +pub const UPSTREAM_HEADER: (&str, &str) = ("x-upstream", "canned"); + +/// The number of trailing hex digits of a resource handle the trace components log. +const HANDLE_DIGITS: usize = 4; + +/// The prefix marking a logged value as a resource handle. +const HANDLE_PREFIX: char = '&'; + +/// The trailing hex digits of a logged resource handle, e.g. `00a3` for `&00a3`, `None` if the +/// value is not a handle. +fn handle_digits(value: &str) -> Option<&str> { + let digits = value.strip_prefix(HANDLE_PREFIX)?; + let is_hex = digits + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)); + (digits.len() == HANDLE_DIGITS && is_hex).then_some(digits) +} + +/// Replace the resource handles in a trace message, e.g. `SELF=&00a3` becomes `SELF=&----`. +/// +/// Handles are assigned by the runtime, masking them keeps assertions about what was logged +/// independent of the order resources were created and dropped. +pub fn mask_handles(message: &str) -> String { + message + .split(' ') + .map(|part| match part.split_once('=') { + Some((key, value)) if handle_digits(value).is_some() => { + format!("{key}={HANDLE_PREFIX}{}", "-".repeat(HANDLE_DIGITS)) + } + _ => part.to_string(), + }) + .collect::>() + .join(" ") +} + +/// A future for the guest that resolves to the value. +pub fn ready( + accessor: &Accessor, + value: T, +) -> Result> { + accessor.with(|store| FutureReader::new(store, async move { Ok::<_, wasmtime::Error>(value) })) +} + +/// Read every item written to a guest stream, until it closes. +pub async fn collect( + accessor: &Accessor, + stream: StreamReader, +) -> Result> { + let (tx, mut rx) = mpsc::unbounded_channel(); + accessor.with(|store| stream.pipe(store, ChannelConsumer(tx)))?; + let mut items = vec![]; + while let Some(item) = rx.recv().await { + items.push(item); + } + Ok(items) +} + +/// Wait for the value of a guest future. +/// +/// Only for futures the guest writes itself. A future the guest passes through from a +/// wasmtime-wasi-http host import is transferred host to host, which requires the same Rust type on +/// both ends, and the harness' exported types are generated separately from wasmtime-wasi-http's. +pub async fn resolve( + accessor: &Accessor, + future: FutureReader, +) -> Result { + let (tx, rx) = oneshot::channel(); + accessor.with(|store| future.pipe(store, OneshotConsumer(Some(tx))))?; + rx.await + .map_err(|_| format_err!("future closed without a value")) +} + +struct OneshotConsumer(Option>); + +impl FutureConsumer for OneshotConsumer { + type Item = T; + + fn poll_consume( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + store: StoreContextMut, + mut source: Source<'_, T>, + _finish: bool, + ) -> Poll> { + let mut item = None; + source.read(store, &mut item)?; + if let (Some(item), Some(tx)) = (item, self.get_mut().0.take()) { + // the receiver is only dropped when the test stopped waiting + let _ = tx.send(item); + } + Poll::Ready(Ok(())) + } +} + +struct ChannelConsumer(mpsc::UnboundedSender); + +impl StreamConsumer for ChannelConsumer { + type Item = T; + + fn poll_consume( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + store: StoreContextMut, + mut source: Source<'_, T>, + _finish: bool, + ) -> Poll> { + let mut item = None; + source.read(store, &mut item)?; + if let Some(item) = item { + if self.0.send(item).is_err() { + return Poll::Ready(Ok(StreamResult::Dropped)); + } + } + Poll::Ready(Ok(StreamResult::Completed)) + } +} + +/// Root of the workspace, where the Makefile lives. +fn workspace_dir() -> PathBuf { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + dir.canonicalize().unwrap_or(dir) +} + +/// Path containing the built component. +pub fn component_path(name: &str) -> PathBuf { + workspace_dir().join(format!("target/components/{name}/{name}.wasm")) +} + +/// Rebuild the named component in `target/components/` with make, unless it was already built +/// by this process. +fn ensure_built(name: &str) -> Result<()> { + static BUILT: Mutex>> = Mutex::new(None); + + // hold the lock while building so concurrent tests don't run make over each other + let mut built = BUILT.lock().unwrap_or_else(|err| err.into_inner()); + let built = built.get_or_insert_with(HashSet::new); + if built.contains(name) { + return Ok(()); + } + + // the Makefile's targets are relative to the workspace, make matches them textually + let target = format!("target/components/{name}/{name}.wasm"); + let output = Command::new("make") + .arg("-C") + .arg(workspace_dir()) + .arg(&target) + .output() + .context("failed to run make")?; + if !output.status.success() { + bail!( + "failed to build {target}:\n{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + built.insert(name.to_string()); + Ok(()) +} + +/// A message logged by the test subject via `wasi:logging`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LogEntry { + pub level: Level, + pub context: String, + pub message: String, +} + +impl LogEntry { + /// A trace message logged by the test subject. + pub fn trace(context: impl Into, message: impl Into) -> Self { + Self { + level: Level::Trace, + context: context.into(), + message: message.into(), + } + } + + /// The entry with the resource handles in its message masked, see [`mask_handles`]. + pub fn masked(self) -> Self { + Self { + message: mask_handles(&self.message), + ..self + } + } +} + +/// A resource handle in a trace message, e.g. `SELF=&00a3`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LoggedHandle { + /// The traced operation, e.g. `wasi:http/types#request.get-method` + pub operation: String, + /// The name the handle is logged under, e.g. `SELF` + pub key: String, + /// The handle as logged, e.g. `&00a3` + pub value: String, +} + +/// The resource handles in a trace message. +pub fn logged_handles(message: &str) -> Vec { + let mut parts = message.split(' '); + let Some(operation) = parts + .next() + .and_then(|part| part.strip_prefix("OPERATION=")) + else { + return vec![]; + }; + parts + .filter_map(|part| part.split_once('=')) + .filter(|(_, value)| handle_digits(value).is_some()) + .map(|(key, value)| LoggedHandle { + operation: operation.to_string(), + key: key.to_string(), + value: value.to_string(), + }) + .collect() +} + +/// A request the test subject sent upstream. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SentRequest { + pub method: String, + pub uri: String, +} + +/// Observations recorded while the test subject runs. +#[derive(Clone, Default)] +pub struct Recorder { + logs: Arc>>, + requests: Arc>>, +} + +impl Recorder { + /// Messages logged by the test subject, in order. + pub fn logs(&self) -> Vec { + self.logs.lock().unwrap().clone() + } + + /// Messages logged by the test subject, in order, with resource handles masked. + pub fn masked_logs(&self) -> Vec { + self.logs().into_iter().map(LogEntry::masked).collect() + } + + /// Resource handles in the messages logged by the test subject, in order. + pub fn handles(&self) -> Vec { + self.logs() + .iter() + .flat_map(|log| logged_handles(&log.message)) + .collect() + } + + /// Values of the resource handles logged by operations starting with the prefix, in order, + /// e.g. `wasi:http/types#request.` for every request method. + pub fn handle_values(&self, operation_prefix: &str) -> Vec { + self.handles() + .into_iter() + .filter(|handle| handle.operation.starts_with(operation_prefix)) + .map(|handle| handle.value) + .collect() + } + + /// Requests the test subject sent upstream, in order. + pub fn requests(&self) -> Vec { + self.requests.lock().unwrap().clone() + } +} + +/// Answers requests sent upstream with a canned response, instead of the network. +struct Upstream { + recorder: Recorder, +} + +impl WasiHttpHooks for Upstream { + fn send_request( + &mut self, + request: http::Request, + _options: Option, + _fut: Box> + Send>, + ) -> Box< + dyn Future< + Output = Result< + ( + http::Response, + Box> + Send>, + ), + wasmtime_wasi_http::Error, + >, + > + Send, + > { + self.recorder.requests.lock().unwrap().push(SentRequest { + method: request.method().to_string(), + uri: request.uri().to_string(), + }); + Box::new(async move { + // the body is passed through the test subject to the harness host to host, which the + // harness can't read, see `resolve` + let body = Empty::::new() + .map_err(|never| match never {}) + .boxed_unsync(); + let response = http::Response::builder() + .status(UPSTREAM_STATUS) + .header(UPSTREAM_HEADER.0, UPSTREAM_HEADER.1) + .body(body) + .expect("canned response"); + let io = Box::new(async { Ok(()) }) + as Box> + Send>; + Ok((response, io)) + }) + } +} + +pub struct Ctx { + wasi: WasiCtx, + http: WasiHttpCtx, + upstream: Upstream, + table: ResourceTable, + recorder: Recorder, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } +} + +impl WasiHttpView for Ctx { + fn http(&mut self) -> WasiHttpCtxView<'_> { + WasiHttpCtxView { + ctx: &mut self.http, + table: &mut self.table, + hooks: &mut self.upstream, + } + } +} + +impl logging::Host for Ctx { + fn log(&mut self, level: Level, context: String, message: String) { + self.recorder.logs.lock().unwrap().push(LogEntry { + level, + context, + message, + }); + } +} + +/// Upstream for `componentized:http/client`, requests are recorded and answered with a canned +/// response. +struct UpstreamClient; + +impl HasData for UpstreamClient { + type Data<'a> = &'a mut Ctx; +} + +impl upstream_client::Host for Ctx {} + +impl UpstreamClient { + fn respond( + accessor: &Accessor, + method: &str, + url: String, + body: Option>, + ) -> Result { + accessor.with(|mut store| { + store + .get() + .recorder + .requests + .lock() + .unwrap() + .push(SentRequest { + method: method.to_string(), + uri: url, + }); + if let Some(mut body) = body { + body.close(&mut store) + .map_err(|err| upstream_client::ErrorCode::Other(Some(err.to_string())))?; + } + let response_body = StreamReader::new(&mut store, Vec::::new()) + .map_err(|err| upstream_client::ErrorCode::Other(Some(err.to_string())))?; + let trailers = + FutureReader::new(&mut store, async { Ok::<_, wasmtime::Error>(Ok(vec![])) }) + .map_err(|err| upstream_client::ErrorCode::Other(Some(err.to_string())))?; + Ok(upstream_client::HttpResponse { + status: UPSTREAM_STATUS, + headers: vec![(UPSTREAM_HEADER.0.to_string(), UPSTREAM_HEADER.1.to_string())], + body: response_body, + trailers, + }) + }) + } +} + +impl upstream_client::HostWithStore for UpstreamClient { + async fn request( + accessor: &Accessor, + method: upstream_client::Method, + url: String, + _headers: Vec<(String, String)>, + body: Option>, + _options: Option, + ) -> Result { + use upstream_client::Method; + let method = match method { + Method::Get => "GET", + Method::Post => "POST", + Method::Put => "PUT", + Method::Delete => "DELETE", + Method::Patch => "PATCH", + Method::Head => "HEAD", + Method::Options => "OPTIONS", + Method::Trace => "TRACE", + Method::Query => "QUERY", + }; + Self::respond(accessor, method, url, body) + } + + async fn get( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, "GET", url, None) + } + + async fn post( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + body: StreamReader, + _options: Option, + ) -> Result { + Self::respond(accessor, "POST", url, Some(body)) + } + + async fn put( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + body: StreamReader, + _options: Option, + ) -> Result { + Self::respond(accessor, "PUT", url, Some(body)) + } + + async fn delete( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, "DELETE", url, None) + } + + async fn patch( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + body: StreamReader, + _options: Option, + ) -> Result { + Self::respond(accessor, "PATCH", url, Some(body)) + } + + async fn head( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, "HEAD", url, None) + } + + async fn options( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, "OPTIONS", url, None) + } + + async fn trace( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, "TRACE", url, None) + } + + async fn query( + accessor: &Accessor, + url: String, + _headers: Vec<(String, String)>, + body: StreamReader, + _options: Option, + ) -> Result { + Self::respond(accessor, "QUERY", url, Some(body)) + } +} + +/// Define `wasi:http/handler` with the same implementation as `wasi:http/client`, the request is +/// sent upstream. +/// +/// wasmtime-wasi-http only defines `wasi:http/client`, the two have the same signature. +fn add_handler_to_linker(linker: &mut Linker) -> Result<()> { + type Request = wasmtime_wasi_http::p3::Request; + type Response = wasmtime_wasi_http::p3::Response; + type HttpErrorCode = wasmtime_wasi_http::p3::bindings::http::types::ErrorCode; + + linker + .instance("wasi:http/handler@0.3.0")? + .func_wrap_concurrent( + "handle", + |accessor: &Accessor, (request,): (Resource,)| { + Box::pin(async move { + let accessor = accessor.with_getter::(::http); + let result: Result, HttpErrorCode> = + match WasiHttp::send(&accessor, request).await { + Ok(response) => Ok(response), + Err(err) => Err(err.downcast()?), + }; + Ok((result,)) + }) + }, + ) +} + +/// Builds a subject instance for a test. +pub struct Harness { + subject: String, +} + +impl Harness { + /// Test the named component from `target/components/`, e.g. `trace-types`. + pub fn new(component_name: &str) -> Self { + Self { + subject: component_name.to_string(), + } + } + + /// Instantiate the test subject. + pub async fn build(self) -> Result { + let mut config = Config::new(); + config.wasm_component_model_async(true); + let engine = Engine::new(&config)?; + + ensure_built(&self.subject)?; + let path = component_path(&self.subject); + let component = Component::from_file(&engine, &path) + .with_context(|| format!("failed to load {}", path.display()))?; + + let mut linker = Linker::new(&engine); + wasmtime_wasi::p3::add_to_linker(&mut linker)?; + wasmtime_wasi_http::p3::add_to_linker(&mut linker)?; + add_handler_to_linker(&mut linker)?; + upstream_client::add_to_linker::<_, UpstreamClient>(&mut linker, |ctx| ctx)?; + logging::add_to_linker::<_, HasSelf>(&mut linker, |ctx| ctx)?; + + let recorder = Recorder::default(); + let mut store = Store::new( + &engine, + Ctx { + wasi: WasiCtxBuilder::new().inherit_stdio().build(), + http: WasiHttpCtx::new(), + upstream: Upstream { + recorder: recorder.clone(), + }, + table: ResourceTable::new(), + recorder: recorder.clone(), + }, + ); + let instance_pre = linker.instantiate_pre(&component)?; + let instance = instance_pre + .instantiate_async(&mut store) + .await + .with_context(|| format!("failed to instantiate {}", self.subject))?; + let exports = Exports { + name: self.subject, + types: match types::GuestIndices::new(&instance_pre) { + Ok(indices) => Some(indices.load(&mut store, &instance)?), + Err(_) => None, + }, + client: match client::GuestIndices::new(&instance_pre) { + Ok(indices) => Some(indices.load(&mut store, &instance)?), + Err(_) => None, + }, + handler: match handler::GuestIndices::new(&instance_pre) { + Ok(indices) => Some(indices.load(&mut store, &instance)?), + Err(_) => None, + }, + http_client: match http_client::GuestIndices::new(&instance_pre) { + Ok(indices) => Some(indices.load(&mut store, &instance)?), + Err(_) => None, + }, + }; + + Ok(TestSubject { + store, + exports, + recorder, + }) + } +} + +/// The test subject's exported interfaces. +/// +/// Accessing an interface the component does not export panics. +pub struct Exports { + name: String, + types: Option, + client: Option, + handler: Option, + http_client: Option, +} + +impl Exports { + /// The exported `wasi:http/types` interface. + pub fn wasi_http_types(&self) -> &types::Guest { + self.types + .as_ref() + .unwrap_or_else(|| panic!("{} does not export wasi:http/types", self.name)) + } + + /// The exported `wasi:http/client` interface. + pub fn wasi_http_client(&self) -> &client::Guest { + self.client + .as_ref() + .unwrap_or_else(|| panic!("{} does not export wasi:http/client", self.name)) + } + + /// The exported `wasi:http/handler` interface. + pub fn wasi_http_handler(&self) -> &handler::Guest { + self.handler + .as_ref() + .unwrap_or_else(|| panic!("{} does not export wasi:http/handler", self.name)) + } + + /// The exported `componentized:http/client` interface. + pub fn componentized_http_client(&self) -> &http_client::Guest { + self.http_client + .as_ref() + .unwrap_or_else(|| panic!("{} does not export componentized:http/client", self.name)) + } + + /// Whether the component exports `wasi:http/types`. + pub fn exports_types(&self) -> bool { + self.types.is_some() + } + + /// Whether the component exports `wasi:http/client`. + pub fn exports_client(&self) -> bool { + self.client.is_some() + } + + /// Whether the component exports `wasi:http/handler`. + pub fn exports_handler(&self) -> bool { + self.handler.is_some() + } + + /// Create a request with the exported `wasi:http/types`, without a body or trailers. + pub async fn new_request( + &self, + accessor: &Accessor, + method: Method, + scheme: Scheme, + authority: &str, + path_with_query: &str, + ) -> Result { + let types = self.wasi_http_types(); + let headers = types.fields().call_constructor(accessor).await?; + let trailers = ready(accessor, Ok(None))?; + let (request, _transmitted) = types + .request() + .call_new(accessor, headers, None, trailers, None) + .await?; + let request_methods = types.request(); + request_methods + .call_set_method(accessor, request, method) + .await? + .map_err(|()| format_err!("invalid method"))?; + request_methods + .call_set_scheme(accessor, request, Some(scheme)) + .await? + .map_err(|()| format_err!("invalid scheme"))?; + request_methods + .call_set_authority(accessor, request, Some(authority.to_string())) + .await? + .map_err(|()| format_err!("invalid authority"))?; + request_methods + .call_set_path_with_query(accessor, request, Some(path_with_query.to_string())) + .await? + .map_err(|()| format_err!("invalid path with query"))?; + Ok(request) + } +} + +/// An instantiated test subject. +pub struct TestSubject { + store: Store, + exports: Exports, + recorder: Recorder, +} + +impl TestSubject { + /// Logs and upstream requests recorded while the test subject runs. + pub fn recorder(&self) -> Recorder { + self.recorder.clone() + } + + /// The test subject's exported interfaces. + pub fn exports(&self) -> &Exports { + &self.exports + } + + /// Run a test body against the test subject's exports. + pub async fn run( + &mut self, + f: impl AsyncFnOnce(&Accessor, &Exports) -> Result + Send, + ) -> Result { + let exports = &self.exports; + self.store + .run_concurrent(async move |accessor| f(accessor, exports).await) + .await? + } +} diff --git a/crates/trace/src/client.rs b/crates/trace/src/client.rs new file mode 100644 index 0000000..d8038d3 --- /dev/null +++ b/crates/trace/src/client.rs @@ -0,0 +1,21 @@ +use crate::{ + Trace, + exports::wasi::http::{ + client::Guest, + types::{ErrorCode, Request, Response}, + }, + trace, + types::{TraceRequest, TraceResponse}, + wasi::http::client, +}; + +impl Guest for Trace { + #[doc = " This function may be used to either send an outgoing request over the"] + #[doc = " network or to forward it to another component."] + async fn send(request: Request) -> Result { + let request = request.into_inner::(); + trace!("OPERATION=wasi:http/client#send REQUEST={request}"); + let response = client::send(request.request).await?; + Ok(Response::new(TraceResponse::new(response))) + } +} diff --git a/crates/trace/src/handler.rs b/crates/trace/src/handler.rs new file mode 100644 index 0000000..5e85f30 --- /dev/null +++ b/crates/trace/src/handler.rs @@ -0,0 +1,21 @@ +use crate::{ + Trace, + exports::wasi::http::{ + handler::Guest, + types::{ErrorCode, Request, Response}, + }, + trace, + types::{TraceRequest, TraceResponse}, + wasi::http::handler, +}; + +impl Guest for Trace { + #[doc = " This function may be called with either an incoming request read from the"] + #[doc = " network or a request synthesized or forwarded by another component."] + async fn handle(request: Request) -> Result { + let request = request.into_inner::(); + trace!("OPERATION=wasi:http/handler#handle REQUEST={request}"); + let response = handler::handle(request.request).await?; + Ok(Response::new(TraceResponse::new(response))) + } +} diff --git a/crates/trace/src/lib.rs b/crates/trace/src/lib.rs new file mode 100644 index 0000000..db3b3b5 --- /dev/null +++ b/crates/trace/src/lib.rs @@ -0,0 +1,102 @@ +//! Virtualizes wasi:http interfaces, logging each call at the TRACE level. +//! +//! These sources are shared by the `trace`, `trace-client`, `trace-handler` and `trace-types` +//! components. Each component compiles them with the features for the interfaces it exports, +//! `wasi:http/types` is always exported: +//! +//! - `client`: exports `wasi:http/client` +//! - `handler`: exports `wasi:http/handler` + +use std::fmt::Display; + +mod types; + +#[cfg(feature = "client")] +mod client; + +#[cfg(feature = "handler")] +mod handler; + +#[macro_export] +macro_rules! trace { + ($dst:expr, $($arg:tt)*) => { + $crate::wasi::logging::logging::log( + $crate::wasi::logging::logging::Level::Trace, + "componentized-trace", + &format!($dst, $($arg)*), + ); + }; + ($dst:expr) => { + $crate::wasi::logging::logging::log( + $crate::wasi::logging::logging::Level::Trace, + "componentized-trace", + &format!($dst), + ); + }; +} + +/// The number of trailing hex digits of a resource handle to display +const HANDLE_DIGITS: u32 = 4; +const HANDLE_MODULUS: u32 = 16u32.pow(HANDLE_DIGITS); + +/// Write the trailing hex digits of a resource handle, padded with zeros and prefixed with `&`, +/// e.g. `&002a`. The prefix marks the value as a handle, a reference to a resource. +fn fmt_handle(f: &mut std::fmt::Formatter<'_>, handle: u32) -> std::fmt::Result { + write!( + f, + "&{:0width$x}", + handle % HANDLE_MODULUS, + width = HANDLE_DIGITS as usize + ) +} + +struct DisplayOption(Option); + +impl Display for DisplayOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self(Some(value)) => write!(f, "some<{value}>"), + Self(None) => f.write_str("none"), + } + } +} + +struct Trace; + +#[cfg(all(feature = "client", feature = "handler"))] +wit_bindgen::generate!({ + // `path` is relative to the manifest of the component compiling these sources, `components//` + path: "../wit", + world: "trace", + merge_structurally_equal_types: true, + generate_all, +}); + +#[cfg(all(feature = "client", not(feature = "handler")))] +wit_bindgen::generate!({ + // `path` is relative to the manifest of the component compiling these sources, `components//` + path: "../wit", + world: "trace-client", + merge_structurally_equal_types: true, + generate_all, +}); + +#[cfg(all(not(feature = "client"), feature = "handler"))] +wit_bindgen::generate!({ + // `path` is relative to the manifest of the component compiling these sources, `components//` + path: "../wit", + world: "trace-handler", + merge_structurally_equal_types: true, + generate_all, +}); + +#[cfg(all(not(feature = "client"), not(feature = "handler")))] +wit_bindgen::generate!({ + // `path` is relative to the manifest of the component compiling these sources, `components//` + path: "../wit", + world: "trace-types", + merge_structurally_equal_types: true, + generate_all, +}); + +export!(Trace); diff --git a/crates/trace/src/types.rs b/crates/trace/src/types.rs new file mode 100644 index 0000000..545d6af --- /dev/null +++ b/crates/trace/src/types.rs @@ -0,0 +1,620 @@ +use std::fmt::Display; + +use crate::{ + DisplayOption, Trace, + exports::wasi::http::types::{ + Duration, ErrorCode, FieldName, FieldValue, Fields, Guest, GuestFields, GuestRequest, + GuestRequestOptions, GuestResponse, HeaderError, Headers, Method, Request, RequestOptions, + RequestOptionsError, Response, Scheme, StatusCode, Trailers, + }, + fmt_handle, trace, + wasi::http::types, + wit_future, +}; + +impl Guest for Trace { + type Fields = TraceFields; + type Request = TraceRequest; + type RequestOptions = TraceRequestOptions; + type Response = TraceResponse; +} + +pub(crate) struct TraceFields { + fields: types::Fields, +} + +impl TraceFields { + fn new(fields: types::Fields) -> Self { + Self { fields } + } +} + +impl GuestFields for TraceFields { + #[doc = " Construct an empty HTTP Fields."] + #[doc = ""] + #[doc = " The resulting `fields` is mutable."] + #[allow(async_fn_in_trait)] + fn new() -> Self { + trace!("OPERATION=wasi:http/types#fields.new"); + Self::new(types::Fields::new()) + } + + #[doc = " Construct an HTTP Fields."] + #[doc = ""] + #[doc = " The resulting `fields` is mutable."] + #[doc = ""] + #[doc = " The list represents each name-value pair in the Fields. Names"] + #[doc = " which have multiple values are represented by multiple entries in this"] + #[doc = " list with the same name."] + #[doc = ""] + #[doc = " The tuple is a pair of the field name, represented as a string, and"] + #[doc = " Value, represented as a list of bytes. In a valid Fields, all names"] + #[doc = " and values are valid UTF-8 strings. However, values are not always"] + #[doc = " well-formed, so they are represented as a raw list of bytes."] + #[doc = ""] + #[doc = " An error result will be returned if any header or value was"] + #[doc = " syntactically invalid, if a header was forbidden, or if the"] + #[doc = " entries would exceed an implementation size limit."] + #[allow(async_fn_in_trait)] + fn from_list(entries: Vec<(FieldName, FieldValue)>) -> Result { + trace!( + "OPERATION=wasi:http/types#fields.from-list ENTRIES-LEN={}", + entries.len() + ); + match types::Fields::from_list(&entries) { + Ok(fields) => Ok(Fields::new(Self::new(fields))), + Err(err) => Err(err), + } + } + + #[doc = " Get all of the values corresponding to a name. If the name is not present"] + #[doc = " in this `fields`, an empty list is returned. However, if the name is"] + #[doc = " present but empty, this is represented by a list with one or more"] + #[doc = " empty field-values present."] + #[allow(async_fn_in_trait)] + fn get(&self, name: FieldName) -> Vec { + trace!("OPERATION=wasi:http/types#fields.get SELF={self} NAME={name}"); + self.fields.get(&name) + } + + #[doc = " Returns `true` when the name is present in this `fields`. If the name is"] + #[doc = " syntactically invalid, `false` is returned."] + #[allow(async_fn_in_trait)] + fn has(&self, name: FieldName) -> bool { + trace!("OPERATION=wasi:http/types#fields.has SELF={self} NAME={name}"); + self.fields.has(&name) + } + + #[doc = " Set all of the values for a name. Clears any existing values for that"] + #[doc = " name, if they have been set."] + #[doc = ""] + #[doc = " Fails with `header-error.immutable` if the `fields` are immutable."] + #[doc = ""] + #[doc = " Fails with `header-error.size-exceeded` if the name or values would"] + #[doc = " exceed an implementation-defined size limit."] + #[allow(async_fn_in_trait)] + fn set(&self, name: FieldName, value: Vec) -> Result<(), HeaderError> { + trace!( + "OPERATION=wasi:http/types#fields.set SELF={self} NAME={name} VALUE-LEN={}", + value.len() + ); + self.fields.set(&name, &value) + } + + #[doc = " Delete all values for a name. Does nothing if no values for the name"] + #[doc = " exist."] + #[doc = ""] + #[doc = " Fails with `header-error.immutable` if the `fields` are immutable."] + #[allow(async_fn_in_trait)] + fn delete(&self, name: FieldName) -> Result<(), HeaderError> { + trace!("OPERATION=wasi:http/types#fields.delete SELF={self} NAME={name}"); + self.fields.delete(&name) + } + + #[doc = " Delete all values for a name. Does nothing if no values for the name"] + #[doc = " exist."] + #[doc = ""] + #[doc = " Returns all values previously corresponding to the name, if any."] + #[doc = ""] + #[doc = " Fails with `header-error.immutable` if the `fields` are immutable."] + #[allow(async_fn_in_trait)] + fn get_and_delete(&self, name: FieldName) -> Result, HeaderError> { + trace!("OPERATION=wasi:http/types#fields.get-and-delete SELF={self} NAME={name}"); + self.fields.get_and_delete(&name) + } + + #[doc = " Append a value for a name. Does not change or delete any existing"] + #[doc = " values for that name."] + #[doc = ""] + #[doc = " Fails with `header-error.immutable` if the `fields` are immutable."] + #[doc = ""] + #[doc = " Fails with `header-error.size-exceeded` if the value would exceed"] + #[doc = " an implementation-defined size limit."] + #[allow(async_fn_in_trait)] + fn append(&self, name: FieldName, value: FieldValue) -> Result<(), HeaderError> { + trace!( + "OPERATION=wasi:http/types#fields.append SELF={self} NAME={name} VALUE={value}", + value = DisplayFieldValue(value.clone()) + ); + self.fields.append(&name, &value) + } + + #[doc = " Retrieve the full set of names and values in the Fields. Like the"] + #[doc = " constructor, the list represents each name-value pair."] + #[doc = ""] + #[doc = " The outer list represents each name-value pair in the Fields. Names"] + #[doc = " which have multiple values are represented by multiple entries in this"] + #[doc = " list with the same name."] + #[doc = ""] + #[doc = " The names and values are always returned in the original casing and in"] + #[doc = " the order in which they will be serialized for transport."] + #[allow(async_fn_in_trait)] + fn copy_all(&self) -> Vec<(FieldName, FieldValue)> { + trace!("OPERATION=wasi:http/types#fields.copy-all SELF={self}"); + self.fields.copy_all() + } + + #[doc = " Make a deep copy of the Fields. Equivalent in behavior to calling the"] + #[doc = " `fields` constructor on the return value of `copy-all`. The resulting"] + #[doc = " `fields` is mutable."] + #[allow(async_fn_in_trait)] + fn clone(&self) -> Fields { + trace!("OPERATION=wasi:http/types#fields.clone SELF={self}"); + Fields::new(Self::new(self.fields.clone())) + } +} + +impl Display for TraceFields { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt_handle(f, self.fields.handle()) + } +} + +struct DisplayFieldValue(FieldValue); + +impl Display for DisplayFieldValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(value) = self; + f.write_str(&String::from_utf8_lossy(value)) + } +} + +pub(crate) struct TraceRequest { + pub(crate) request: types::Request, +} + +impl TraceRequest { + pub(crate) fn new(request: types::Request) -> Self { + Self { request } + } +} + +impl GuestRequest for TraceRequest { + #[doc = " Construct a new `request` with a default `method` of `GET`, and"] + #[doc = " `none` values for `path-with-query`, `scheme`, and `authority`."] + #[doc = ""] + #[doc = " `headers` is the HTTP Headers for the Request."] + #[doc = ""] + #[doc = " `contents` is the optional body content stream with `none`"] + #[doc = " representing a zero-length content stream."] + #[doc = " Once it is closed, `trailers` future must resolve to a result."] + #[doc = " If `trailers` resolves to an error, underlying connection"] + #[doc = " will be closed immediately."] + #[doc = ""] + #[doc = " `options` is optional `request-options` resource to be used"] + #[doc = " if the request is sent over a network connection."] + #[doc = ""] + #[doc = " It is possible to construct, or manipulate with the accessor functions"] + #[doc = " below, a `request` with an invalid combination of `scheme`"] + #[doc = " and `authority`, or `headers` which are not permitted to be sent."] + #[doc = " It is the obligation of the `handler.handle` implementation"] + #[doc = " to reject invalid constructions of `request`."] + #[doc = ""] + #[doc = " The returned future resolves to result of transmission of this request."] + #[allow(async_fn_in_trait)] + fn new( + headers: Headers, + contents: Option>, + trailers: wit_bindgen::FutureReader, ErrorCode>>, + options: Option, + ) -> (Request, wit_bindgen::FutureReader>) { + trace!("OPERATION=wasi:http/types#request.new"); + let headers = headers.into_inner::().fields; + let (trailers_tx, trailers_rx) = wit_future::new(|| Ok(None)); + wit_bindgen::spawn_local(async move { + let trailers = match trailers.await { + Ok(None) => Ok(None), + Ok(Some(trailers)) => Ok(Some(trailers.into_inner::().fields)), + Err(err) => Err(err), + }; + let _ = trailers_tx.write(trailers).await; + }); + let options = options.map(|options| options.into_inner::().options); + let (request, read) = types::Request::new(headers, contents, trailers_rx, options); + (Request::new(Self::new(request)), read) + } + + #[doc = " Get the Method for the Request."] + #[allow(async_fn_in_trait)] + fn get_method(&self) -> Method { + trace!("OPERATION=wasi:http/types#request.get-method SELF={self}"); + self.request.get_method() + } + + #[doc = " Set the Method for the Request. Fails if the string present in a"] + #[doc = " `method.other` argument is not a syntactically valid method."] + #[allow(async_fn_in_trait)] + fn set_method(&self, method: Method) -> Result<(), ()> { + trace!("OPERATION=wasi:http/types#request.set-method SELF={self} METHOD={method}"); + self.request.set_method(&method) + } + + #[doc = " Get the combination of the HTTP Path and Query for the Request. When"] + #[doc = " `none`, this represents an empty Path and empty Query."] + #[allow(async_fn_in_trait)] + fn get_path_with_query(&self) -> Option { + trace!("OPERATION=wasi:http/types#request.get-path-with-query SELF={self}"); + self.request.get_path_with_query() + } + + #[doc = " Set the combination of the HTTP Path and Query for the Request. When"] + #[doc = " `none`, this represents an empty Path and empty Query. Fails is the"] + #[doc = " string given is not a syntactically valid path and query uri component."] + #[allow(async_fn_in_trait)] + fn set_path_with_query(&self, path_with_query: Option) -> Result<(), ()> { + trace!( + "OPERATION=wasi:http/types#request.set-path-with-query SELF={self} PATH-WITH-QUERY={path_with_query}", + path_with_query = DisplayOption(path_with_query.clone()) + ); + self.request.set_path_with_query(path_with_query.as_deref()) + } + + #[doc = " Get the HTTP Related Scheme for the Request. When `none`, the"] + #[doc = " implementation may choose an appropriate default scheme."] + #[allow(async_fn_in_trait)] + fn get_scheme(&self) -> Option { + trace!("OPERATION=wasi:http/types#request.get-scheme SELF={self}"); + self.request.get_scheme() + } + + #[doc = " Set the HTTP Related Scheme for the Request. When `none`, the"] + #[doc = " implementation may choose an appropriate default scheme. Fails if the"] + #[doc = " string given is not a syntactically valid uri scheme."] + #[allow(async_fn_in_trait)] + fn set_scheme(&self, scheme: Option) -> Result<(), ()> { + trace!( + "OPERATION=wasi:http/types#request.set-scheme SELF={self} SCHEME={scheme}", + scheme = DisplayOption(scheme.clone()) + ); + self.request.set_scheme(scheme.as_ref()) + } + + #[doc = " Get the authority of the Request\'s target URI. A value of `none` may be used"] + #[doc = " with Related Schemes which do not require an authority. The HTTP and"] + #[doc = " HTTPS schemes always require an authority."] + #[allow(async_fn_in_trait)] + fn get_authority(&self) -> Option { + trace!("OPERATION=wasi:http/types#request.get-authority SELF={self}"); + self.request.get_authority() + } + + #[doc = " Set the authority of the Request\'s target URI. A value of `none` may be used"] + #[doc = " with Related Schemes which do not require an authority. The HTTP and"] + #[doc = " HTTPS schemes always require an authority. Fails if the string given is"] + #[doc = " not a syntactically valid URI authority."] + #[allow(async_fn_in_trait)] + fn set_authority(&self, authority: Option) -> Result<(), ()> { + trace!( + "OPERATION=wasi:http/types#request.set-authority SELF={self} AUTHORITY={authority}", + authority = DisplayOption(authority.clone()) + ); + self.request.set_authority(authority.as_deref()) + } + + #[doc = " Get the `request-options` to be associated with this request"] + #[doc = ""] + #[doc = " The returned `request-options` resource is immutable: `set-*` operations"] + #[doc = " will fail if invoked."] + #[doc = ""] + #[doc = " This `request-options` resource is a child: it must be dropped before"] + #[doc = " the parent `request` is dropped, or its ownership is transferred to"] + #[doc = " another component by e.g. `handler.handle`."] + #[allow(async_fn_in_trait)] + fn get_options(&self) -> Option { + trace!("OPERATION=wasi:http/types#request.get-options SELF={self}"); + self.request + .get_options() + .map(|options| RequestOptions::new(TraceRequestOptions::new(options))) + } + + #[doc = " Get the headers associated with the Request."] + #[doc = ""] + #[doc = " The returned `headers` resource is immutable: `set`, `append`, and"] + #[doc = " `delete` operations will fail with `header-error.immutable`."] + #[allow(async_fn_in_trait)] + fn get_headers(&self) -> Headers { + trace!("OPERATION=wasi:http/types#request.get-headers SELF={self}"); + Headers::new(TraceFields::new(self.request.get_headers())) + } + + #[doc = " Get body of the Request."] + #[doc = ""] + #[doc = " Stream returned by this method represents the contents of the body."] + #[doc = " Once the stream is reported as closed, callers should await the returned"] + #[doc = " future to determine whether the body was received successfully."] + #[doc = " The future will only resolve after the stream is reported as closed."] + #[doc = ""] + #[doc = " This function takes a `res` future as a parameter, which can be used to"] + #[doc = " communicate an error in handling of the request."] + #[doc = ""] + #[doc = " Note that function will move the `request`, but references to headers or"] + #[doc = " request options acquired from it previously will remain valid."] + #[allow(async_fn_in_trait)] + fn consume_body( + this: Request, + res: wit_bindgen::FutureReader>, + ) -> ( + wit_bindgen::StreamReader, + wit_bindgen::FutureReader, ErrorCode>>, + ) { + let this = this.into_inner::(); + trace!("OPERATION=wasi:http/types#request.consume-body THIS={this}"); + let request = this.request; + let (contents, trailers) = types::Request::consume_body(request, res); + let (trailers_tx, trailers_rx) = wit_future::new(|| Ok(None)); + wit_bindgen::spawn_local(async move { + let trailers = match trailers.await { + Ok(None) => Ok(None), + Ok(Some(trailers)) => Ok(Some(Trailers::new(TraceFields::new(trailers)))), + Err(err) => Err(err), + }; + let _ = trailers_tx.write(trailers).await; + }); + (contents, trailers_rx) + } +} + +impl Display for TraceRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt_handle(f, self.request.handle()) + } +} + +impl Display for Method { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + types::Method::Get => f.write_str("get"), + types::Method::Head => f.write_str("head"), + types::Method::Post => f.write_str("post"), + types::Method::Put => f.write_str("put"), + types::Method::Delete => f.write_str("delete"), + types::Method::Connect => f.write_str("connect"), + types::Method::Options => f.write_str("options"), + types::Method::Trace => f.write_str("trace"), + types::Method::Patch => f.write_str("patch"), + types::Method::Other(method) => f.write_str(&method.to_ascii_lowercase()), + } + } +} + +impl Display for Scheme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + types::Scheme::Http => f.write_str("http"), + types::Scheme::Https => f.write_str("https"), + types::Scheme::Other(scheme) => write!(f, "other<{scheme}>"), + } + } +} + +pub(crate) struct TraceRequestOptions { + options: types::RequestOptions, +} + +impl TraceRequestOptions { + fn new(options: types::RequestOptions) -> Self { + Self { options } + } +} + +impl GuestRequestOptions for TraceRequestOptions { + #[doc = " Construct a default `request-options` value."] + #[allow(async_fn_in_trait)] + fn new() -> Self { + trace!("OPERATION=wasi:http/types#request-options.new"); + Self::new(types::RequestOptions::new()) + } + + #[doc = " The timeout for the initial connect to the HTTP Server."] + #[allow(async_fn_in_trait)] + fn get_connect_timeout(&self) -> Option { + trace!("OPERATION=wasi:http/types#request-options.get-connect-timeout SELF={self}"); + self.options.get_connect_timeout() + } + + #[doc = " Set the timeout for the initial connect to the HTTP Server. An error"] + #[doc = " return value indicates that this timeout is not supported or that this"] + #[doc = " handle is immutable."] + #[allow(async_fn_in_trait)] + fn set_connect_timeout(&self, duration: Option) -> Result<(), RequestOptionsError> { + trace!( + "OPERATION=wasi:http/types#request-options.set-connect-timeout SELF={self} DURATION={duration}", + duration = DisplayOption(duration) + ); + self.options.set_connect_timeout(duration) + } + + #[doc = " The timeout for receiving the first byte of the Response body."] + #[allow(async_fn_in_trait)] + fn get_first_byte_timeout(&self) -> Option { + trace!("OPERATION=wasi:http/types#request-options.get-first-byte-timeout SELF={self}"); + self.options.get_first_byte_timeout() + } + + #[doc = " Set the timeout for receiving the first byte of the Response body. An"] + #[doc = " error return value indicates that this timeout is not supported or that"] + #[doc = " this handle is immutable."] + #[allow(async_fn_in_trait)] + fn set_first_byte_timeout( + &self, + duration: Option, + ) -> Result<(), RequestOptionsError> { + trace!( + "OPERATION=wasi:http/types#request-options.set-first-byte-timeout SELF={self} DURATION={duration}", + duration = DisplayOption(duration) + ); + self.options.set_first_byte_timeout(duration) + } + + #[doc = " The timeout for receiving subsequent chunks of bytes in the Response"] + #[doc = " body stream."] + #[allow(async_fn_in_trait)] + fn get_between_bytes_timeout(&self) -> Option { + trace!("OPERATION=wasi:http/types#request-options.get-between-bytes-timeout SELF={self}"); + self.options.get_between_bytes_timeout() + } + + #[doc = " Set the timeout for receiving subsequent chunks of bytes in the Response"] + #[doc = " body stream. An error return value indicates that this timeout is not"] + #[doc = " supported or that this handle is immutable."] + #[allow(async_fn_in_trait)] + fn set_between_bytes_timeout( + &self, + duration: Option, + ) -> Result<(), RequestOptionsError> { + trace!( + "OPERATION=wasi:http/types#request-options.set-between-bytes-timeout SELF={self} DURATION={duration}", + duration = DisplayOption(duration) + ); + self.options.set_between_bytes_timeout(duration) + } + + #[doc = " Make a deep copy of the `request-options`."] + #[doc = " The resulting `request-options` is mutable."] + #[allow(async_fn_in_trait)] + fn clone(&self) -> RequestOptions { + trace!("OPERATION=wasi:http/types#request-options.clone SELF={self}"); + RequestOptions::new(Self::new(self.options.clone())) + } +} + +impl Display for TraceRequestOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt_handle(f, self.options.handle()) + } +} + +pub(crate) struct TraceResponse { + response: types::Response, +} + +impl TraceResponse { + pub(crate) fn new(response: types::Response) -> Self { + Self { response } + } +} + +impl GuestResponse for TraceResponse { + #[doc = " Construct a new `response`, with a default `status-code` of `200`."] + #[doc = " If a different `status-code` is needed, it must be set via the"] + #[doc = " `set-status-code` method."] + #[doc = ""] + #[doc = " `headers` is the HTTP Headers for the Response."] + #[doc = ""] + #[doc = " `contents` is the optional body content stream with `none`"] + #[doc = " representing a zero-length content stream."] + #[doc = " Once it is closed, `trailers` future must resolve to a result."] + #[doc = " If `trailers` resolves to an error, underlying connection"] + #[doc = " will be closed immediately."] + #[doc = ""] + #[doc = " The returned future resolves to result of transmission of this response."] + #[allow(async_fn_in_trait)] + fn new( + headers: Headers, + contents: Option>, + trailers: wit_bindgen::FutureReader, ErrorCode>>, + ) -> (Response, wit_bindgen::FutureReader>) { + trace!("OPERATION=wasi:http/types#response.new"); + let headers = headers.into_inner::().fields; + let (trailers_tx, trailers_rx) = wit_future::new(|| Ok(None)); + wit_bindgen::spawn_local(async move { + let trailers = match trailers.await { + Ok(None) => Ok(None), + Ok(Some(trailers)) => Ok(Some(trailers.into_inner::().fields)), + Err(err) => Err(err), + }; + let _ = trailers_tx.write(trailers).await; + }); + let (response, read) = types::Response::new(headers, contents, trailers_rx); + (Response::new(Self::new(response)), read) + } + + #[doc = " Get the HTTP Status Code for the Response."] + #[allow(async_fn_in_trait)] + fn get_status_code(&self) -> StatusCode { + trace!("OPERATION=wasi:http/types#response.get-status-code SELF={self}"); + self.response.get_status_code() + } + + #[doc = " Set the HTTP Status Code for the Response. Fails if the status-code"] + #[doc = " given is not a valid http status code."] + #[allow(async_fn_in_trait)] + fn set_status_code(&self, status_code: StatusCode) -> Result<(), ()> { + trace!( + "OPERATION=wasi:http/types#response.set-status-code SELF={self} STATUS-CODE={status_code}" + ); + self.response.set_status_code(status_code) + } + + #[doc = " Get the headers associated with the Response."] + #[doc = ""] + #[doc = " The returned `headers` resource is immutable: `set`, `append`, and"] + #[doc = " `delete` operations will fail with `header-error.immutable`."] + #[allow(async_fn_in_trait)] + fn get_headers(&self) -> Headers { + trace!("OPERATION=wasi:http/types#response.get-headers SELF={self}"); + Headers::new(TraceFields::new(self.response.get_headers())) + } + + #[doc = " Get body of the Response."] + #[doc = ""] + #[doc = " Stream returned by this method represents the contents of the body."] + #[doc = " Once the stream is reported as closed, callers should await the returned"] + #[doc = " future to determine whether the body was received successfully."] + #[doc = " The future will only resolve after the stream is reported as closed."] + #[doc = ""] + #[doc = " This function takes a `res` future as a parameter, which can be used to"] + #[doc = " communicate an error in handling of the response."] + #[doc = ""] + #[doc = " Note that function will move the `response`, but references to headers"] + #[doc = " acquired from it previously will remain valid."] + #[allow(async_fn_in_trait)] + fn consume_body( + this: Response, + res: wit_bindgen::FutureReader>, + ) -> ( + wit_bindgen::StreamReader, + wit_bindgen::FutureReader, ErrorCode>>, + ) { + let this = this.into_inner::(); + trace!("OPERATION=wasi:http/types#response.consume-body THIS={this}"); + let response = this.response; + let (contents, trailers) = types::Response::consume_body(response, res); + let (trailers_tx, trailers_rx) = wit_future::new(|| Ok(None)); + wit_bindgen::spawn_local(async move { + let trailers = match trailers.await { + Ok(None) => Ok(None), + Ok(Some(trailers)) => Ok(Some(Trailers::new(TraceFields::new(trailers)))), + Err(err) => Err(err), + }; + let _ = trailers_tx.write(trailers).await; + }); + (contents, trailers_rx) + } +} + +impl Display for TraceResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt_handle(f, self.response.handle()) + } +}