telemetry_rust/instrumentations/http/hyper/
mod.rs1use crate::{
41 Context, Value,
42 instrumentations::http::client::{HttpClientSpanBuilder, HttpError, UrlParts},
43};
44
45#[cfg(feature = "hyper-client-legacy")]
47pub mod legacy_client;
48
49#[cfg(feature = "hyper-client-legacy")]
50pub use legacy_client::HyperLegacyClientInstrument;
51
52impl UrlParts for hyper::Uri {
53 fn full_url(&self) -> Option<impl Into<Value>> {
54 self.host().map(|_| self.to_string())
55 }
56
57 fn path(&self) -> Option<impl Into<Value>> {
58 Some(self.path().to_owned())
59 }
60
61 fn host(&self) -> Option<impl Into<Value>> {
62 self.host().map(ToOwned::to_owned)
63 }
64
65 fn scheme(&self) -> Option<impl Into<Value>> {
66 self.scheme_str().map(ToOwned::to_owned)
67 }
68
69 fn port(&self) -> Option<impl Into<Value>> {
70 self.port_u16().map(i64::from)
71 }
72
73 fn query(&self) -> Option<impl Into<Value>> {
74 self.query().map(ToOwned::to_owned)
75 }
76}
77
78impl HttpError for hyper::Error {
79 fn error_type(&self) -> &'static str {
80 if self.is_timeout() {
81 "timeout"
82 } else if self.is_closed() {
83 "closed"
84 } else if self.is_canceled() {
85 "canceled"
86 } else if self.is_parse_status() {
87 "parse_status"
88 } else if self.is_parse() {
89 "parse"
90 } else if self.is_incomplete_message() {
91 "incomplete_message"
92 } else if self.is_body_write_aborted() {
93 "body_write_aborted"
94 } else if self.is_shutdown() {
95 "shutdown"
96 } else if self.is_user() {
97 "user"
98 } else {
99 "_OTHER"
100 }
101 }
102}
103
104impl<B> From<&hyper::Request<B>> for HttpClientSpanBuilder {
105 fn from(request: &hyper::Request<B>) -> Self {
106 Self::from_parts(request.method(), request.headers(), request.uri())
107 }
108}
109
110pub trait HyperSendRequestInstrument
112where
113 Self: Sized,
114{
115 fn instrument(self) -> InstrumentedSendRequest<Self>;
118}
119
120#[cfg(feature = "hyper-http1")]
121impl<B> HyperSendRequestInstrument for hyper::client::conn::http1::SendRequest<B> {
122 fn instrument(self) -> InstrumentedSendRequest<Self> {
123 InstrumentedSendRequest::new(self)
124 }
125}
126
127#[cfg(feature = "hyper-http2")]
128impl<B> HyperSendRequestInstrument for hyper::client::conn::http2::SendRequest<B> {
129 fn instrument(self) -> InstrumentedSendRequest<Self> {
130 InstrumentedSendRequest::new(self)
131 }
132}
133
134#[must_use = "SendRequest does nothing until you call send_request()"]
137pub struct InstrumentedSendRequest<S> {
138 inner: S,
139 context: Option<Context>,
140}
141
142impl<S> InstrumentedSendRequest<S> {
143 pub fn new(inner: S) -> Self {
145 Self {
146 inner,
147 context: None,
148 }
149 }
150
151 pub fn context(mut self, context: &Context) -> Self {
153 self.context = Some(context.clone());
154 self
155 }
156
157 pub fn set_context(mut self, context: Option<&Context>) -> Self {
159 self.context = context.cloned();
160 self
161 }
162
163 pub fn into_inner(self) -> S {
165 self.inner
166 }
167}
168
169impl<S> Clone for InstrumentedSendRequest<S>
170where
171 S: Clone,
172{
173 fn clone(&self) -> Self {
174 Self {
175 inner: self.inner.clone(),
176 context: self.context.clone(),
177 }
178 }
179}
180
181#[cfg(any(feature = "hyper-http1", feature = "hyper-http2"))]
182macro_rules! impl_instrumented_send_request {
183 ($http:ident) => {
184 mod $http {
185 use std::future::Future;
186 use std::task::{Context as TaskContext, Poll};
187
188 use hyper::{
189 Request, Response, Result,
190 body::{Body, Incoming},
191 client::conn::$http::SendRequest,
192 };
193
194 use super::InstrumentedSendRequest;
195 use crate::{
196 future::InstrumentedFuture, http,
197 instrumentations::http::client::HttpClientSpanBuilder,
198 };
199
200 impl<B: Body + 'static> InstrumentedSendRequest<SendRequest<B>> {
201 pub fn poll_ready(
203 &mut self,
204 cx: &mut TaskContext<'_>,
205 ) -> Poll<Result<()>> {
206 self.inner.poll_ready(cx)
207 }
208
209 pub async fn ready(&mut self) -> Result<()> {
211 self.inner.ready().await
212 }
213
214 pub fn is_ready(&self) -> bool {
216 self.inner.is_ready()
217 }
218
219 pub fn is_closed(&self) -> bool {
221 self.inner.is_closed()
222 }
223
224 pub fn send_request(
226 &mut self,
227 mut request: Request<B>,
228 ) -> impl Future<Output = Result<Response<Incoming>>> + '_ {
229 let span = HttpClientSpanBuilder::from(&request).start(&self.context);
230
231 http::inject_context_on_context(
232 span.context(),
233 request.headers_mut(),
234 );
235
236 let future = self.inner.send_request(request);
237 InstrumentedFuture::new(future, span)
238 }
239 }
240 }
241 };
242}
243
244#[cfg(feature = "hyper-http1")]
245impl_instrumented_send_request!(http1);
246#[cfg(feature = "hyper-http2")]
247impl_instrumented_send_request!(http2);
248
249#[cfg(all(test, any(feature = "hyper-http1", feature = "hyper-http2")))]
250mod tests {
251 use super::HyperSendRequestInstrument;
252 use crate::{Context, instrumentations::http::test_utils::*, semconv};
253 use axum::http::StatusCode;
254 use bytes::Bytes;
255 use http_body_util::Empty;
256 use hyper::{
257 Request,
258 header::{HOST, USER_AGENT},
259 };
260 use hyper_util::rt::TokioIo;
261 use opentelemetry::trace::{Span, SpanKind, TraceContextExt, Tracer, TracerProvider};
262 use serial_test::serial;
263 use tokio::net::TcpStream;
264 use tracing_subscriber::{Registry, layer::SubscriberExt};
265
266 #[cfg(feature = "hyper-http1")]
267 mod http1 {
268 use super::*;
269 use assert2::assert;
270
271 #[tokio::test]
272 #[serial]
273 async fn instruments_successful_requests() {
274 let telemetry = configure_test_tracing();
275 let server = spawn_server().await;
276 let io = TokioIo::new(TcpStream::connect(server.addr).await.unwrap());
277 let (send_request, connection) =
278 hyper::client::conn::http1::handshake(io).await.unwrap();
279
280 tokio::spawn(async move {
281 connection.await.unwrap();
282 });
283
284 let mut send_request = send_request.instrument();
285 let request_url = format!("{}/ok?ready=true", server.base_url);
286 let response = send_request
287 .send_request(
288 Request::builder()
289 .uri(&request_url)
290 .header(HOST, server.authority())
291 .header(USER_AGENT, "telemetry-rust-tests")
292 .body(Empty::<Bytes>::new())
293 .unwrap(),
294 )
295 .await
296 .unwrap();
297
298 assert!(response.status() == StatusCode::OK);
299
300 let spans = force_flush_and_get_spans(&telemetry);
301 let span = find_span(&spans, "GET");
302 let traceparent = server.state.traceparent_for("/ok").unwrap();
303 let (trace_id, span_id) = traceparent_ids(&traceparent);
304
305 assert!(span.span_kind == SpanKind::Client);
306 assert!(span.span_context.trace_id().to_string() == trace_id);
307 assert!(span.span_context.span_id().to_string() == span_id);
308 assert!(matches!(span.status, opentelemetry::trace::Status::Unset));
309 assert!(string_attr(span, semconv::HTTP_REQUEST_METHOD) == Some("GET"));
310 assert!(string_attr(span, semconv::URL_SCHEME) == Some("http"));
311 assert!(string_attr(span, semconv::SERVER_ADDRESS) == Some("127.0.0.1"));
312 assert!(
313 i64_attr(span, semconv::SERVER_PORT)
314 == Some(i64::from(server.addr.port()))
315 );
316 assert!(string_attr(span, semconv::URL_PATH) == Some("/ok"));
317 assert!(string_attr(span, semconv::URL_QUERY) == Some("ready=true"));
318 assert!(
319 string_attr(span, semconv::USER_AGENT_ORIGINAL)
320 == Some("telemetry-rust-tests")
321 );
322 assert!(string_attr(span, semconv::URL_FULL) == Some(request_url.as_str()));
323 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE) == Some(200));
324 assert!(string_attr(span, semconv::NETWORK_PROTOCOL_VERSION).is_some());
325 assert!(string_attr(span, semconv::NETWORK_PEER_ADDRESS).is_none());
326 assert!(i64_attr(span, semconv::NETWORK_PEER_PORT).is_none());
327 }
328
329 #[tokio::test]
330 #[serial]
331 async fn falls_back_to_host_header_for_url_full_on_origin_form_uri() {
332 let telemetry = configure_test_tracing();
333 let server = spawn_server().await;
334 let io = TokioIo::new(TcpStream::connect(server.addr).await.unwrap());
335 let (send_request, connection) =
336 hyper::client::conn::http1::handshake(io).await.unwrap();
337
338 tokio::spawn(async move {
339 connection.await.unwrap();
340 });
341
342 let mut send_request = send_request.instrument();
343 let response = send_request
344 .send_request(
345 Request::builder()
346 .uri("/ok?ready=true")
347 .header(HOST, server.authority())
348 .header(USER_AGENT, "telemetry-rust-tests")
349 .body(Empty::<Bytes>::new())
350 .unwrap(),
351 )
352 .await
353 .unwrap();
354
355 assert!(response.status() == StatusCode::OK);
356
357 let spans = force_flush_and_get_spans(&telemetry);
358 let span = find_span(&spans, "GET");
359
360 let expected_url = format!("//{}/ok?ready=true", server.authority());
361 assert!(string_attr(span, semconv::URL_FULL) == Some(expected_url.as_str()));
362 assert!(string_attr(span, semconv::SERVER_ADDRESS) == Some("127.0.0.1"));
363 assert!(
364 i64_attr(span, semconv::SERVER_PORT)
365 == Some(i64::from(server.addr.port()))
366 );
367 }
368
369 #[tokio::test]
370 #[serial]
371 async fn falls_back_to_host_header_without_port_for_origin_form_uri() {
372 let telemetry = configure_test_tracing();
373 let server = spawn_server().await;
374 let io = TokioIo::new(TcpStream::connect(server.addr).await.unwrap());
375 let (send_request, connection) =
376 hyper::client::conn::http1::handshake(io).await.unwrap();
377
378 tokio::spawn(async move {
379 connection.await.unwrap();
380 });
381
382 let mut send_request = send_request.instrument();
383 let response = send_request
384 .send_request(
385 Request::builder()
386 .uri("/ok?ready=true")
387 .header(HOST, "localhost")
388 .header(USER_AGENT, "telemetry-rust-tests")
389 .body(Empty::<Bytes>::new())
390 .unwrap(),
391 )
392 .await
393 .unwrap();
394
395 assert!(response.status() == StatusCode::OK);
396
397 let spans = force_flush_and_get_spans(&telemetry);
398 let span = find_span(&spans, "GET");
399
400 assert!(
401 string_attr(span, semconv::URL_FULL) == Some("//localhost/ok?ready=true")
402 );
403 assert!(string_attr(span, semconv::SERVER_ADDRESS) == Some("localhost"));
404 assert!(i64_attr(span, semconv::SERVER_PORT).is_none());
405 }
406
407 #[tokio::test]
408 #[serial]
409 async fn ignores_invalid_host_header_for_origin_form_uri() {
410 let telemetry = configure_test_tracing();
411 let server = spawn_server().await;
412 let io = TokioIo::new(TcpStream::connect(server.addr).await.unwrap());
413 let (send_request, connection) =
414 hyper::client::conn::http1::handshake(io).await.unwrap();
415
416 tokio::spawn(async move {
417 connection.await.unwrap();
418 });
419
420 let mut send_request = send_request.instrument();
421 let response = send_request
422 .send_request(
423 Request::builder()
424 .uri("/ok?ready=true")
425 .header(HOST, "bad host value")
426 .header(USER_AGENT, "telemetry-rust-tests")
427 .body(Empty::<Bytes>::new())
428 .unwrap(),
429 )
430 .await
431 .unwrap();
432
433 assert!(response.status() == StatusCode::OK);
434
435 let spans = force_flush_and_get_spans(&telemetry);
436 let span = find_span(&spans, "GET");
437
438 assert!(string_attr(span, semconv::URL_FULL).is_none());
439 assert!(string_attr(span, semconv::SERVER_ADDRESS).is_none());
440 assert!(i64_attr(span, semconv::SERVER_PORT).is_none());
441 }
442
443 #[tokio::test]
444 #[serial]
445 async fn uri_values_take_precedence_over_host_header_fallback() {
446 let telemetry = configure_test_tracing();
447 let server = spawn_server().await;
448 let io = TokioIo::new(TcpStream::connect(server.addr).await.unwrap());
449 let (send_request, connection) =
450 hyper::client::conn::http1::handshake(io).await.unwrap();
451
452 tokio::spawn(async move {
453 connection.await.unwrap();
454 });
455
456 let mut send_request = send_request.instrument();
457 let request_url = format!("{}/ok?ready=true", server.base_url);
458 let response = send_request
459 .send_request(
460 Request::builder()
461 .uri(&request_url)
462 .header(HOST, "example.com:1234")
463 .header(USER_AGENT, "telemetry-rust-tests")
464 .body(Empty::<Bytes>::new())
465 .unwrap(),
466 )
467 .await
468 .unwrap();
469
470 assert!(response.status() == StatusCode::OK);
471
472 let spans = force_flush_and_get_spans(&telemetry);
473 let span = find_span(&spans, "GET");
474
475 assert!(string_attr(span, semconv::URL_FULL) == Some(request_url.as_str()));
476 assert!(string_attr(span, semconv::SERVER_ADDRESS) == Some("127.0.0.1"));
477 assert!(
478 i64_attr(span, semconv::SERVER_PORT)
479 == Some(i64::from(server.addr.port()))
480 );
481 }
482
483 #[tokio::test]
484 #[serial]
485 async fn uses_explicit_parent_context_when_provided() {
486 let telemetry = configure_test_tracing();
487 let server = spawn_server().await;
488 let io = TokioIo::new(TcpStream::connect(server.addr).await.unwrap());
489 let (send_request, connection) =
490 hyper::client::conn::http1::handshake(io).await.unwrap();
491 let tracer = telemetry.provider.tracer("hyper-tests");
492 let explicit_parent = tracer.start("explicit-parent");
493 let explicit_parent_span_id = explicit_parent.span_context().span_id();
494 let explicit_parent_cx = Context::current_with_span(explicit_parent);
495 let tracing_tracer = telemetry.provider.tracer("tracing-tests");
496 let subscriber = Registry::default()
497 .with(tracing_opentelemetry::layer().with_tracer(tracing_tracer));
498 let guard = tracing::subscriber::set_default(subscriber);
499 let current_parent = tracing::info_span!("current-parent");
500
501 tokio::spawn(async move {
502 connection.await.unwrap();
503 });
504
505 tracing::Instrument::instrument(
506 async {
507 let mut send_request =
508 send_request.instrument().context(&explicit_parent_cx);
509 send_request
510 .send_request(
511 Request::builder()
512 .uri(format!("{}/ok", server.base_url))
513 .header(HOST, server.authority())
514 .body(Empty::<Bytes>::new())
515 .unwrap(),
516 )
517 .await
518 .unwrap();
519 },
520 current_parent,
521 )
522 .await;
523
524 drop(guard);
525 explicit_parent_cx.span().end();
526
527 let spans = force_flush_and_get_spans(&telemetry);
528 let hyper_span = find_span(&spans, "GET");
529 let current_span = find_span(&spans, "current-parent");
530
531 assert!(hyper_span.parent_span_id == explicit_parent_span_id);
532 assert!(hyper_span.parent_span_id != current_span.span_context.span_id());
533 }
534
535 #[tokio::test]
536 #[serial]
537 async fn marks_error_responses_as_errors() {
538 let telemetry = configure_test_tracing();
539 let server = spawn_server().await;
540 let io = TokioIo::new(TcpStream::connect(server.addr).await.unwrap());
541 let (send_request, connection) =
542 hyper::client::conn::http1::handshake(io).await.unwrap();
543
544 tokio::spawn(async move {
545 connection.await.unwrap();
546 });
547
548 let mut send_request = send_request.instrument();
549 let response = send_request
550 .send_request(
551 Request::builder()
552 .uri(format!("{}/not-found", server.base_url))
553 .header(HOST, server.authority())
554 .body(Empty::<Bytes>::new())
555 .unwrap(),
556 )
557 .await
558 .unwrap();
559
560 assert!(response.status() == StatusCode::NOT_FOUND);
561
562 let spans = force_flush_and_get_spans(&telemetry);
563 let span = find_span(&spans, "GET");
564
565 assert!(matches!(
566 span.status,
567 opentelemetry::trace::Status::Error { .. }
568 ));
569 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE) == Some(404));
570 assert!(string_attr(span, semconv::ERROR_TYPE) == Some("404"));
571 }
572 }
573
574 #[cfg(feature = "hyper-http2")]
575 mod http2 {
576 use super::*;
577 use assert2::assert;
578 use hyper_util::rt::TokioExecutor;
579
580 #[tokio::test]
581 #[serial]
582 async fn instruments_successful_requests() {
583 let telemetry = configure_test_tracing();
584 let server = spawn_server().await;
585 let io = TokioIo::new(TcpStream::connect(server.addr).await.unwrap());
586 let (send_request, connection) =
587 hyper::client::conn::http2::Builder::new(TokioExecutor::new())
588 .handshake(io)
589 .await
590 .unwrap();
591
592 tokio::spawn(async move {
593 connection.await.unwrap();
594 });
595
596 let mut send_request = send_request.instrument();
597 let request_url = format!("{}/ok?ready=true", server.base_url);
598 let response = send_request
599 .send_request(
600 Request::builder()
601 .uri(&request_url)
602 .header(HOST, server.authority())
603 .header(USER_AGENT, "telemetry-rust-tests")
604 .body(Empty::<Bytes>::new())
605 .unwrap(),
606 )
607 .await
608 .unwrap();
609
610 assert!(response.status() == StatusCode::OK);
611
612 let spans = force_flush_and_get_spans(&telemetry);
613 let span = find_span(&spans, "GET");
614 let traceparent = server.state.traceparent_for("/ok").unwrap();
615 let (trace_id, span_id) = traceparent_ids(&traceparent);
616
617 assert!(span.span_kind == SpanKind::Client);
618 assert!(span.span_context.trace_id().to_string() == trace_id);
619 assert!(span.span_context.span_id().to_string() == span_id);
620 assert!(matches!(span.status, opentelemetry::trace::Status::Unset));
621 assert!(string_attr(span, semconv::HTTP_REQUEST_METHOD) == Some("GET"));
622 assert!(string_attr(span, semconv::URL_SCHEME) == Some("http"));
623 assert!(string_attr(span, semconv::SERVER_ADDRESS) == Some("127.0.0.1"));
624 assert!(
625 i64_attr(span, semconv::SERVER_PORT)
626 == Some(i64::from(server.addr.port()))
627 );
628 assert!(string_attr(span, semconv::URL_PATH) == Some("/ok"));
629 assert!(string_attr(span, semconv::URL_QUERY) == Some("ready=true"));
630 assert!(string_attr(span, semconv::URL_FULL) == Some(request_url.as_str()));
631 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE) == Some(200));
632 assert!(string_attr(span, semconv::NETWORK_PROTOCOL_VERSION).is_some());
633 }
634 }
635}