Skip to main content

openstack_cli_core/
tracing_stats.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14//! `tracing` utilities
15//!
16//! The module provides mechanics for capturing HTTP requests to output timing statistics when
17//! requested by user.
18use 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/// HTTP Request statistics container
28#[derive(Default)]
29pub struct HttpRequestStats {
30    requests: Vec<HttpRequest>,
31}
32
33impl HttpRequestStats {
34    /// Summarize captured requests by url without query parameters and method
35    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
58/// Tracing collector capturing HTTP request metrics
59///
60/// Added as a `tracing` layer it captures all events with name "request" and mandatory fields: [url,
61/// duration_ms, method] (additional optional fields: [status, request_id]
62pub struct RequestTracingCollector {
63    pub stats: Arc<Mutex<HttpRequestStats>>,
64}
65
66/// Single HTTP request profile record
67#[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    /// Notifies this layer that an event has occurred.
105    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}