Skip to main content

linkrs_metrics/manager/
profiles.rs

1//! Query portrait ring and slow-query logging.
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::profile::QueryProfile;
6use crate::utils::micros_to_millis;
7
8use super::core::StatsManager;
9
10/// Slow query statistics
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SlowQueryStats {
13    pub total: u64,
14    pub min_duration_secs: f64,
15    pub max_duration_secs: f64,
16    pub avg_duration_secs: f64,
17}
18
19/// Executor rollup for handler responses.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ExecutorSummary {
22    pub executor_type: String,
23    pub count: u64,
24    pub total_time_ms: u64,
25    pub total_rows: u64,
26    pub avg_time_ms: f64,
27}
28
29impl StatsManager {
30    pub fn record_query_profile(&self, profile: QueryProfile) {
31        if !self.monitoring_enabled {
32            return;
33        }
34
35        let is_failed = matches!(profile.status, crate::profile::QueryStatus::Failed);
36        let is_slow = profile.total_duration_us >= self.slow_query_threshold_us;
37        if is_slow {
38            self.write_slow_query_log(&profile);
39        } else {
40            self.aggregated_stats.record_query(&profile, false);
41        }
42        self.record_timeseries_query(profile.total_duration_us, is_failed);
43
44        let mut profiles = self.query_profiles.write();
45        if profiles.len() >= self.profile_cache_size {
46            profiles.pop_front();
47        }
48        profiles.push_back(profile);
49    }
50
51    pub(crate) fn write_slow_query_log(&self, profile: &QueryProfile) {
52        // Record to aggregated stats
53        self.aggregated_stats.record_query(profile, true);
54
55        if let Some(ref logger) = self.slow_query_logger {
56            logger.log(profile);
57        } else {
58            self.write_slow_query_log_fallback(profile);
59        }
60    }
61
62    fn write_slow_query_log_fallback(&self, profile: &QueryProfile) {
63        let executor_summary: Vec<String> = profile
64            .executor_stats
65            .iter()
66            .map(|stat| {
67                format!(
68                    "{}[id={}, {}ms, rows={}, mem={}]",
69                    stat.executor_type,
70                    stat.executor_id,
71                    stat.duration_ms(),
72                    stat.rows_processed(),
73                    stat.memory_used()
74                )
75            })
76            .collect();
77
78        let error_str = if let Some(ref info) = profile.error_info {
79            format!(
80                " [error={} phase={}]: {}",
81                info.error_type, info.error_phase, info.error_message
82            )
83        } else if let Some(ref msg) = profile.error_message {
84            format!(" [error]: {}", msg)
85        } else {
86            String::new()
87        };
88
89        log::warn!(
90            "Slow query [trace_id={}] [session_id={}] [duration={}ms] [status={}]\n\
91Queries: {}\n\
92Stage statistics: parse={}ms validate={}ms plan={}ms optimize={}ms execute={}ms\n\
93Number of results: {} Number of executors: {} Total executor time: {}ms\n\\\
94Executor details: {}{}",
95            profile.trace_id,
96            profile.session_id,
97            micros_to_millis(profile.total_duration_us),
98            match profile.status {
99                crate::profile::QueryStatus::Success => "success",
100                crate::profile::QueryStatus::Failed => "failed",
101            },
102            profile.query_text,
103            profile.stages.parse_ms(),
104            profile.stages.validate_ms(),
105            profile.stages.plan_ms(),
106            profile.stages.optimize_ms(),
107            profile.stages.execute_ms(),
108            profile.result_count,
109            profile.executor_stats.len(),
110            profile.total_executor_time_ms(),
111            executor_summary.join(", "),
112            error_str
113        );
114    }
115
116    pub fn get_recent_queries(&self, limit: usize) -> Vec<QueryProfile> {
117        let profiles = self.query_profiles.read();
118        profiles.iter().rev().take(limit).cloned().collect()
119    }
120
121    pub fn get_slow_queries(&self, limit: usize) -> Vec<QueryProfile> {
122        let profiles = self.query_profiles.read();
123        profiles
124            .iter()
125            .filter(|p| p.total_duration_us >= self.slow_query_threshold_us)
126            .rev()
127            .take(limit)
128            .cloned()
129            .collect()
130    }
131
132    pub fn get_all_slow_queries(&self, limit: usize) -> SlowQueryStats {
133        let profiles = self.query_profiles.read();
134        let slow_queries: Vec<_> = profiles
135            .iter()
136            .filter(|p| p.total_duration_us >= self.slow_query_threshold_us)
137            .take(limit)
138            .cloned()
139            .collect();
140
141        let total = slow_queries.len() as u64;
142        let durations: Vec<f64> = slow_queries
143            .iter()
144            .map(|p| p.total_duration_us as f64 / 1_000_000.0)
145            .collect();
146
147        let (min, max, avg) = if durations.is_empty() {
148            (0.0, 0.0, 0.0)
149        } else {
150            let min = durations.iter().cloned().fold(f64::INFINITY, f64::min);
151            let max = durations.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
152            let avg = durations.iter().sum::<f64>() / durations.len() as f64;
153            (min, max, avg)
154        };
155
156        SlowQueryStats {
157            total,
158            min_duration_secs: min,
159            max_duration_secs: max,
160            avg_duration_secs: avg,
161        }
162    }
163
164    pub fn get_query_profile(&self, trace_id: &str) -> Option<QueryProfile> {
165        let profiles = self.query_profiles.read();
166        profiles.iter().find(|p| p.trace_id == trace_id).cloned()
167    }
168
169    pub fn get_session_queries(&self, session_id: i64, limit: usize) -> Vec<QueryProfile> {
170        let profiles = self.query_profiles.read();
171        profiles
172            .iter()
173            .filter(|p| p.session_id == session_id)
174            .rev()
175            .take(limit)
176            .cloned()
177            .collect()
178    }
179
180    pub fn get_executor_stats_summary(&self) -> HashMap<String, (u64, u64, usize)> {
181        let profiles = self.query_profiles.read();
182        let mut stats: HashMap<String, (u64, u64, usize)> = HashMap::new();
183
184        for profile in profiles.iter() {
185            for exec_stat in &profile.executor_stats {
186                let entry = stats
187                    .entry(exec_stat.executor_type.clone())
188                    .or_insert((0, 0, 0));
189                entry.0 += exec_stat.duration_ms() as u64;
190                entry.1 += exec_stat.rows_processed() as u64;
191                entry.2 += 1;
192            }
193        }
194
195        stats
196    }
197
198    pub fn executor_summary_snapshot(&self) -> Vec<ExecutorSummary> {
199        let mut out: Vec<ExecutorSummary> = self
200            .get_executor_stats_summary()
201            .into_iter()
202            .map(|(executor_type, (total_time_ms, total_rows, count))| {
203                let avg_time_ms = if count == 0 {
204                    0.0
205                } else {
206                    total_time_ms as f64 / count as f64
207                };
208                ExecutorSummary {
209                    executor_type,
210                    count: count as u64,
211                    total_time_ms,
212                    total_rows,
213                    avg_time_ms,
214                }
215            })
216            .collect();
217        out.sort_by_key(|p| std::cmp::Reverse(p.total_time_ms));
218        out
219    }
220
221    pub fn profiles_in_window(
222        &self,
223        from_secs: Option<u64>,
224        to_secs: Option<u64>,
225        limit: usize,
226    ) -> Vec<QueryProfile> {
227        let profiles = self.query_profiles.read();
228        profiles
229            .iter()
230            .rev()
231            .filter(|p| {
232                if let Some(from) = from_secs {
233                    if p.started_at_secs < from {
234                        return false;
235                    }
236                }
237                if let Some(to) = to_secs {
238                    if p.started_at_secs > to {
239                        return false;
240                    }
241                }
242                true
243            })
244            .take(limit)
245            .cloned()
246            .collect()
247    }
248
249    pub fn clear_query_cache(&self) {
250        let mut profiles = self.query_profiles.write();
251        profiles.clear();
252    }
253
254    pub fn query_cache_size(&self) -> usize {
255        let profiles = self.query_profiles.read();
256        profiles.len()
257    }
258}