use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use tracing::{
Event, Subscriber,
field::{Field, Visit},
};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
#[derive(Default)]
pub(crate) struct HttpRequestStats {
pub requests: Vec<HttpRequest>,
}
impl HttpRequestStats {
pub fn summarize_by_url_method(&self) -> impl IntoIterator<Item = (String, String, u128)> + '_ {
let mut timings: BTreeMap<String, BTreeMap<String, u128>> = BTreeMap::new();
for rec in &self.requests {
let url: String = rec
.url
.get(0..rec.url.find('?').unwrap_or(rec.url.len()))
.unwrap_or(&rec.url)
.to_string();
timings
.entry(url)
.and_modify(|x| {
x.entry(rec.method.clone())
.and_modify(|t| *t = t.wrapping_add(rec.duration))
.or_insert(rec.duration);
})
.or_insert(BTreeMap::from([(rec.method.clone(), rec.duration)]));
}
timings
.into_iter()
.flat_map(move |(u, v)| v.into_iter().map(move |(m, d)| (u.clone(), m, d)))
}
}
pub(crate) struct RequestTracingCollector {
pub stats: Arc<Mutex<HttpRequestStats>>,
}
#[derive(Debug, Default)]
pub(crate) struct HttpRequest {
pub url: String,
pub method: String,
pub duration: u128,
pub status: u16,
pub request_id: Option<String>,
}
impl Visit for HttpRequest {
fn record_u64(&mut self, field: &Field, value: u64) {
if field.name() == "status" {
self.status = value as u16;
}
}
fn record_u128(&mut self, field: &Field, value: u128) {
if field.name() == "duration_ms" {
self.duration = value;
}
}
fn record_str(&mut self, field: &Field, value: &str) {
match field.name() {
"url" => self.url = String::from(value),
"method" => self.method = String::from(value),
"request_id" => self.request_id = Some(String::from(value)),
_ => {}
};
}
fn record_debug(&mut self, _: &Field, _: &dyn core::fmt::Debug) {}
}
impl<C> Layer<C> for RequestTracingCollector
where
C: Subscriber + Send + Sync + 'static,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, C>) {
let fields = event.metadata().fields();
if event.metadata().name() == "http_request"
&& fields.field("url").is_some()
&& fields.field("duration_ms").is_some()
{
let mut record = HttpRequest::default();
event.record(&mut record);
if let Ok(mut lock) = self.stats.lock() {
lock.requests.push(record);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_summarize() {
let records = vec![
HttpRequest {
url: String::from("http://foo.bar/"),
method: String::from("get"),
duration: 1,
status: 200,
request_id: None,
},
HttpRequest {
url: String::from("http://foo.bar/1?foo=bar"),
method: String::from("get"),
duration: 2,
status: 200,
request_id: None,
},
HttpRequest {
url: String::from("http://foo.bar/1?foo=bar"),
method: String::from("get"),
duration: 3,
status: 200,
request_id: None,
},
HttpRequest {
url: String::from("http://foo.bar/1?foo=baz"),
method: String::from("get"),
duration: 4,
status: 200,
request_id: None,
},
HttpRequest {
url: String::from("http://foo.bar/"),
method: String::from("post"),
duration: 5,
status: 200,
request_id: None,
},
];
let r = HttpRequestStats { requests: records };
let summaries: Vec<(String, String, u128)> =
r.summarize_by_url_method().into_iter().collect();
assert!(
summaries
.iter()
.any(|x| *x == (String::from("http://foo.bar/"), String::from("get"), 1))
);
assert!(
summaries
.iter()
.any(|x| *x == (String::from("http://foo.bar/1"), String::from("get"), 9))
);
assert!(
summaries
.iter()
.any(|x| *x == (String::from("http://foo.bar/"), String::from("post"), 5))
);
}
}