warp_prometheus/
lib.rs

1use log::trace;
2use prometheus::HistogramOpts;
3use prometheus::{HistogramVec, Registry};
4
5#[derive(Debug, Clone)]
6pub struct Metrics {
7    http_timer: HistogramVec,
8    include_path_labels: Vec<String>,
9}
10
11impl Metrics {
12    pub fn new(cr: &Registry, include_path_labels: &Vec<String>) -> Self {
13        let internal_http_timer_opts = HistogramOpts::new(
14            "server_response_duration_seconds",
15            "Route response time in seconds.",
16        );
17        let internal_http_timer =
18            HistogramVec::new(internal_http_timer_opts, &["classifier", "status"]).unwrap();
19        cr.register(Box::new(internal_http_timer.clone())).unwrap();
20
21        Self {
22            http_timer: internal_http_timer,
23            include_path_labels: include_path_labels.clone(),
24        }
25    }
26
27    fn sanitize_path_segments(&self, path: &str) -> String {
28        let path_segments: Vec<&str> = path.split('/').collect();
29        path_segments.iter().fold(String::new(), |acc, &path| {
30            if self.include_path_labels.contains(&path.to_string()) {
31                format!("{}/{}", acc, path)
32            } else if path == "" {
33                acc.to_string()
34            } else {
35                format!("{}/*", acc)
36            }
37        })
38    }
39
40    /// Get prometheus metrics per-route and how long each route takes.
41    /// ```
42    /// use prometheus::Registry;
43    /// use warp::Filter;
44    /// use warp_prometheus::Metrics;
45    ///
46    ///
47    /// let registry: &Registry = prometheus::default_registry();
48    /// let path_includes: Vec<String> = vec![String::from("hello")];
49    ///
50    /// let route_one = warp::path("hello")
51    ///    .and(warp::path::param())
52    ///    .and(warp::header("user-agent"))
53    ///    .map(|param: String, agent: String| {
54    ///        format!("Hello {}, whose agent is {}", param, agent)
55    ///    });
56    /// let metrics = Metrics::new(&registry, &path_includes);
57    ///
58    /// let test_routes = route_one.with(warp::log::custom(move |log| {
59    ///            metrics.http_metrics(log)
60    ///        }));
61    ///
62    /// ```
63    pub fn http_metrics(&self, info: warp::log::Info) {
64        trace!(
65            "Metric Status: {} - Method: {} - Path: {}",
66            &info.status().as_u16().to_string(),
67            &info.method(),
68            &info.path()
69        );
70        let sanitized_classifier = format!(
71            "{} - {}",
72            info.method(),
73            self.sanitize_path_segments(info.path())
74        );
75        self.http_timer
76            .with_label_values(&[&sanitized_classifier, info.status().as_str()])
77            .observe(info.elapsed().as_secs_f64());
78
79    }
80}
81
82#[cfg(test)]
83mod test {
84
85    use super::*;
86
87    #[test]
88    fn test_sanitize_path() {
89        let registry: Registry = Registry::new();
90        let path_includes: Vec<String> = vec![String::from("users"), String::from("registration")];
91
92        let metrics = Metrics::new(&registry, &path_includes);
93        let path = "/users/12345/registration/9797731279";
94        let sanitized_path = metrics.sanitize_path_segments(path);
95
96        assert_eq!("/users/*/registration/*".to_string(), sanitized_path)
97    }
98
99    #[test]
100    fn test_sanitize_path_with_value_first() {
101        let registry: Registry = Registry::new();
102        let path_includes: Vec<String> = vec![String::from("users"), String::from("registration")];
103
104        let metrics = Metrics::new(&registry, &path_includes);
105        let path = "12344235/users/12345/12314151252/registration";
106        let sanitized_path = metrics.sanitize_path_segments(path);
107
108        assert_eq!("/*/users/*/*/registration".to_string(), sanitized_path)
109    }
110
111    #[test]
112    fn test_sanitize_path_with_multiple_segments_in_order() {
113        let registry: Registry = Registry::new();
114        let path_includes: Vec<String> = vec![String::from("users"), String::from("registration")];
115
116        let metrics = Metrics::new(&registry, &path_includes);
117        let path = "/users/12345/12314151252/registration";
118        let sanitized_path = metrics.sanitize_path_segments(path);
119
120        assert_eq!("/users/*/*/registration".to_string(), sanitized_path)
121    }
122
123    #[test]
124    fn test_totally_wrong_path() {
125
126        let registry: Registry = Registry::new();
127        let path_includes: Vec<String> = vec![String::from("users"), String::from("registration")];
128
129        let metrics = Metrics::new(&registry, &path_includes);
130        let path = "12344235/12141242/12345/12314151252/235235235";
131        let sanitized_path = metrics.sanitize_path_segments(path);
132
133        assert_eq!("/*/*/*/*/*".to_string(), sanitized_path)
134    }
135}