konduto/sdk/metrics/
middleware.rs

1//! Middleware para instrumentação automática de requisições HTTP com StatsD
2
3use cadence::StatsdClient;
4use cadence::prelude::*;
5use http::Extensions;
6use reqwest::{Request, Response};
7use reqwest_middleware::{Middleware, Next, Result as MiddlewareResult};
8use std::sync::Arc;
9use std::time::Instant;
10
11/// Middleware que envia métricas para StatsD
12pub struct StatsdMiddleware {
13    client: Arc<StatsdClient>,
14}
15
16impl StatsdMiddleware {
17    /// Cria um novo middleware com o cliente StatsD
18    pub fn new(client: StatsdClient) -> Self {
19        Self {
20            client: Arc::new(client),
21        }
22    }
23
24    /// Normaliza o path removendo IDs e valores dinâmicos
25    /// Exemplo: /v1/orders/ORDER123 -> /v1/orders/:id
26    fn normalize_path(path: &str) -> String {
27        let segments: Vec<&str> = path.split('/').collect();
28        let normalized: Vec<String> = segments
29            .iter()
30            .map(|segment| {
31                // Identifica segmentos dinâmicos comuns
32                if segment.is_empty() {
33                    return String::from("");
34                }
35
36                // Versões da API (v1, v2, etc.)
37                if segment.starts_with('v') && segment.len() == 2 {
38                    return segment.to_string();
39                }
40
41                // Endpoints conhecidos
42                match *segment {
43                    "orders" | "blacklist" | "whitelist" | "greylist" | "email" | "ip"
44                    | "tax_id" | "status" => segment.to_string(),
45                    // Qualquer outro segmento é considerado dinâmico
46                    _ => ":id".to_string(),
47                }
48            })
49            .collect();
50
51        normalized.join("/")
52    }
53
54    /// Categoriza a latência em buckets
55    fn latency_bucket(duration_ms: u64) -> &'static str {
56        match duration_ms {
57            0..=100 => "fast",
58            101..=500 => "medium",
59            501..=1000 => "slow",
60            _ => "very_slow",
61        }
62    }
63}
64
65#[async_trait::async_trait]
66impl Middleware for StatsdMiddleware {
67    async fn handle(
68        &self,
69        req: Request,
70        extensions: &mut Extensions,
71        next: Next<'_>,
72    ) -> MiddlewareResult<Response> {
73        let start = Instant::now();
74        let method = req.method().to_string();
75        let path = req.url().path().to_string();
76        let normalized_path = Self::normalize_path(&path);
77
78        // Executa a requisição
79        let result = next.run(req, extensions).await;
80
81        // Calcula a latência
82        let duration = start.elapsed();
83        let duration_ms = duration.as_millis() as u64;
84
85        match &result {
86            Ok(response) => {
87                let status = response.status().as_u16();
88                let status_class = format!("{}xx", status / 100);
89
90                // Envia métricas
91                // 1. Hit counter
92                let _ = self
93                    .client
94                    .count_with_tags("konduto.requests.hits", 1)
95                    .with_tag("method", &method)
96                    .with_tag("endpoint", &normalized_path)
97                    .with_tag("status", &status.to_string())
98                    .with_tag("status_class", &status_class)
99                    .send();
100                println!("metrica requets.hts send");
101                // 2. Latência (timer)
102                let _ = self
103                    .client
104                    .time_with_tags("konduto.requests.latency", duration_ms)
105                    .with_tag("method", &method)
106                    .with_tag("endpoint", &normalized_path)
107                    .with_tag("latency_bucket", Self::latency_bucket(duration_ms))
108                    .send();
109
110                // 3. Status code counter
111                let _ = self
112                    .client
113                    .count_with_tags("konduto.requests.status", 1)
114                    .with_tag("status", &status.to_string())
115                    .with_tag("status_class", &status_class)
116                    .send();
117            }
118            Err(_error) => {
119                // Envia métricas de erro
120                let _ = self
121                    .client
122                    .count_with_tags("konduto.requests.errors", 1)
123                    .with_tag("method", &method)
124                    .with_tag("endpoint", &normalized_path)
125                    .with_tag("error_type", "network_error")
126                    .send();
127
128                // Ainda registra latência mesmo em erro
129                let _ = self
130                    .client
131                    .time_with_tags("konduto.requests.latency", duration_ms)
132                    .with_tag("method", &method)
133                    .with_tag("endpoint", &normalized_path)
134                    .with_tag("latency_bucket", Self::latency_bucket(duration_ms))
135                    .with_tag("error", "true")
136                    .send();
137            }
138        }
139
140        result
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_normalize_path_orders() {
150        assert_eq!(
151            StatsdMiddleware::normalize_path("/v1/orders/ORDER123"),
152            "/v1/orders/:id"
153        );
154    }
155
156    #[test]
157    fn test_normalize_path_decision_list() {
158        assert_eq!(
159            StatsdMiddleware::normalize_path("/v1/blacklist/email/test@example.com"),
160            "/v1/blacklist/email/:id"
161        );
162    }
163
164    #[test]
165    fn test_normalize_path_simple() {
166        assert_eq!(StatsdMiddleware::normalize_path("/v1/orders"), "/v1/orders");
167    }
168
169    #[test]
170    fn test_normalize_path_with_status() {
171        assert_eq!(
172            StatsdMiddleware::normalize_path("/v1/orders/ORDER123/status"),
173            "/v1/orders/:id/status"
174        );
175    }
176
177    #[test]
178    fn test_latency_bucket_fast() {
179        assert_eq!(StatsdMiddleware::latency_bucket(50), "fast");
180        assert_eq!(StatsdMiddleware::latency_bucket(100), "fast");
181    }
182
183    #[test]
184    fn test_latency_bucket_medium() {
185        assert_eq!(StatsdMiddleware::latency_bucket(101), "medium");
186        assert_eq!(StatsdMiddleware::latency_bucket(500), "medium");
187    }
188
189    #[test]
190    fn test_latency_bucket_slow() {
191        assert_eq!(StatsdMiddleware::latency_bucket(501), "slow");
192        assert_eq!(StatsdMiddleware::latency_bucket(1000), "slow");
193    }
194
195    #[test]
196    fn test_latency_bucket_very_slow() {
197        assert_eq!(StatsdMiddleware::latency_bucket(1001), "very_slow");
198        assert_eq!(StatsdMiddleware::latency_bucket(5000), "very_slow");
199    }
200}