1#![deny(missing_docs)]
60
61use std::{borrow::Cow, time::Instant};
62
63use http::{Extensions, Method};
64use metrics::{describe_histogram, histogram, Unit};
65use reqwest_middleware::{
66 reqwest::{Request, Response},
67 Error, Middleware, Next, Result,
68};
69
70const HTTP_CLIENT_REQUEST_DURATION: &str = "http.client.request.duration";
73const HTTP_CLIENT_REQUEST_BODY_SIZE: &str = "http.client.request.body.size";
74const HTTP_CLIENT_RESPONSE_BODY_SIZE: &str = "http.client.response.body.size";
75const HTTP_REQUEST_METHOD: &str = "http.request.method";
77const SERVER_ADDRESS: &str = "server.address";
78const SERVER_PORT: &str = "server.port";
79const ERROR_TYPE: &str = "error.type";
80const HTTP_RESPONSE_STATUS_CODE: &str = "http.response.status_code";
81const NETWORK_PROTOCOL_NAME: &str = "network.protocol.name";
82const NETWORK_PROTOCOL_VERSION: &str = "network.protocol.version";
83const URL_SCHEME: &str = "url.scheme";
84
85#[derive(Debug, Clone)]
88pub struct MetricsMiddleware {
89 label_names: LabelNames,
90}
91
92impl MetricsMiddleware {
93 pub fn new() -> Self {
95 Self::new_inner(LabelNames::default())
96 }
97
98 fn new_inner(label_names: LabelNames) -> Self {
99 describe_histogram!(
100 HTTP_CLIENT_REQUEST_DURATION,
101 Unit::Seconds,
102 "Duration of HTTP client requests."
103 );
104 describe_histogram!(
105 HTTP_CLIENT_REQUEST_BODY_SIZE,
106 Unit::Bytes,
107 "Size of HTTP client request bodies."
108 );
109 describe_histogram!(
110 HTTP_CLIENT_RESPONSE_BODY_SIZE,
111 Unit::Bytes,
112 "Size of HTTP client response bodies."
113 );
114 Self { label_names }
115 }
116
117 pub fn builder() -> MetricsMiddlewareBuilder {
119 MetricsMiddlewareBuilder::new()
120 }
121}
122
123#[derive(Debug, Clone)]
124struct LabelNames {
125 http_request_method: String,
126 server_address: String,
127 server_port: String,
128 error_type: String,
129 http_response_status: String,
130 network_protocol_name: String,
131 network_protocol_version: String,
132 url_scheme: String,
133}
134
135impl Default for LabelNames {
136 fn default() -> Self {
137 Self {
138 http_request_method: HTTP_REQUEST_METHOD.to_string(),
139 server_address: SERVER_ADDRESS.to_string(),
140 server_port: SERVER_PORT.to_string(),
141 error_type: ERROR_TYPE.to_string(),
142 http_response_status: HTTP_RESPONSE_STATUS_CODE.to_string(),
143 network_protocol_name: NETWORK_PROTOCOL_NAME.to_string(),
144 network_protocol_version: NETWORK_PROTOCOL_VERSION.to_string(),
145 url_scheme: URL_SCHEME.to_string(),
146 }
147 }
148}
149
150impl Default for MetricsMiddleware {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct MetricsMiddlewareBuilder {
159 label_names: LabelNames,
160}
161
162macro_rules! label_setters {
163 (
165 $(
166 $(#[$attr:meta])*
168 $method_name:ident, $field_name:ident
169 );+
170 $(;)?
171 ) => {
172 $(
173 $(#[$attr])*
174 pub fn $method_name<T: Into<String>>(&mut self, label: T) -> &mut Self {
175 self.label_names.$field_name = label.into();
176 self
177 }
178 )+
179 };
180}
181impl MetricsMiddlewareBuilder {
182 pub fn new() -> Self {
184 Self {
185 label_names: LabelNames::default(),
186 }
187 }
188
189 label_setters! {
190 http_request_method_label, http_request_method;
192 server_address_label, server_address;
194 server_port_label, server_port;
196 error_type_label, error_type;
198 http_response_status_label, http_response_status;
200 network_protocol_name_label, network_protocol_name;
202 network_protocol_version_label, network_protocol_name;
204 url_scheme_label, url_scheme
206 }
207
208 pub fn build(&self) -> MetricsMiddleware {
210 MetricsMiddleware::new_inner(self.label_names.clone())
211 }
212}
213
214impl Default for MetricsMiddlewareBuilder {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
221#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
222impl Middleware for MetricsMiddleware {
223 async fn handle(
224 &self,
225 req: Request,
226 extensions: &mut Extensions,
227 next: Next<'_>,
228 ) -> Result<Response> {
229 let http_request_method = http_request_method(&req);
230 let url_scheme = url_scheme(&req);
231 let server_address = server_address(&req);
232 let server_port = server_port(&req);
233 let network_protocol_version = network_protocol_version(&req);
234 let request_body_size = req
235 .body()
236 .and_then(|body| body.as_bytes())
237 .map(|bytes| bytes.len())
238 .unwrap_or(0);
239
240 let start = Instant::now();
241 let res = next.run(req, extensions).await;
242 let duration = start.elapsed();
243
244 let mut labels = vec![
245 (
246 self.label_names.http_request_method.to_string(),
247 http_request_method,
248 ),
249 (self.label_names.url_scheme.to_string(), url_scheme),
250 (
251 self.label_names.network_protocol_name.to_string(),
252 Cow::Borrowed("http"),
253 ),
254 ];
255
256 if let Some(server_address) = server_address {
257 labels.push((
258 self.label_names.server_address.to_string(),
259 Cow::Owned(server_address),
260 ));
261 }
262
263 if let Some(port) = server_port {
264 labels.push((
265 self.label_names.server_port.to_string(),
266 Cow::Owned(port.to_string()),
267 ));
268 }
269
270 if let Some(network_protocol_version) = network_protocol_version {
271 labels.push((
272 self.label_names.network_protocol_version.to_string(),
273 Cow::Borrowed(network_protocol_version),
274 ));
275 }
276
277 if let Some(status) = http_response_status(&res) {
278 labels.push((self.label_names.http_response_status.to_string(), status));
279 }
280
281 if let Some(error) = error_type(&res) {
282 labels.push((self.label_names.error_type.to_string(), error));
283 }
284
285 histogram!(HTTP_CLIENT_REQUEST_DURATION, &labels)
286 .record(duration.as_millis() as f64 / 1000.0);
287
288 histogram!(HTTP_CLIENT_REQUEST_BODY_SIZE, &labels).record(request_body_size as f64);
289
290 let response_body_size = res
294 .as_ref()
295 .ok()
296 .and_then(|res| res.content_length())
297 .unwrap_or(0);
298 histogram!(HTTP_CLIENT_RESPONSE_BODY_SIZE, &labels).record(response_body_size as f64);
299
300 res
301 }
302}
303
304fn http_request_method(req: &Request) -> Cow<'static, str> {
305 match req.method() {
306 &Method::GET => Cow::Borrowed("GET"),
307 &Method::POST => Cow::Borrowed("POST"),
308 &Method::PUT => Cow::Borrowed("PUT"),
309 &Method::DELETE => Cow::Borrowed("DELETE"),
310 &Method::HEAD => Cow::Borrowed("HEAD"),
311 &Method::OPTIONS => Cow::Borrowed("OPTIONS"),
312 &Method::CONNECT => Cow::Borrowed("CONNECT"),
313 &Method::PATCH => Cow::Borrowed("PATCH"),
314 &Method::TRACE => Cow::Borrowed("TRACE"),
315 method => Cow::Owned(method.as_str().to_string()),
316 }
317}
318
319fn url_scheme(req: &Request) -> Cow<'static, str> {
320 match req.url().scheme() {
321 "http" => Cow::Borrowed("http"),
322 "https" => Cow::Borrowed("https"),
323 s => Cow::Owned(s.to_string()),
324 }
325}
326
327fn server_address(req: &Request) -> Option<String> {
328 req.url().host().map(|h| h.to_string())
329}
330
331fn server_port(req: &Request) -> Option<u16> {
332 req.url().port_or_known_default()
333}
334
335fn http_response_status(res: &Result<Response>) -> Option<Cow<'static, str>> {
336 res.as_ref()
337 .map(|r| Cow::Owned(r.status().as_u16().to_string()))
338 .ok()
339}
340
341fn error_type(res: &Result<Response>) -> Option<Cow<'static, str>> {
342 Some(match res {
343 Ok(res) if res.status().is_client_error() || res.status().is_server_error() => {
344 Cow::Owned(res.status().as_str().to_string())
345 }
346 Err(Error::Middleware(err)) => Cow::Owned(format!("{err}")),
347 Err(Error::Reqwest(err)) => Cow::Owned(format!("{err}")),
348 _ => return None,
349 })
350}
351
352#[cfg(target_arch = "wasm32")]
353fn network_protocol_version(_req: &Request) -> Option<&'static str> {
354 None
355}
356
357#[cfg(not(target_arch = "wasm32"))]
358fn network_protocol_version(req: &Request) -> Option<&'static str> {
359 let version = req.version();
360
361 Some(match version {
362 http::Version::HTTP_09 => "0.9",
363 http::Version::HTTP_10 => "1.0",
364 http::Version::HTTP_11 => "1.1",
365 http::Version::HTTP_2 => "2",
366 http::Version::HTTP_3 => "3",
367 _ => return None,
368 })
369}