1use crate::http::otel;
7use tracing::Level;
8
9#[derive(Clone)]
12pub struct TracedClient {
13 inner: reqwest::Client,
14}
15
16impl TracedClient {
17 #[must_use]
19 pub fn new(inner: reqwest::Client) -> Self {
20 Self { inner }
21 }
22
23 pub async fn execute(&self, req: reqwest::Request) -> reqwest::Result<reqwest::Response> {
29 let url = req.url().clone();
30 let method = req.method().clone();
31
32 let span = tracing::span!(
33 Level::INFO, "outgoing_http",
34 http.method = %method,
35 http.url = %url,
36 otel.kind = "client",
37 );
38 let _g = span.enter();
39
40 let req = {
42 let mut req = req.try_clone().unwrap_or(req);
43 otel::inject_current_span(req.headers_mut());
44 req
45 };
46
47 let response = self.inner.execute(req).await?;
48
49 span.record("http.status_code", response.status().as_u16());
51 if response.status().is_client_error() || response.status().is_server_error() {
52 span.record("error", true);
53 }
54
55 Ok(response)
56 }
57
58 pub async fn get(&self, url: &str) -> reqwest::Result<reqwest::Response> {
63 let req = self.inner.get(url).build()?;
64 self.execute(req).await
65 }
66
67 pub async fn post(&self, url: &str) -> reqwest::Result<reqwest::Response> {
72 let req = self.inner.post(url).build()?;
73 self.execute(req).await
74 }
75
76 pub async fn put(&self, url: &str) -> reqwest::Result<reqwest::Response> {
81 let req = self.inner.put(url).build()?;
82 self.execute(req).await
83 }
84
85 pub async fn patch(&self, url: &str) -> reqwest::Result<reqwest::Response> {
90 let req = self.inner.patch(url).build()?;
91 self.execute(req).await
92 }
93
94 pub async fn delete(&self, url: &str) -> reqwest::Result<reqwest::Response> {
99 let req = self.inner.delete(url).build()?;
100 self.execute(req).await
101 }
102
103 #[must_use]
105 pub fn inner(&self) -> &reqwest::Client {
106 &self.inner
107 }
108
109 pub fn request(&self, method: reqwest::Method, url: &str) -> reqwest::RequestBuilder {
111 self.inner.request(method, url)
112 }
113}
114
115impl From<reqwest::Client> for TracedClient {
116 fn from(c: reqwest::Client) -> Self {
117 Self::new(c)
118 }
119}
120
121impl Default for TracedClient {
122 fn default() -> Self {
123 Self::new(reqwest::Client::new())
124 }
125}
126
127#[cfg(test)]
128#[cfg_attr(coverage_nightly, coverage(off))]
129mod tests {
130 use super::*;
131 use httpmock::prelude::*;
132
133 #[tokio::test]
134 async fn test_traced_client_basic_functionality() {
135 let client = TracedClient::default();
136
137 assert!(client.inner().get("https://example.com").build().is_ok());
140 }
141
142 #[tokio::test]
143 async fn test_traced_client_executes_request() {
144 let server = MockServer::start();
146 let _m = server.mock(|when, then| {
147 when.method(GET).path("/ping");
148 then.status(200).body("ok");
149 });
150
151 let client = TracedClient::from(reqwest::Client::new());
152 let url = format!("{}/ping", server.base_url());
153 let resp = client.get(&url).await.unwrap();
154
155 assert!(resp.status().is_success());
156 }
159
160 #[tokio::test]
161 async fn test_all_http_methods() {
162 let client = TracedClient::default();
163
164 let url = "http://example.com/test";
166
167 let _ = client.get(url).await;
169 let _ = client.post(url).await;
170 let _ = client.put(url).await;
171 let _ = client.patch(url).await;
172 let _ = client.delete(url).await;
173 }
174}