Skip to main content

armature_analytics/
insights.rs

1//! Analytics insights and alerting
2
3use crate::{AnalyticsSnapshot, EndpointMetrics, LatencyMetrics, RequestMetrics};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7/// Types of insights
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum InsightType {
11    HighErrorRate,
12    HighLatency,
13    RateLimitPressure,
14    TrafficSpike,
15    SlowEndpoint,
16    ErrorSpike,
17}
18
19/// Severity levels
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Severity {
23    Info,
24    Warning,
25    Critical,
26}
27
28/// An analytics insight
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Insight {
31    pub insight_type: InsightType,
32    pub severity: Severity,
33    pub title: String,
34    pub description: String,
35    pub value: f64,
36    pub threshold: f64,
37    pub timestamp: DateTime<Utc>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub endpoint: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub recommendation: Option<String>,
42}
43
44impl Insight {
45    pub fn new(
46        insight_type: InsightType,
47        severity: Severity,
48        title: impl Into<String>,
49        description: impl Into<String>,
50    ) -> Self {
51        Self {
52            insight_type,
53            severity,
54            title: title.into(),
55            description: description.into(),
56            value: 0.0,
57            threshold: 0.0,
58            timestamp: Utc::now(),
59            endpoint: None,
60            recommendation: None,
61        }
62    }
63
64    pub fn with_value(mut self, value: f64) -> Self {
65        self.value = value;
66        self
67    }
68
69    pub fn with_threshold(mut self, threshold: f64) -> Self {
70        self.threshold = threshold;
71        self
72    }
73
74    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
75        self.endpoint = Some(endpoint.into());
76        self
77    }
78
79    pub fn with_recommendation(mut self, rec: impl Into<String>) -> Self {
80        self.recommendation = Some(rec.into());
81        self
82    }
83}
84
85/// Configuration for insight detection
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct InsightConfig {
88    /// Error rate threshold (percentage)
89    pub error_rate_warning: f64,
90    pub error_rate_critical: f64,
91
92    /// Latency thresholds (milliseconds)
93    pub p99_latency_warning_ms: f64,
94    pub p99_latency_critical_ms: f64,
95
96    /// Rate limit thresholds (percentage of requests limited)
97    pub rate_limit_warning: f64,
98    pub rate_limit_critical: f64,
99
100    /// Traffic spike threshold (multiplier over average)
101    pub traffic_spike_multiplier: f64,
102
103    /// Minimum requests before generating insights
104    pub min_requests: u64,
105}
106
107impl Default for InsightConfig {
108    fn default() -> Self {
109        Self {
110            error_rate_warning: 1.0,  // 1% error rate
111            error_rate_critical: 5.0, // 5% error rate
112            p99_latency_warning_ms: 500.0,
113            p99_latency_critical_ms: 2000.0,
114            rate_limit_warning: 10.0,  // 10% of requests limited
115            rate_limit_critical: 25.0, // 25% of requests limited
116            traffic_spike_multiplier: 3.0,
117            min_requests: 100,
118        }
119    }
120}
121
122/// Insight generator
123pub struct InsightGenerator {
124    config: InsightConfig,
125    baseline_rps: Option<f64>,
126}
127
128impl InsightGenerator {
129    pub fn new(config: InsightConfig) -> Self {
130        Self {
131            config,
132            baseline_rps: None,
133        }
134    }
135
136    /// Set baseline RPS for traffic spike detection
137    pub fn set_baseline_rps(&mut self, rps: f64) {
138        self.baseline_rps = Some(rps);
139    }
140
141    /// Generate insights from analytics snapshot
142    pub fn generate(&self, snapshot: &AnalyticsSnapshot) -> Vec<Insight> {
143        let mut insights = Vec::new();
144
145        // Skip if not enough data
146        if snapshot.requests.total < self.config.min_requests {
147            return insights;
148        }
149
150        // Check error rate
151        if let Some(insight) = self.check_error_rate(&snapshot.requests) {
152            insights.push(insight);
153        }
154
155        // Check latency
156        if let Some(insight) = self.check_latency(&snapshot.latency) {
157            insights.push(insight);
158        }
159
160        // Check rate limits
161        if let Some(insight) = self.check_rate_limits(snapshot) {
162            insights.push(insight);
163        }
164
165        // Check traffic spike
166        if let Some(insight) = self.check_traffic_spike(snapshot) {
167            insights.push(insight);
168        }
169
170        // Check slow endpoints
171        for insight in self.check_slow_endpoints(&snapshot.endpoints) {
172            insights.push(insight);
173        }
174
175        insights
176    }
177
178    fn check_error_rate(&self, requests: &RequestMetrics) -> Option<Insight> {
179        let error_rate = requests.error_rate();
180
181        if error_rate >= self.config.error_rate_critical {
182            Some(
183                Insight::new(
184                    InsightType::HighErrorRate,
185                    Severity::Critical,
186                    "Critical Error Rate",
187                    format!("Error rate is {:.2}%, above critical threshold of {:.2}%",
188                        error_rate, self.config.error_rate_critical),
189                )
190                .with_value(error_rate)
191                .with_threshold(self.config.error_rate_critical)
192                .with_recommendation("Investigate error logs immediately. Check for deployment issues or upstream service failures."),
193            )
194        } else if error_rate >= self.config.error_rate_warning {
195            Some(
196                Insight::new(
197                    InsightType::HighErrorRate,
198                    Severity::Warning,
199                    "Elevated Error Rate",
200                    format!(
201                        "Error rate is {:.2}%, above warning threshold of {:.2}%",
202                        error_rate, self.config.error_rate_warning
203                    ),
204                )
205                .with_value(error_rate)
206                .with_threshold(self.config.error_rate_warning)
207                .with_recommendation("Review error logs and monitor for further increase."),
208            )
209        } else {
210            None
211        }
212    }
213
214    fn check_latency(&self, latency: &LatencyMetrics) -> Option<Insight> {
215        if latency.p99_ms >= self.config.p99_latency_critical_ms {
216            Some(
217                Insight::new(
218                    InsightType::HighLatency,
219                    Severity::Critical,
220                    "Critical Latency",
221                    format!(
222                        "P99 latency is {:.0}ms, above critical threshold of {:.0}ms",
223                        latency.p99_ms, self.config.p99_latency_critical_ms
224                    ),
225                )
226                .with_value(latency.p99_ms)
227                .with_threshold(self.config.p99_latency_critical_ms)
228                .with_recommendation(
229                    "Check database queries, external service calls, and resource utilization.",
230                ),
231            )
232        } else if latency.p99_ms >= self.config.p99_latency_warning_ms {
233            Some(
234                Insight::new(
235                    InsightType::HighLatency,
236                    Severity::Warning,
237                    "Elevated Latency",
238                    format!(
239                        "P99 latency is {:.0}ms, above warning threshold of {:.0}ms",
240                        latency.p99_ms, self.config.p99_latency_warning_ms
241                    ),
242                )
243                .with_value(latency.p99_ms)
244                .with_threshold(self.config.p99_latency_warning_ms)
245                .with_recommendation("Profile slow requests and consider caching or optimization."),
246            )
247        } else {
248            None
249        }
250    }
251
252    fn check_rate_limits(&self, snapshot: &AnalyticsSnapshot) -> Option<Insight> {
253        let rate_limits = &snapshot.rate_limits;
254        if rate_limits.total_checks == 0 {
255            return None;
256        }
257
258        let limited_rate = (rate_limits.limited as f64 / rate_limits.total_checks as f64) * 100.0;
259
260        if limited_rate >= self.config.rate_limit_critical {
261            Some(
262                Insight::new(
263                    InsightType::RateLimitPressure,
264                    Severity::Critical,
265                    "Critical Rate Limit Pressure",
266                    format!("{:.2}% of requests are being rate limited", limited_rate),
267                )
268                .with_value(limited_rate)
269                .with_threshold(self.config.rate_limit_critical)
270                .with_recommendation("Consider increasing rate limits, adding capacity, or implementing request queuing."),
271            )
272        } else if limited_rate >= self.config.rate_limit_warning {
273            Some(
274                Insight::new(
275                    InsightType::RateLimitPressure,
276                    Severity::Warning,
277                    "Rate Limit Pressure",
278                    format!("{:.2}% of requests are being rate limited", limited_rate),
279                )
280                .with_value(limited_rate)
281                .with_threshold(self.config.rate_limit_warning)
282                .with_recommendation("Monitor rate limit usage and consider adjusting limits for legitimate traffic."),
283            )
284        } else {
285            None
286        }
287    }
288
289    fn check_traffic_spike(&self, snapshot: &AnalyticsSnapshot) -> Option<Insight> {
290        let baseline = self.baseline_rps?;
291        let current_rps = snapshot.throughput.requests_per_second;
292
293        if current_rps > baseline * self.config.traffic_spike_multiplier {
294            Some(
295                Insight::new(
296                    InsightType::TrafficSpike,
297                    Severity::Warning,
298                    "Traffic Spike Detected",
299                    format!(
300                        "Current RPS ({:.1}) is {:.1}x higher than baseline ({:.1})",
301                        current_rps,
302                        current_rps / baseline,
303                        baseline
304                    ),
305                )
306                .with_value(current_rps)
307                .with_threshold(baseline * self.config.traffic_spike_multiplier)
308                .with_recommendation(
309                    "Investigate traffic source. Consider enabling auto-scaling if available.",
310                ),
311            )
312        } else {
313            None
314        }
315    }
316
317    fn check_slow_endpoints(&self, endpoints: &[EndpointMetrics]) -> Vec<Insight> {
318        let mut insights = Vec::new();
319
320        for endpoint in endpoints {
321            // Skip endpoints with few requests
322            if endpoint.requests < 10 {
323                continue;
324            }
325
326            // Check for slow endpoints
327            if endpoint.p99_latency_ms >= self.config.p99_latency_critical_ms {
328                insights.push(
329                    Insight::new(
330                        InsightType::SlowEndpoint,
331                        Severity::Warning,
332                        "Slow Endpoint",
333                        format!(
334                            "{} {} has P99 latency of {:.0}ms",
335                            endpoint.method, endpoint.path, endpoint.p99_latency_ms
336                        ),
337                    )
338                    .with_value(endpoint.p99_latency_ms)
339                    .with_threshold(self.config.p99_latency_critical_ms)
340                    .with_endpoint(format!("{} {}", endpoint.method, endpoint.path))
341                    .with_recommendation(
342                        "Profile this specific endpoint for optimization opportunities.",
343                    ),
344                );
345            }
346
347            // Check for high error rate endpoints
348            if endpoint.error_rate >= self.config.error_rate_critical {
349                insights.push(
350                    Insight::new(
351                        InsightType::ErrorSpike,
352                        Severity::Warning,
353                        "High Error Rate Endpoint",
354                        format!(
355                            "{} {} has error rate of {:.2}%",
356                            endpoint.method, endpoint.path, endpoint.error_rate
357                        ),
358                    )
359                    .with_value(endpoint.error_rate)
360                    .with_threshold(self.config.error_rate_critical)
361                    .with_endpoint(format!("{} {}", endpoint.method, endpoint.path))
362                    .with_recommendation("Investigate errors specific to this endpoint."),
363                );
364            }
365        }
366
367        insights
368    }
369}
370
371impl Default for InsightGenerator {
372    fn default() -> Self {
373        Self::new(InsightConfig::default())
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_insight_creation() {
383        let insight = Insight::new(
384            InsightType::HighErrorRate,
385            Severity::Critical,
386            "Test",
387            "Description",
388        )
389        .with_value(5.0)
390        .with_threshold(1.0)
391        .with_recommendation("Fix it");
392
393        assert_eq!(insight.insight_type, InsightType::HighErrorRate);
394        assert_eq!(insight.severity, Severity::Critical);
395        assert_eq!(insight.value, 5.0);
396    }
397
398    #[test]
399    fn test_insight_config_defaults() {
400        let config = InsightConfig::default();
401        assert_eq!(config.error_rate_warning, 1.0);
402        assert_eq!(config.error_rate_critical, 5.0);
403    }
404}