diff --git a/README.md b/README.md index a232068..2ba0475 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,10 @@ request-body shapes fail generation. Generated clients leave OpenTelemetry dependencies and SDK setup to the application. Pass an instrumented transport to a Go client or an instrumented -`fetch` implementation to a TypeScript client. These examples assume the -application has initialized an OpenTelemetry SDK; `@opentelemetry/api` alone -uses no-op tracing and propagation implementations. +`fetch` implementation to a TypeScript client. Rust clients accept a +middleware-enabled HTTP client at construction. +These examples assume the application has initialized an OpenTelemetry SDK; +`@opentelemetry/api` alone uses no-op tracing and propagation implementations. ### Go @@ -141,6 +142,36 @@ Context propagator must be registered for `propagation.inject` to add origins, the API's CORS policy must also allow the propagation headers configured by the application, commonly `traceparent`, `tracestate`, and `baggage`. +### Rust + +The insertion point is the **HTTP client passed to `api::Client::new`**. Configure +[`reqwest-tracing`](https://docs.rs/reqwest-tracing/0.5.8/reqwest_tracing/) +middleware once; every generated operation's `.send()` then propagates the active +trace automatically: + +```rust +let http = reqwest_middleware::ClientBuilder::new(reqwest::Client::builder().build()?) + .with(reqwest_tracing::TracingMiddleware::default()) + .build(); + +let api = api::Client::new(http, "https://api.example.com".into(), None); + +// Inside the application's existing tracing span: +let response = api.create_thing(params).send().await?; +let result = api::CreateThingResponse::decode(response).await?; +``` + +For the generated Reqwest 0.12 client, use `reqwest-middleware` 0.4.2 with its +`json` feature and `reqwest-tracing` 0.5.8 with its `opentelemetry_0_30` feature. +The application must already have OpenTelemetry 0.30, a `tracing-opentelemetry` +0.31 subscriber layer, and a registered W3C `TraceContextPropagator`. + +The middleware reads the current `tracing` span when the request is sent, creates +a child HTTP span, and injects its `traceparent` and `tracestate` headers. +One shared client can therefore serve calls from different traces. Normal async +context propagation still applies: instrument spawned tasks with +[`.in_current_span()`](https://docs.rs/tracing/latest/tracing/trait.Instrument.html#method.in_current_span). + ## Develop [Go](https://go.dev) builds the generator and runs its tests. @@ -173,15 +204,19 @@ reports are attached to the workflow, including on failure. ## Rust clients Use `--mode client --lang rust --out src/api`, then `mod api;`. Add `serde` 1 -(with `derive`), `serde_json` 1, and `reqwest` 0.12 (with `json`) to Cargo dependencies. +(with `derive`), `serde_json` 1, `reqwest` 0.12, and `reqwest-middleware` 0.4.2 +(with `json`) to Cargo dependencies. Choose the Reqwest TLS features appropriate to your application. Construct `api::Client::new(http, base_url, bearer_token)` with your configured -Reqwest client. Operation methods return request builders, so callers own timeouts, +`reqwest_middleware::ClientWithMiddleware`. For a client without middleware, +construct it with `reqwest_middleware::ClientBuilder::new(http).build()`. +Operation methods return `reqwest_middleware::RequestBuilder`, so callers own timeouts, cancellation, tracing, and bounded body reads. Models and operation-specific `Response::decode` enums preserve declared HTTP statuses; undeclared statuses retain their original response. SSE responses remain streaming Reqwest responses, leaving event framing and cancellation to the caller. +See [Rust trace propagation](#rust) to configure automatic propagation once. Rust supports JSON and raw request bodies, optional bodies, scalar and repeated query parameters, headers, escaped path parameters, enums, nullable values and diff --git a/internal/rustemit/rustemit.go b/internal/rustemit/rustemit.go index 5e3e351..7cef8a7 100644 --- a/internal/rustemit/rustemit.go +++ b/internal/rustemit/rustemit.go @@ -215,7 +215,7 @@ func (e *emitter) operation(op clientgen.Operation) string { if hasParams { fmt.Fprintf(&body, ", params: %sParams", name) } - body.WriteString(") -> reqwest::RequestBuilder {\n") + body.WriteString(") -> reqwest_middleware::RequestBuilder {\n") fmt.Fprintf(&body, " let path = %s.to_owned();\n", strconv.Quote(op.Route.Path)) for _, param := range op.Route.Operation.Parameters { typ := e.rustType(param.Schema, name+typeName(param.Name)) @@ -324,7 +324,7 @@ func (e *emitter) response(op clientgen.Operation, name string) { const clientPrelude = ` #[derive(Clone, Debug)] pub struct Client { - http: reqwest::Client, + http: reqwest_middleware::ClientWithMiddleware, base_url: String, bearer_token: Option, } @@ -336,7 +336,8 @@ fn encode_path(value: &str) -> String { }).collect() } impl Client { - pub fn new(http: reqwest::Client, base_url: String, bearer_token: Option) -> Self { + /// Every operation uses this client's middleware when its request is sent. + pub fn new(http: reqwest_middleware::ClientWithMiddleware, base_url: String, bearer_token: Option) -> Self { Self { http, base_url: base_url.trim_end_matches('/').to_owned(), bearer_token } } ` diff --git a/testdata/rust-client/Cargo.lock b/testdata/rust-client/Cargo.lock index ed2f137..193fe0a 100644 --- a/testdata/rust-client/Cargo.lock +++ b/testdata/rust-client/Cargo.lock @@ -2,6 +2,42 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -38,6 +74,24 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "displaydoc" version = "0.2.7" @@ -46,9 +100,21 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -58,6 +124,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.34" @@ -65,6 +146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -73,6 +155,40 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + [[package]] name = "futures-task" version = "0.3.34" @@ -85,12 +201,73 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "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.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + [[package]] name = "http" version = "1.5.0" @@ -130,6 +307,12 @@ 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" @@ -140,9 +323,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -277,6 +462,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "ipnet" version = "2.12.2" @@ -300,6 +495,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.189" @@ -318,6 +519,12 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "matchit" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9" + [[package]] name = "memchr" version = "2.8.3" @@ -335,15 +542,43 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "oasmith-rust-contract-tests" version = "0.0.0" dependencies = [ "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry_sdk", "reqwest", + "reqwest-middleware", + "reqwest-tracing", "serde", "serde_json", "tokio", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "wiremock", ] [[package]] @@ -352,6 +587,50 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.21", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror 2.0.21", + "tokio", + "tokio-stream", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -373,6 +652,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -391,6 +679,70 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "reqwest" version = "0.12.28" @@ -423,6 +775,39 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "reqwest-tracing" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70ea85f131b2ee9874f0b160ac5976f8af75f3c9badfe0d955880257d10bd83" +dependencies = [ + "anyhow", + "async-trait", + "getrandom 0.2.17", + "http", + "matchit", + "opentelemetry", + "reqwest", + "reqwest-middleware", + "tracing", + "tracing-opentelemetry", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -462,7 +847,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] @@ -490,6 +875,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "slab" version = "0.4.12" @@ -518,6 +912,17 @@ 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.6" @@ -546,7 +951,56 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", +] + +[[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.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", ] [[package]] @@ -565,6 +1019,7 @@ version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", @@ -581,7 +1036,32 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[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]] @@ -636,9 +1116,21 @@ 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" @@ -646,6 +1138,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -678,6 +1214,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "want" version = "0.3.1" @@ -693,6 +1235,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.128" @@ -735,7 +1286,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 3.0.6", "wasm-bindgen-shared", ] @@ -758,6 +1309,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -773,6 +1334,35 @@ dependencies = [ "windows-link", ] +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.4" @@ -798,10 +1388,30 @@ checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -819,7 +1429,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", "synstructure", ] @@ -853,7 +1463,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] diff --git a/testdata/rust-client/Cargo.toml b/testdata/rust-client/Cargo.toml index 9e4da3a..106a482 100644 --- a/testdata/rust-client/Cargo.toml +++ b/testdata/rust-client/Cargo.toml @@ -7,5 +7,16 @@ publish = false serde = { version = "1", features = ["derive"] } serde_json = "1" reqwest = { version = "0.12", default-features = false, features = ["json"] } -tokio = { version = "1", features = ["macros", "rt"] } +reqwest-middleware = { version = "0.4.2", features = ["json"] } +tokio = { version = "1", features = ["macros", "rt", "time"] } http = "1" + +[dev-dependencies] +opentelemetry = "0.30" +opentelemetry_sdk = { version = "0.30", features = ["testing"] } +opentelemetry-http = "0.30" +reqwest-tracing = { version = "0.5.8", features = ["opentelemetry_0_30"] } +tracing = "0.1" +tracing-opentelemetry = "0.31" +tracing-subscriber = "0.3" +wiremock = "0.6" diff --git a/testdata/rust-client/src/lib.rs b/testdata/rust-client/src/lib.rs index 4494e20..d19d68c 100644 --- a/testdata/rust-client/src/lib.rs +++ b/testdata/rust-client/src/lib.rs @@ -5,7 +5,8 @@ mod models; mod tests { use super::{public::*, models}; fn client() -> Client { - Client::new(reqwest::Client::new(), "http://localhost:1234/".into(), Some("test-key".into())) + let http = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()).build(); + Client::new(http, "http://localhost:1234/".into(), Some("test-key".into())) } #[test] fn encodes_path_repeated_query_headers_and_json() { @@ -68,4 +69,81 @@ mod tests { assert!(serde_json::from_value::(serde_json::json!({"status":"completed","thing":{"id":"a","name":"b"}})).is_ok()); assert!(serde_json::from_value::(serde_json::json!({"status":"unknown","message":"no"})).is_err()); } + + #[tokio::test] + async fn configured_client_propagates_the_context_active_at_send() { + use opentelemetry::{global, Context}; + use opentelemetry::trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState, TracerProvider}; + use opentelemetry_sdk::{propagation::TraceContextPropagator, trace::{InMemorySpanExporter, SdkTracerProvider}}; + use tracing::{Instrument, instrument::WithSubscriber}; + use tracing_opentelemetry::OpenTelemetrySpanExt; + use tracing_subscriber::prelude::*; + use wiremock::{Mock, MockServer, ResponseTemplate, matchers::{method, path, header, body_json}}; + + // This is the only test using the process-wide propagator. The provider, + // subscriber, HTTP server, and requests all belong to this test. + global::set_text_map_propagator(TraceContextPropagator::new()); + let exporter = InMemorySpanExporter::default(); + let provider = SdkTracerProvider::builder().with_simple_exporter(exporter.clone()).build(); + let subscriber = tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("contract-test"))); + + let work = async { + let server = MockServer::start().await; + Mock::given(method("POST")).and(path("/things/a%2Fb")) + .and(header("authorization", "Bearer test-key")) + .and(header("x-request-id", "request-one")) + .and(body_json(serde_json::json!({"name":"Podcast"}))) + .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({"message":"invalid"}))) + .expect(1).mount(&server).await; + let event = "event: progress\ndata: {}\n\n"; + Mock::given(method("GET")).and(path("/events")) + .and(header("accept", "text/event-stream")) + .and(header("authorization", "Bearer test-key")) + .respond_with(ResponseTemplate::new(200).set_body_string(event)) + .expect(1).mount(&server).await; + + let http = reqwest_middleware::ClientBuilder::new(reqwest::Client::builder().no_proxy().build().unwrap()) + .with(reqwest_tracing::TracingMiddleware::default()).build(); + let api = Client::new(http, server.uri(), Some("test-key".into())); + // Build before entering either span: injection must happen at send, + // including through a clone of the shared generated client. + let post = api.create_thing(CreateThingParams { + thing_id: "a/b".into(), tag: None, notify: false, label: None, + x_request_id: "request-one".into(), body: CreateThing { name: "Podcast".into() }, + }); + let events = api.clone().watch_events(); + let first = tracing::info_span!("first operation"); + let second = tracing::info_span!("second operation"); + for (span, trace_id, span_id) in [(&first, 1u128, 2u64), (&second, 3u128, 4u64)] { + span.set_parent(Context::new().with_remote_span_context(SpanContext::new( + TraceId::from(trace_id), SpanId::from(span_id), TraceFlags::SAMPLED, true, + TraceState::from_key_value([("acme", "state")]).unwrap(), + ))); + } + let parents = [first.context().span().span_context().clone(), second.context().span().span_context().clone()]; + let (post, events) = tokio::join!(post.send().instrument(first), events.send().instrument(second)); + assert!(matches!(CreateThingResponse::decode(post.unwrap()).await.unwrap(), CreateThingResponse::Status400(_))); + let WatchEventsResponse::Status200(stream) = WatchEventsResponse::decode(events.unwrap()).await.unwrap() else { panic!("wrong event variant") }; + assert_eq!(stream.text().await.unwrap(), event); + server.verify().await; + + let requests = server.received_requests().await.unwrap(); + let spans = exporter.get_finished_spans().unwrap(); + for (route, parent) in [("/things/a%2Fb", &parents[0]), ("/events", &parents[1])] { + let request = requests.iter().find(|r| r.url.path() == route).unwrap(); + let cx = global::get_text_map_propagator(|p| p.extract(&opentelemetry_http::HeaderExtractor(&request.headers))); + let propagated = cx.span().span_context().clone(); + assert!(propagated.is_valid()); + assert_eq!(propagated.trace_id(), parent.trace_id()); + assert_ne!(propagated.span_id(), parent.span_id()); + assert_eq!(request.headers["tracestate"], "acme=state"); + let http_span = spans.iter().find(|s| s.span_context.span_id() == propagated.span_id()).unwrap(); + assert_eq!(http_span.span_kind, opentelemetry::trace::SpanKind::Client); + assert_eq!(http_span.parent_span_id, parent.span_id()); + } + }.with_subscriber(subscriber); + tokio::time::timeout(std::time::Duration::from_secs(5), work).await.expect("trace propagation did not complete"); + provider.shutdown().unwrap(); + } }