telemetry_rust/instrumentations/http/
reqwest.rs1use ::reqwest as reqwest_crate;
20use std::future::Future;
21
22use crate::{
23 Context, Value, http,
24 instrumentations::http::client::{HttpClientSpanBuilder, UrlParts},
25};
26
27impl UrlParts for url::Url {
28 fn full_url(&self) -> Option<impl Into<Value>> {
29 Some(self.as_str().to_owned())
30 }
31
32 fn path(&self) -> Option<impl Into<Value>> {
33 Some(self.path().to_owned())
34 }
35
36 fn host(&self) -> Option<impl Into<Value>> {
37 self.host_str().map(ToOwned::to_owned)
38 }
39
40 fn scheme(&self) -> Option<impl Into<Value>> {
41 match self.scheme() {
42 "" => None,
43 scheme => Some(scheme.to_owned()),
44 }
45 }
46
47 fn port(&self) -> Option<impl Into<Value>> {
48 self.port_or_known_default().map(i64::from)
49 }
50
51 fn query(&self) -> Option<impl Into<Value>> {
52 self.query().map(ToOwned::to_owned)
53 }
54}
55
56pub trait ReqwestBuilderInstrument
72where
73 Self: Sized,
74{
75 fn instrument(self) -> InstrumentedRequestBuilder;
77}
78
79impl ReqwestBuilderInstrument for reqwest_crate::RequestBuilder {
80 fn instrument(self) -> InstrumentedRequestBuilder {
81 InstrumentedRequestBuilder::new(self)
82 }
83}
84
85impl HttpClientSpanBuilder {
86 pub(crate) fn from_reqwest_request(request: &reqwest_crate::Request) -> Self {
87 Self::from_parts(request.method(), request.headers(), request.url())
88 }
89}
90
91#[must_use = "RequestBuilder does nothing until you call send()"]
93pub struct InstrumentedRequestBuilder {
94 inner: reqwest_crate::RequestBuilder,
95 context: Option<Context>,
96}
97
98impl InstrumentedRequestBuilder {
99 pub fn new(inner: reqwest_crate::RequestBuilder) -> Self {
101 Self {
102 inner,
103 context: None,
104 }
105 }
106
107 pub fn context(mut self, context: &Context) -> Self {
109 self.context = Some(context.clone());
110 self
111 }
112
113 pub fn set_context(mut self, context: Option<&Context>) -> Self {
115 self.context = context.cloned();
116 self
117 }
118
119 pub fn send(
121 self,
122 ) -> impl Future<Output = Result<reqwest_crate::Response, reqwest_crate::Error>> {
123 let (client, request_result) = self.inner.build_split();
124 let context = self.context;
125
126 async move {
127 let mut request = request_result?;
128 let span_builder = HttpClientSpanBuilder::from_reqwest_request(&request);
129 let span = match context.as_ref() {
130 Some(context) => span_builder.start_with_context(context),
131 None => span_builder.start(),
132 };
133
134 http::inject_context_on_context(span.context(), request.headers_mut());
135
136 let result = client.execute(request).await;
137 match &result {
138 Ok(response) => {
139 span.end_response(
140 response.status(),
141 response.version(),
142 response.remote_addr(),
143 );
144 }
145 Err(error) => span.end_error(reqwest_error_type(error), error),
146 }
147 result
148 }
149 }
150}
151
152fn reqwest_error_type(error: &reqwest_crate::Error) -> &'static str {
153 if error.is_timeout() {
154 "timeout"
155 } else if error.is_connect() {
156 "connect"
157 } else if error.is_redirect() {
158 "redirect"
159 } else if error.is_request() {
160 "request"
161 } else if error.is_body() {
162 "body"
163 } else if error.is_decode() {
164 "decode"
165 } else if error.is_builder() {
166 "builder"
167 } else {
168 "_OTHER"
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::ReqwestBuilderInstrument;
175 use crate::{Context, OpenTelemetryLayer, Value, semconv};
176 use assert2::assert;
177 use axum::{
178 Router,
179 extract::State,
180 http::{HeaderMap, StatusCode},
181 response::{IntoResponse, Redirect},
182 routing::get,
183 };
184 use opentelemetry::{
185 global,
186 trace::{Span as _, SpanKind, TraceContextExt, Tracer as _, TracerProvider as _},
187 };
188 use opentelemetry_sdk::{
189 propagation::TraceContextPropagator,
190 trace::{InMemorySpanExporter, SdkTracerProvider as TracerProvider},
191 };
192 use serial_test::serial;
193 use std::sync::{Arc, Mutex};
194 use tokio::{net::TcpListener, task::JoinHandle};
195 use tracing_opentelemetry::OpenTelemetrySpanExt as _;
196 use tracing_subscriber::{Registry, layer::SubscriberExt};
197
198 #[derive(Clone, Default)]
199 struct TestState {
200 traceparents: Arc<Mutex<Vec<(String, String)>>>,
201 }
202
203 impl TestState {
204 fn record(&self, path: &str, headers: &HeaderMap) {
205 if let Some(traceparent) = headers
206 .get("traceparent")
207 .and_then(|value| value.to_str().ok())
208 {
209 self.traceparents
210 .lock()
211 .unwrap()
212 .push((path.to_owned(), traceparent.to_owned()));
213 }
214 }
215
216 fn traceparent_for(&self, path: &str) -> Option<String> {
217 self.traceparents
218 .lock()
219 .unwrap()
220 .iter()
221 .rev()
222 .find(|(recorded_path, _)| recorded_path == path)
223 .map(|(_, traceparent)| traceparent.clone())
224 }
225 }
226
227 #[tokio::test]
228 #[serial]
229 async fn instruments_successful_requests() {
230 let telemetry = configure_test_tracing();
231 let server = spawn_server().await;
232
233 let response = test_client()
234 .get(format!("{}/ok?ready=true", server.base_url))
235 .header(::reqwest::header::USER_AGENT, "telemetry-rust-tests")
236 .instrument()
237 .send()
238 .await
239 .unwrap();
240
241 assert!(response.status() == StatusCode::OK);
242
243 let spans = force_flush_and_get_spans(&telemetry);
244 let span = find_span(&spans, "GET");
245 let traceparent = server.state.traceparent_for("/ok").unwrap();
246 let (trace_id, span_id) = traceparent_ids(&traceparent);
247
248 assert!(span.span_kind == SpanKind::Client);
249 assert!(span.span_context.trace_id().to_string() == trace_id);
250 assert!(span.span_context.span_id().to_string() == span_id);
251 assert!(matches!(span.status, opentelemetry::trace::Status::Unset));
252 assert!(string_attr(span, semconv::HTTP_REQUEST_METHOD) == Some("GET"));
253 assert!(string_attr(span, semconv::URL_SCHEME) == Some("http"));
254 assert!(string_attr(span, semconv::SERVER_ADDRESS) == Some("127.0.0.1"));
255 assert!(string_attr(span, semconv::URL_PATH) == Some("/ok"));
256 assert!(string_attr(span, semconv::URL_QUERY) == Some("ready=true"));
257 assert!(
258 string_attr(span, semconv::USER_AGENT_ORIGINAL)
259 == Some("telemetry-rust-tests")
260 );
261 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE) == Some(200));
262 assert!(string_attr(span, semconv::NETWORK_PROTOCOL_VERSION).is_some());
263 assert!(string_attr(span, semconv::NETWORK_PEER_ADDRESS).is_some());
264 assert!(i64_attr(span, semconv::NETWORK_PEER_PORT).is_some());
265 }
266
267 #[tokio::test]
268 #[serial]
269 async fn propagates_traceparent_with_client_span_id() {
270 let telemetry = configure_test_tracing();
271 let server = spawn_server().await;
272
273 let tracer = global::tracer("reqwest-propagation-test");
274 let subscriber = Registry::default().with(OpenTelemetryLayer::new(tracer));
275 let _guard = tracing::subscriber::set_default(subscriber);
276
277 let parent = tracing::info_span!("parent");
278 let parent_context = parent.context();
279 let expected_trace_id = parent_context.span().span_context().trace_id();
280
281 tracing::Instrument::instrument(
282 async {
283 test_client()
284 .get(format!("{}/ok", server.base_url))
285 .instrument()
286 .send()
287 .await
288 .unwrap();
289 },
290 parent,
291 )
292 .await;
293
294 let spans = force_flush_and_get_spans(&telemetry);
295 let client_span = find_span(&spans, "GET");
296 let traceparent = server.state.traceparent_for("/ok").unwrap();
297 let (trace_id, span_id) = traceparent_ids(&traceparent);
298
299 assert!(trace_id == expected_trace_id.to_string());
301 assert!(span_id == client_span.span_context.span_id().to_string());
302 }
303
304 #[tokio::test]
305 #[serial]
306 async fn marks_client_error_responses_as_errors() {
307 let telemetry = configure_test_tracing();
308 let server = spawn_server().await;
309
310 let response = test_client()
311 .get(format!("{}/not-found", server.base_url))
312 .instrument()
313 .send()
314 .await
315 .unwrap();
316
317 assert!(response.status() == StatusCode::NOT_FOUND);
318
319 let spans = force_flush_and_get_spans(&telemetry);
320 let span = find_span(&spans, "GET");
321
322 assert!(matches!(
323 span.status,
324 opentelemetry::trace::Status::Error { .. }
325 ));
326 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE) == Some(404));
327 assert!(string_attr(span, semconv::ERROR_TYPE) == Some("404"));
328 }
329
330 #[tokio::test]
331 #[serial]
332 async fn marks_server_error_responses_as_errors() {
333 let telemetry = configure_test_tracing();
334 let server = spawn_server().await;
335
336 test_client()
337 .get(format!("{}/server-error", server.base_url))
338 .instrument()
339 .send()
340 .await
341 .unwrap();
342
343 let spans = force_flush_and_get_spans(&telemetry);
344 let span = find_span(&spans, "GET");
345
346 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE) == Some(500));
347 assert!(string_attr(span, semconv::ERROR_TYPE) == Some("500"));
348 assert!(matches!(
349 span.status,
350 opentelemetry::trace::Status::Error { .. }
351 ));
352 }
353
354 #[tokio::test]
355 #[serial]
356 async fn marks_transport_failures_as_errors() {
357 let telemetry = configure_test_tracing();
358
359 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
360 let addr = listener.local_addr().unwrap();
361 drop(listener);
362
363 let error = test_client()
364 .get(format!("http://{addr}/unavailable"))
365 .instrument()
366 .send()
367 .await
368 .unwrap_err();
369
370 assert!(error.is_connect());
371
372 let spans = force_flush_and_get_spans(&telemetry);
373 let span = find_span(&spans, "GET");
374
375 assert!(matches!(
376 span.status,
377 opentelemetry::trace::Status::Error { .. }
378 ));
379 assert!(string_attr(span, semconv::ERROR_TYPE) == Some("connect"));
380 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE).is_none());
381 }
382
383 #[tokio::test]
384 #[serial]
385 async fn preserves_original_url_when_redirects_are_followed() {
386 let telemetry = configure_test_tracing();
387 let server = spawn_server().await;
388
389 let response = test_client()
390 .get(format!("{}/redirect?step=1", server.base_url))
391 .instrument()
392 .send()
393 .await
394 .unwrap();
395
396 assert!(response.url().path() == "/final");
397
398 let spans = force_flush_and_get_spans(&telemetry);
399 let span = find_span(&spans, "GET");
400
401 let expected_url = format!("{}/redirect?step=1", server.base_url);
402 assert!(string_attr(span, semconv::URL_FULL) == Some(expected_url.as_str()));
403 assert!(server.state.traceparent_for("/redirect").is_some());
404 assert!(server.state.traceparent_for("/final").is_some());
405 }
406
407 #[tokio::test]
408 #[serial]
409 async fn uses_explicit_parent_context_when_provided() {
410 let telemetry = configure_test_tracing();
411 let server = spawn_server().await;
412 let tracer = telemetry.provider.tracer("reqwest-tests");
413 let explicit_parent = tracer.start("explicit-parent");
414 let explicit_parent_span_id = explicit_parent.span_context().span_id();
415 let explicit_parent_cx = Context::current_with_span(explicit_parent);
416 let tracing_tracer = telemetry.provider.tracer("tracing-tests");
417 let subscriber = Registry::default()
418 .with(tracing_opentelemetry::layer().with_tracer(tracing_tracer));
419 let guard = tracing::subscriber::set_default(subscriber);
420 let current_parent = tracing::info_span!("current-parent");
421
422 tracing::Instrument::instrument(
423 async {
424 test_client()
425 .get(format!("{}/ok", server.base_url))
426 .instrument()
427 .context(&explicit_parent_cx)
428 .send()
429 .await
430 .unwrap();
431 },
432 current_parent,
433 )
434 .await;
435
436 drop(guard);
437 explicit_parent_cx.span().end();
438
439 let spans = force_flush_and_get_spans(&telemetry);
440 let reqwest_span = find_span(&spans, "GET");
441 let current_span = find_span(&spans, "current-parent");
442
443 assert!(reqwest_span.parent_span_id == explicit_parent_span_id);
444 assert!(reqwest_span.parent_span_id != current_span.span_context.span_id());
445 }
446
447 #[tokio::test]
448 #[serial]
449 async fn does_not_emit_span_for_invalid_builder() {
450 let telemetry = configure_test_tracing();
451
452 let result = test_client()
453 .get("http://example.com")
454 .header("bad\nheader", "value")
455 .instrument()
456 .send()
457 .await;
458
459 assert!(result.is_err());
460 assert!(result.unwrap_err().is_builder());
461
462 let spans = force_flush_and_get_spans(&telemetry);
463 assert!(client_spans(&spans).is_empty());
464 }
465
466 struct TestServer {
469 base_url: String,
470 state: TestState,
471 _handle: JoinHandle<()>,
472 }
473
474 async fn spawn_server() -> TestServer {
475 async fn ok(
476 State(state): State<TestState>,
477 headers: HeaderMap,
478 ) -> impl IntoResponse {
479 state.record("/ok", &headers);
480 StatusCode::OK
481 }
482
483 async fn not_found(
484 State(state): State<TestState>,
485 headers: HeaderMap,
486 ) -> impl IntoResponse {
487 state.record("/not-found", &headers);
488 StatusCode::NOT_FOUND
489 }
490
491 async fn server_error(
492 State(state): State<TestState>,
493 headers: HeaderMap,
494 ) -> impl IntoResponse {
495 state.record("/server-error", &headers);
496 StatusCode::INTERNAL_SERVER_ERROR
497 }
498
499 async fn redirect(
500 State(state): State<TestState>,
501 headers: HeaderMap,
502 ) -> impl IntoResponse {
503 state.record("/redirect", &headers);
504 Redirect::temporary("/final")
505 }
506
507 async fn final_route(
508 State(state): State<TestState>,
509 headers: HeaderMap,
510 ) -> impl IntoResponse {
511 state.record("/final", &headers);
512 StatusCode::OK
513 }
514
515 let state = TestState::default();
516 let app = Router::new()
517 .route("/ok", get(ok))
518 .route("/not-found", get(not_found))
519 .route("/server-error", get(server_error))
520 .route("/redirect", get(redirect))
521 .route("/final", get(final_route))
522 .with_state(state.clone());
523
524 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
525 let addr = listener.local_addr().unwrap();
526 let handle = tokio::spawn(async move {
527 axum::serve(listener, app).await.unwrap();
528 });
529
530 TestServer {
531 base_url: format!("http://{addr}"),
532 state,
533 _handle: handle,
534 }
535 }
536
537 fn configure_test_tracing() -> TestTelemetry {
538 let exporter = InMemorySpanExporter::default();
539 let provider = TracerProvider::builder()
540 .with_simple_exporter(exporter.clone())
541 .build();
542 global::set_tracer_provider(provider.clone());
543 global::set_text_map_propagator(TraceContextPropagator::new());
544 TestTelemetry { exporter, provider }
545 }
546
547 fn test_client() -> ::reqwest::Client {
548 ::reqwest::Client::builder().no_proxy().build().unwrap()
549 }
550
551 fn force_flush_and_get_spans(
552 telemetry: &TestTelemetry,
553 ) -> Vec<opentelemetry_sdk::trace::SpanData> {
554 telemetry.provider.force_flush().unwrap();
555 telemetry.exporter.get_finished_spans().unwrap()
556 }
557
558 fn client_spans(
559 spans: &[opentelemetry_sdk::trace::SpanData],
560 ) -> Vec<&opentelemetry_sdk::trace::SpanData> {
561 spans
562 .iter()
563 .filter(|span| span.span_kind == SpanKind::Client)
564 .collect()
565 }
566
567 fn find_span<'a>(
568 spans: &'a [opentelemetry_sdk::trace::SpanData],
569 name: &str,
570 ) -> &'a opentelemetry_sdk::trace::SpanData {
571 spans.iter().find(|span| span.name == name).unwrap()
572 }
573
574 fn string_attr<'a>(
575 span: &'a opentelemetry_sdk::trace::SpanData,
576 key: &str,
577 ) -> Option<&'a str> {
578 match attr(span, key) {
579 Some(Value::String(value)) => Some(value.as_str()),
580 _ => None,
581 }
582 }
583
584 fn i64_attr(span: &opentelemetry_sdk::trace::SpanData, key: &str) -> Option<i64> {
585 match attr(span, key) {
586 Some(Value::I64(value)) => Some(*value),
587 _ => None,
588 }
589 }
590
591 fn attr<'a>(
592 span: &'a opentelemetry_sdk::trace::SpanData,
593 key: &str,
594 ) -> Option<&'a Value> {
595 span.attributes
596 .iter()
597 .find(|kv| kv.key.as_str() == key)
598 .map(|kv| &kv.value)
599 }
600
601 fn traceparent_ids(traceparent: &str) -> (&str, &str) {
602 let mut parts = traceparent.split('-');
603 let _version = parts.next().unwrap();
604 let trace_id = parts.next().unwrap();
605 let span_id = parts.next().unwrap();
606 (trace_id, span_id)
607 }
608
609 struct TestTelemetry {
610 exporter: InMemorySpanExporter,
611 provider: TracerProvider,
612 }
613
614 impl Drop for TestTelemetry {
615 fn drop(&mut self) {
616 let _ = self.provider.shutdown();
617 }
618 }
619}