From bbd523d98d5f3267bb41d28eab2b76dbd7fb5bf8 Mon Sep 17 00:00:00 2001 From: adelnizamutdinov Date: Sun, 27 Sep 2026 05:46:43 +0300 Subject: [PATCH 1/2] Document OpenTelemetry propagation for Rust clients --- README.md | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a232068..a6badd7 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 expose Reqwest request +builders for context injection or execution through application-owned middleware. +These examples assume the application has initialized an OpenTelemetry SDK; +`@opentelemetry/api` alone uses no-op tracing and propagation implementations. ### Go @@ -141,6 +142,169 @@ 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 + +Generated operations return a normal +[`reqwest::RequestBuilder`](https://docs.rs/reqwest/0.12.28/reqwest/struct.RequestBuilder.html). +They prepare the URL, authentication, parameters, and body; the caller chooses +when and how to send the request. Plain Reqwest does not inject OpenTelemetry +headers automatically. Both approaches below work with the generated client +without changing generated code. + +Rust has no standard equivalent of Go's `context.Context` that combines tracing, +deadlines, and cancellation, nor a standard global `fetch` hook. The +[`opentelemetry::Context`](https://docs.rs/opentelemetry/0.30.0/opentelemetry/context/struct.Context.html) +type carries telemetry context. Pass it explicitly, or use async instrumentation +to make a span current while a future is polled. Timeouts and cancellation remain +separate: use Reqwest's `.timeout(...)` and your async runtime's cancellation +mechanism. An OpenTelemetry context does not cancel HTTP requests. +The standard library's `std::task::Context` is an executor polling interface, +unrelated to request or trace context. + +#### Propagate an explicit context + +In addition to the [Rust client dependencies](#rust-clients), this example uses: + +```toml +opentelemetry = "0.30" +opentelemetry_sdk = "0.30" +opentelemetry-http = "0.30" +``` + +Register the application's propagator once at startup: + +```rust +opentelemetry::global::set_text_map_propagator( + opentelemetry_sdk::propagation::TraceContextPropagator::new(), +); +``` + +This configures W3C `traceparent` and `tracestate` propagation. To propagate +baggage too, register a `TextMapCompositePropagator` containing both +`TraceContextPropagator` and `BaggagePropagator`. Propagator setup is separate from +initializing the application's tracing SDK and exporter. + +Keep a send helper in the application. Reqwest's `build_split()` returns the +original HTTP client along with the built request, preserving its connection +pool and configuration: + +```rust +use opentelemetry::{global, Context}; +use opentelemetry_http::HeaderInjector; + +async fn send_with_context( + builder: reqwest::RequestBuilder, + cx: &Context, +) -> Result { + let (http, request) = builder.build_split(); + let mut request = request?; + global::get_text_map_propagator(|propagator| { + propagator.inject_context(cx, &mut HeaderInjector(request.headers_mut())); + }); + http.execute(request).await +} + +async fn create_thing( + api: &api::Client, + params: api::CreateThingParams, + cx: &Context, +) -> Result { + let response = send_with_context(api.create_thing(params), cx).await?; + api::CreateThingResponse::decode(response).await +} +``` + +Here `api` is the generated module; substitute your own operation and parameter +names. Pass the context of the span that should parent the remote operation. +This helper propagates that span; it does not create an HTTP client span. Without +a valid span context or a configured propagator, it adds no trace header. Inject +per request; putting a trace's headers in `ClientBuilder::default_headers` would +reuse that trace across unrelated calls. + +For applications using `tracing`, obtain the context with +`tracing::Span::current().context()` after importing +`tracing_opentelemetry::OpenTelemetrySpanExt`. This requires a +`tracing-opentelemetry` subscriber layer connected to the SDK. A plain `tracing` +span, or `Context::current()` without an attached OpenTelemetry context, is not +enough. The compatible versions for the example above are `tracing` 0.1 and +`tracing-opentelemetry` 0.31. + +#### Create HTTP spans and propagate through middleware + +[`reqwest-middleware`](https://docs.rs/reqwest-middleware/0.4.2/reqwest_middleware/) +and [`reqwest-tracing`](https://docs.rs/reqwest-tracing/0.5.8/reqwest_tracing/) +provide automatic HTTP client spans and header injection. With the Reqwest 0.12 +client, add these dependencies alongside OpenTelemetry 0.30 above: + +```toml +reqwest-middleware = "0.4.2" +reqwest-tracing = { version = "0.5.8", features = ["opentelemetry_0_30"] } +tracing = "0.1" +tracing-opentelemetry = "0.31" +tracing-subscriber = "0.3" +``` + +These versions form a compatible set. The OpenTelemetry feature is required for +header injection; `TracingMiddleware` without it only creates `tracing` spans. +Keep Reqwest, middleware, and tracing integration versions aligned when upgrading. +Use versioned Reqwest documentation: this client's documented and tested +dependency is 0.12, while the `/latest/` documentation can describe a different +release. + +Alongside the propagator setup above, connect the application's existing +`SdkTracerProvider` to `tracing` once at startup (or add the layer to your existing +subscriber). Keep the provider alive and shut it down at application exit to +flush exported spans: + +```rust +use opentelemetry::trace::TracerProvider; +use tracing_subscriber::prelude::*; + +tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("example-app"))) + .try_init()?; +``` + +Share the configured Reqwest client between the generated request builders and +the middleware executor. Building a request does not send it: + +```rust +use reqwest_middleware::ClientBuilder; +use reqwest_tracing::TracingMiddleware; +use tracing::Instrument; + +let http = reqwest::Client::builder().build()?; +let api = api::Client::new(http.clone(), "https://api.example.com".into(), None); +let traced_http = ClientBuilder::new(http) + .with(TracingMiddleware::default()) + .build(); + +let result: Result = async { + let request = api.create_thing(params).build()?; + let response = traced_http.execute(request).await?; + api::CreateThingResponse::decode(response).await.map_err(Into::into) +} +.instrument(tracing::info_span!("create thing")) +.await; +``` + +Execute through `traced_http`; calling the generated builder's `.send()` sends +directly through plain Reqwest and bypasses middleware. The middleware creates a +child HTTP span under `create thing` and injects that child's context. Declared +error statuses still reach the generated response decoder. SSE responses still +stream; the HTTP middleware span ends when response headers arrive, so instrument +stream consumption separately if its lifetime matters. + +Across `.await`, use +[`Instrument::instrument` / `.in_current_span()`](https://docs.rs/tracing/latest/tracing/trait.Instrument.html) +for `tracing`, or +[`FutureExt::with_context`](https://docs.rs/opentelemetry/0.30.0/opentelemetry/trace/trait.FutureExt.html) +for direct OpenTelemetry contexts. Do not hold a `Span::enter()` or +`Context::attach()` guard across `.await`: those guards set thread-local state, +which can associate another task's work with the wrong span. When spawning a +task, instrument its future explicitly; Tokio does not automatically inherit +the caller's current span. + ## Develop [Go](https://go.dev) builds the generator and runs its tests. @@ -182,6 +346,8 @@ 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) for explicit context injection and automatic +HTTP tracing with middleware. Rust supports JSON and raw request bodies, optional bodies, scalar and repeated query parameters, headers, escaped path parameters, enums, nullable values and From f7e05f35f5b51e4eda9df951edb4217779c15ef2 Mon Sep 17 00:00:00 2001 From: adelnizamutdinov Date: Sun, 27 Sep 2026 05:56:23 +0300 Subject: [PATCH 2/2] Accept middleware clients for automatic Rust trace propagation --- README.md | 187 ++-------- internal/rustemit/rustemit.go | 7 +- testdata/rust-client/Cargo.lock | 626 +++++++++++++++++++++++++++++++- testdata/rust-client/Cargo.toml | 13 +- testdata/rust-client/src/lib.rs | 80 +++- 5 files changed, 741 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index a6badd7..2ba0475 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ 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. Rust clients expose Reqwest request -builders for context injection or execution through application-owned middleware. +`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. @@ -144,166 +144,33 @@ by the application, commonly `traceparent`, `tracestate`, and `baggage`. ### Rust -Generated operations return a normal -[`reqwest::RequestBuilder`](https://docs.rs/reqwest/0.12.28/reqwest/struct.RequestBuilder.html). -They prepare the URL, authentication, parameters, and body; the caller chooses -when and how to send the request. Plain Reqwest does not inject OpenTelemetry -headers automatically. Both approaches below work with the generated client -without changing generated code. - -Rust has no standard equivalent of Go's `context.Context` that combines tracing, -deadlines, and cancellation, nor a standard global `fetch` hook. The -[`opentelemetry::Context`](https://docs.rs/opentelemetry/0.30.0/opentelemetry/context/struct.Context.html) -type carries telemetry context. Pass it explicitly, or use async instrumentation -to make a span current while a future is polled. Timeouts and cancellation remain -separate: use Reqwest's `.timeout(...)` and your async runtime's cancellation -mechanism. An OpenTelemetry context does not cancel HTTP requests. -The standard library's `std::task::Context` is an executor polling interface, -unrelated to request or trace context. - -#### Propagate an explicit context - -In addition to the [Rust client dependencies](#rust-clients), this example uses: - -```toml -opentelemetry = "0.30" -opentelemetry_sdk = "0.30" -opentelemetry-http = "0.30" -``` - -Register the application's propagator once at startup: - -```rust -opentelemetry::global::set_text_map_propagator( - opentelemetry_sdk::propagation::TraceContextPropagator::new(), -); -``` - -This configures W3C `traceparent` and `tracestate` propagation. To propagate -baggage too, register a `TextMapCompositePropagator` containing both -`TraceContextPropagator` and `BaggagePropagator`. Propagator setup is separate from -initializing the application's tracing SDK and exporter. - -Keep a send helper in the application. Reqwest's `build_split()` returns the -original HTTP client along with the built request, preserving its connection -pool and configuration: +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 -use opentelemetry::{global, Context}; -use opentelemetry_http::HeaderInjector; - -async fn send_with_context( - builder: reqwest::RequestBuilder, - cx: &Context, -) -> Result { - let (http, request) = builder.build_split(); - let mut request = request?; - global::get_text_map_propagator(|propagator| { - propagator.inject_context(cx, &mut HeaderInjector(request.headers_mut())); - }); - http.execute(request).await -} - -async fn create_thing( - api: &api::Client, - params: api::CreateThingParams, - cx: &Context, -) -> Result { - let response = send_with_context(api.create_thing(params), cx).await?; - api::CreateThingResponse::decode(response).await -} -``` - -Here `api` is the generated module; substitute your own operation and parameter -names. Pass the context of the span that should parent the remote operation. -This helper propagates that span; it does not create an HTTP client span. Without -a valid span context or a configured propagator, it adds no trace header. Inject -per request; putting a trace's headers in `ClientBuilder::default_headers` would -reuse that trace across unrelated calls. - -For applications using `tracing`, obtain the context with -`tracing::Span::current().context()` after importing -`tracing_opentelemetry::OpenTelemetrySpanExt`. This requires a -`tracing-opentelemetry` subscriber layer connected to the SDK. A plain `tracing` -span, or `Context::current()` without an attached OpenTelemetry context, is not -enough. The compatible versions for the example above are `tracing` 0.1 and -`tracing-opentelemetry` 0.31. - -#### Create HTTP spans and propagate through middleware - -[`reqwest-middleware`](https://docs.rs/reqwest-middleware/0.4.2/reqwest_middleware/) -and [`reqwest-tracing`](https://docs.rs/reqwest-tracing/0.5.8/reqwest_tracing/) -provide automatic HTTP client spans and header injection. With the Reqwest 0.12 -client, add these dependencies alongside OpenTelemetry 0.30 above: - -```toml -reqwest-middleware = "0.4.2" -reqwest-tracing = { version = "0.5.8", features = ["opentelemetry_0_30"] } -tracing = "0.1" -tracing-opentelemetry = "0.31" -tracing-subscriber = "0.3" -``` - -These versions form a compatible set. The OpenTelemetry feature is required for -header injection; `TracingMiddleware` without it only creates `tracing` spans. -Keep Reqwest, middleware, and tracing integration versions aligned when upgrading. -Use versioned Reqwest documentation: this client's documented and tested -dependency is 0.12, while the `/latest/` documentation can describe a different -release. - -Alongside the propagator setup above, connect the application's existing -`SdkTracerProvider` to `tracing` once at startup (or add the layer to your existing -subscriber). Keep the provider alive and shut it down at application exit to -flush exported spans: +let http = reqwest_middleware::ClientBuilder::new(reqwest::Client::builder().build()?) + .with(reqwest_tracing::TracingMiddleware::default()) + .build(); -```rust -use opentelemetry::trace::TracerProvider; -use tracing_subscriber::prelude::*; +let api = api::Client::new(http, "https://api.example.com".into(), None); -tracing_subscriber::registry() - .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("example-app"))) - .try_init()?; +// Inside the application's existing tracing span: +let response = api.create_thing(params).send().await?; +let result = api::CreateThingResponse::decode(response).await?; ``` -Share the configured Reqwest client between the generated request builders and -the middleware executor. Building a request does not send it: - -```rust -use reqwest_middleware::ClientBuilder; -use reqwest_tracing::TracingMiddleware; -use tracing::Instrument; - -let http = reqwest::Client::builder().build()?; -let api = api::Client::new(http.clone(), "https://api.example.com".into(), None); -let traced_http = ClientBuilder::new(http) - .with(TracingMiddleware::default()) - .build(); - -let result: Result = async { - let request = api.create_thing(params).build()?; - let response = traced_http.execute(request).await?; - api::CreateThingResponse::decode(response).await.map_err(Into::into) -} -.instrument(tracing::info_span!("create thing")) -.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`. -Execute through `traced_http`; calling the generated builder's `.send()` sends -directly through plain Reqwest and bypasses middleware. The middleware creates a -child HTTP span under `create thing` and injects that child's context. Declared -error statuses still reach the generated response decoder. SSE responses still -stream; the HTTP middleware span ends when response headers arrive, so instrument -stream consumption separately if its lifetime matters. - -Across `.await`, use -[`Instrument::instrument` / `.in_current_span()`](https://docs.rs/tracing/latest/tracing/trait.Instrument.html) -for `tracing`, or -[`FutureExt::with_context`](https://docs.rs/opentelemetry/0.30.0/opentelemetry/trace/trait.FutureExt.html) -for direct OpenTelemetry contexts. Do not hold a `Span::enter()` or -`Context::attach()` guard across `.await`: those guards set thread-local state, -which can associate another task's work with the wrong span. When spawning a -task, instrument its future explicitly; Tokio does not automatically inherit -the caller's current span. +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 @@ -337,17 +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) for explicit context injection and automatic -HTTP tracing with middleware. +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(); + } }