telemetry_rust/instrumentations/http/hyper/
legacy_client.rs1use std::error::Error as StdError;
34use std::future::Future;
35
36use hyper::{
37 Request, Response,
38 body::{Body, Incoming},
39};
40use hyper_util::client::legacy;
41
42use crate::{
43 Context,
44 future::InstrumentedFuture,
45 http,
46 instrumentations::http::client::{HttpClientSpanBuilder, HttpError},
47};
48
49impl HttpError for legacy::Error {
50 fn error_type(&self) -> &'static str {
51 if self.is_connect() {
52 "connect"
53 } else if let Some(hyper_error) = self
54 .source()
55 .and_then(|source| source.downcast_ref::<hyper::Error>())
56 {
57 hyper_error.error_type()
58 } else {
59 "_OTHER"
60 }
61 }
62}
63
64pub trait HyperLegacyClientInstrument
67where
68 Self: Sized,
69{
70 type Connector;
72
73 type Body;
75
76 fn instrument(self) -> InstrumentedLegacyClient<Self::Connector, Self::Body>;
79}
80
81impl<C, B> HyperLegacyClientInstrument for legacy::Client<C, B> {
82 type Connector = C;
83 type Body = B;
84
85 fn instrument(self) -> InstrumentedLegacyClient<C, B> {
86 InstrumentedLegacyClient::new(self)
87 }
88}
89
90#[must_use = "Client does nothing until you call request()"]
93pub struct InstrumentedLegacyClient<C, B> {
94 inner: legacy::Client<C, B>,
95 context: Option<Context>,
96}
97
98impl<C, B> InstrumentedLegacyClient<C, B> {
99 pub fn new(inner: legacy::Client<C, B>) -> 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 into_inner(self) -> legacy::Client<C, B> {
121 self.inner
122 }
123}
124
125impl<C, B> Clone for InstrumentedLegacyClient<C, B>
126where
127 legacy::Client<C, B>: Clone,
128{
129 fn clone(&self) -> Self {
130 Self {
131 inner: self.inner.clone(),
132 context: self.context.clone(),
133 }
134 }
135}
136
137impl<C, B> InstrumentedLegacyClient<C, B>
138where
139 C: legacy::connect::Connect + Clone + Send + Sync + 'static,
140 B: Body + Send + Unpin + 'static,
141 B::Data: Send,
142 B::Error: Into<Box<dyn StdError + Send + Sync>>,
143{
144 pub fn request(
147 &self,
148 mut request: Request<B>,
149 ) -> impl Future<Output = Result<Response<Incoming>, legacy::Error>> + '_ {
150 let span = HttpClientSpanBuilder::from(&request).start(&self.context);
151
152 http::inject_context_on_context(span.context(), request.headers_mut());
153
154 let future = self.inner.request(request);
155 InstrumentedFuture::new(future, span)
156 }
157
158 pub fn get(
161 &self,
162 uri: hyper::Uri,
163 ) -> impl Future<Output = Result<Response<Incoming>, legacy::Error>> + '_
164 where
165 B: Default,
166 {
167 let mut request = Request::new(B::default());
168 *request.uri_mut() = uri;
169 self.request(request)
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::HyperLegacyClientInstrument;
176 use crate::{Context, instrumentations::http::test_utils::*, semconv};
177 use assert2::assert;
178 use axum::http::StatusCode;
179 use bytes::Bytes;
180 use http_body_util::Empty;
181 use hyper::{Request, header::USER_AGENT};
182 use hyper_util::rt::TokioExecutor;
183 use opentelemetry::trace::{Span, SpanKind, TraceContextExt, Tracer, TracerProvider};
184 use serial_test::serial;
185 use tokio::net::TcpListener;
186 use tracing_subscriber::{Registry, layer::SubscriberExt};
187
188 #[tokio::test]
189 #[serial]
190 async fn instruments_successful_legacy_client_requests() {
191 let telemetry = configure_test_tracing();
192 let server = spawn_server().await;
193 let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
194 .build_http::<Empty<Bytes>>()
195 .instrument();
196 let request_url = format!("{}/ok?ready=true", server.base_url);
197 let response = client
198 .request(
199 Request::builder()
200 .uri(&request_url)
201 .header(USER_AGENT, "telemetry-rust-tests")
202 .body(Empty::<Bytes>::new())
203 .unwrap(),
204 )
205 .await
206 .unwrap();
207
208 assert!(response.status() == StatusCode::OK);
209
210 let spans = force_flush_and_get_spans(&telemetry);
211 let span = find_span(&spans, "GET");
212 let traceparent = server.state.traceparent_for("/ok").unwrap();
213 let (trace_id, span_id) = traceparent_ids(&traceparent);
214
215 assert!(span.span_kind == SpanKind::Client);
216 assert!(span.span_context.trace_id().to_string() == trace_id);
217 assert!(span.span_context.span_id().to_string() == span_id);
218 assert!(matches!(span.status, opentelemetry::trace::Status::Unset));
219 assert!(string_attr(span, semconv::HTTP_REQUEST_METHOD) == Some("GET"));
220 assert!(string_attr(span, semconv::URL_SCHEME) == Some("http"));
221 assert!(string_attr(span, semconv::SERVER_ADDRESS) == Some("127.0.0.1"));
222 assert!(
223 i64_attr(span, semconv::SERVER_PORT) == Some(i64::from(server.addr.port()))
224 );
225 assert!(string_attr(span, semconv::URL_PATH) == Some("/ok"));
226 assert!(string_attr(span, semconv::URL_QUERY) == Some("ready=true"));
227 assert!(
228 string_attr(span, semconv::USER_AGENT_ORIGINAL)
229 == Some("telemetry-rust-tests")
230 );
231 assert!(string_attr(span, semconv::URL_FULL) == Some(request_url.as_str()));
232 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE) == Some(200));
233 assert!(string_attr(span, semconv::NETWORK_PROTOCOL_VERSION).is_some());
234 }
235
236 #[tokio::test]
237 #[serial]
238 async fn legacy_client_uses_explicit_parent_context_when_provided() {
239 let telemetry = configure_test_tracing();
240 let server = spawn_server().await;
241 let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
242 .build_http::<Empty<Bytes>>();
243 let tracer = telemetry.provider.tracer("hyper-legacy-tests");
244 let explicit_parent = tracer.start("explicit-parent");
245 let explicit_parent_span_id = explicit_parent.span_context().span_id();
246 let explicit_parent_cx = Context::current_with_span(explicit_parent);
247 let tracing_tracer = telemetry.provider.tracer("tracing-tests");
248 let subscriber = Registry::default()
249 .with(tracing_opentelemetry::layer().with_tracer(tracing_tracer));
250 let guard = tracing::subscriber::set_default(subscriber);
251 let current_parent = tracing::info_span!("current-parent");
252
253 tracing::Instrument::instrument(
254 async {
255 let client = client.instrument().context(&explicit_parent_cx);
256 client
257 .request(
258 Request::builder()
259 .uri(format!("{}/ok", server.base_url))
260 .body(Empty::<Bytes>::new())
261 .unwrap(),
262 )
263 .await
264 .unwrap();
265 },
266 current_parent,
267 )
268 .await;
269
270 drop(guard);
271 explicit_parent_cx.span().end();
272
273 let spans = force_flush_and_get_spans(&telemetry);
274 let hyper_span = find_span(&spans, "GET");
275 let current_span = find_span(&spans, "current-parent");
276
277 assert!(hyper_span.parent_span_id == explicit_parent_span_id);
278 assert!(hyper_span.parent_span_id != current_span.span_context.span_id());
279 }
280
281 #[tokio::test]
282 #[serial]
283 async fn legacy_client_marks_error_responses_as_errors() {
284 let telemetry = configure_test_tracing();
285 let server = spawn_server().await;
286 let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
287 .build_http::<Empty<Bytes>>()
288 .instrument();
289 let response = client
290 .request(
291 Request::builder()
292 .uri(format!("{}/not-found", server.base_url))
293 .body(Empty::<Bytes>::new())
294 .unwrap(),
295 )
296 .await
297 .unwrap();
298
299 assert!(response.status() == StatusCode::NOT_FOUND);
300
301 let spans = force_flush_and_get_spans(&telemetry);
302 let span = find_span(&spans, "GET");
303
304 assert!(matches!(
305 span.status,
306 opentelemetry::trace::Status::Error { .. }
307 ));
308 assert!(i64_attr(span, semconv::HTTP_RESPONSE_STATUS_CODE) == Some(404));
309 assert!(string_attr(span, semconv::ERROR_TYPE) == Some("404"));
310 }
311
312 #[tokio::test]
313 #[serial]
314 async fn legacy_client_marks_transport_errors_as_errors() {
315 let telemetry = configure_test_tracing();
316 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
317 let addr = listener.local_addr().unwrap();
318 drop(listener);
319 let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
320 .build_http::<Empty<Bytes>>()
321 .instrument();
322 let result = client
323 .request(
324 Request::builder()
325 .uri(format!("http://{addr}/unreachable"))
326 .body(Empty::<Bytes>::new())
327 .unwrap(),
328 )
329 .await;
330
331 assert!(result.is_err());
332
333 let spans = force_flush_and_get_spans(&telemetry);
334 let span = find_span(&spans, "GET");
335
336 assert!(matches!(
337 span.status,
338 opentelemetry::trace::Status::Error { .. }
339 ));
340 assert!(string_attr(span, semconv::ERROR_TYPE) == Some("connect"));
341 }
342}