openstack_cli_core/
tracing_stats.rs1use std::collections::BTreeMap;
19use std::sync::{Arc, Mutex};
20use tracing::{
21 Event, Subscriber,
22 field::{Field, Visit},
23};
24use tracing_subscriber::Layer;
25use tracing_subscriber::layer::Context;
26
27#[derive(Default)]
29pub struct HttpRequestStats {
30 requests: Vec<HttpRequest>,
31}
32
33impl HttpRequestStats {
34 pub fn summarize_by_url_method(&self) -> impl IntoIterator<Item = (String, String, u128)> + '_ {
36 let mut timings: BTreeMap<String, BTreeMap<String, u128>> = BTreeMap::new();
37 for rec in &self.requests {
38 let url: String = rec
39 .url
40 .get(0..rec.url.find('?').unwrap_or(rec.url.len()))
41 .unwrap_or(&rec.url)
42 .to_string();
43 timings
44 .entry(url)
45 .and_modify(|x| {
46 x.entry(rec.method.clone())
47 .and_modify(|t| *t = t.wrapping_add(rec.duration))
48 .or_insert(rec.duration);
49 })
50 .or_insert(BTreeMap::from([(rec.method.clone(), rec.duration)]));
51 }
52 timings
53 .into_iter()
54 .flat_map(move |(u, v)| v.into_iter().map(move |(m, d)| (u.clone(), m, d)))
55 }
56}
57
58pub struct RequestTracingCollector {
63 pub stats: Arc<Mutex<HttpRequestStats>>,
64}
65
66#[derive(Debug, Default)]
68struct HttpRequest {
69 pub url: String,
70 pub method: String,
71 pub duration: u128,
72 pub status: u16,
73 pub request_id: Option<String>,
74}
75
76impl Visit for HttpRequest {
77 fn record_u64(&mut self, field: &Field, value: u64) {
78 if field.name() == "status" {
79 self.status = value as u16;
80 }
81 }
82
83 fn record_u128(&mut self, field: &Field, value: u128) {
84 if field.name() == "duration_ms" {
85 self.duration = value;
86 }
87 }
88
89 fn record_str(&mut self, field: &Field, value: &str) {
90 match field.name() {
91 "url" => self.url = String::from(value),
92 "method" => self.method = String::from(value),
93 "request_id" => self.request_id = Some(String::from(value)),
94 _ => {}
95 };
96 }
97 fn record_debug(&mut self, _: &Field, _: &dyn core::fmt::Debug) {}
98}
99
100impl<C> Layer<C> for RequestTracingCollector
101where
102 C: Subscriber + Send + Sync + 'static,
103{
104 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, C>) {
106 let fields = event.metadata().fields();
107 if event.metadata().name() == "http_request"
108 && fields.field("url").is_some()
109 && fields.field("duration_ms").is_some()
110 {
111 let mut record = HttpRequest::default();
112 event.record(&mut record);
113 if let Ok(mut lock) = self.stats.lock() {
114 lock.requests.push(record);
115 }
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_summarize() {
126 let records = vec![
127 HttpRequest {
128 url: String::from("http://foo.bar/"),
129 method: String::from("get"),
130 duration: 1,
131 status: 200,
132 request_id: None,
133 },
134 HttpRequest {
135 url: String::from("http://foo.bar/1?foo=bar"),
136 method: String::from("get"),
137 duration: 2,
138 status: 200,
139 request_id: None,
140 },
141 HttpRequest {
142 url: String::from("http://foo.bar/1?foo=bar"),
143 method: String::from("get"),
144 duration: 3,
145 status: 200,
146 request_id: None,
147 },
148 HttpRequest {
149 url: String::from("http://foo.bar/1?foo=baz"),
150 method: String::from("get"),
151 duration: 4,
152 status: 200,
153 request_id: None,
154 },
155 HttpRequest {
156 url: String::from("http://foo.bar/"),
157 method: String::from("post"),
158 duration: 5,
159 status: 200,
160 request_id: None,
161 },
162 ];
163 let r = HttpRequestStats { requests: records };
164
165 let summaries: Vec<(String, String, u128)> =
166 r.summarize_by_url_method().into_iter().collect();
167
168 assert!(
169 summaries
170 .iter()
171 .any(|x| *x == (String::from("http://foo.bar/"), String::from("get"), 1))
172 );
173 assert!(
174 summaries
175 .iter()
176 .any(|x| *x == (String::from("http://foo.bar/1"), String::from("get"), 9))
177 );
178 assert!(
179 summaries
180 .iter()
181 .any(|x| *x == (String::from("http://foo.bar/"), String::from("post"), 5))
182 );
183 }
184}