Skip to main content

pg_logstats/output/
text.rs

1//! Human-readable text output formatter for pg-logstats results
2
3use crate::{AnalysisResult, FindingSet, LogEntry, PgLogstatsError, Result, TimingAnalysis};
4use std::fmt::Write;
5
6/// ANSI color helpers (basic)
7pub fn bold(s: &str, color: Option<&str>, enable_color: bool) -> String {
8    if !enable_color {
9        return s.to_string();
10    }
11    let code = match color.unwrap_or("white") {
12        "red" => "\x1b[31;1m",
13        "green" => "\x1b[32;1m",
14        "yellow" => "\x1b[33;1m",
15        "blue" => "\x1b[34;1m",
16        "magenta" => "\x1b[35;1m",
17        "cyan" => "\x1b[36;1m",
18        _ => "\x1b[37;1m",
19    };
20    format!("{}{}\x1b[0m", code, s)
21}
22
23/// Text formatter for analysis results
24pub struct TextFormatter {
25    // Configuration for text formatting
26    enable_color: bool,
27}
28
29impl TextFormatter {
30    /// Create a new text formatter
31    pub fn new() -> Self {
32        Self {
33            enable_color: false,
34        }
35    }
36
37    /// Enable or disable ANSI color output
38    pub fn with_color(mut self, enable: bool) -> Self {
39        self.enable_color = enable;
40        self
41    }
42
43    /// Get whether color output is enabled
44    pub fn is_color_enabled(&self) -> bool {
45        self.enable_color
46    }
47
48    /// Format query analysis results as text
49    pub fn format_query_analysis(&self, analysis: &AnalysisResult) -> Result<String> {
50        let mut output = String::new();
51
52        writeln!(
53            output,
54            "{}",
55            bold("Query Analysis Report", Some("cyan"), self.enable_color)
56        )
57        .map_err(|e| PgLogstatsError::Unexpected {
58            message: e.to_string(),
59            context: Some("text formatting".to_string()),
60        })?;
61        writeln!(
62            output,
63            "{}",
64            bold("===================", Some("cyan"), self.enable_color)
65        )
66        .map_err(|e| PgLogstatsError::Unexpected {
67            message: e.to_string(),
68            context: Some("text formatting".to_string()),
69        })?;
70        writeln!(output, "Total Queries: {}", analysis.total_queries).map_err(|e| {
71            PgLogstatsError::Unexpected {
72                message: e.to_string(),
73                context: Some("text formatting".to_string()),
74            }
75        })?;
76        writeln!(output, "Total Duration: {:.2} ms", analysis.total_duration).map_err(|e| {
77            PgLogstatsError::Unexpected {
78                message: e.to_string(),
79                context: Some("text formatting".to_string()),
80            }
81        })?;
82        writeln!(
83            output,
84            "Average Duration: {:.2} ms",
85            analysis.average_duration
86        )
87        .map_err(|e| PgLogstatsError::Unexpected {
88            message: e.to_string(),
89            context: Some("text formatting".to_string()),
90        })?;
91        writeln!(output, "P95 Duration: {:.2} ms", analysis.p95_duration).map_err(|e| {
92            PgLogstatsError::Unexpected {
93                message: e.to_string(),
94                context: Some("text formatting".to_string()),
95            }
96        })?;
97        writeln!(output, "P99 Duration: {:.2} ms", analysis.p99_duration).map_err(|e| {
98            PgLogstatsError::Unexpected {
99                message: e.to_string(),
100                context: Some("text formatting".to_string()),
101            }
102        })?;
103        writeln!(output, "Error Count: {}", analysis.error_count).map_err(|e| {
104            PgLogstatsError::Unexpected {
105                message: e.to_string(),
106                context: Some("text formatting".to_string()),
107            }
108        })?;
109        writeln!(output, "Connection Count: {}", analysis.connection_count).map_err(|e| {
110            PgLogstatsError::Unexpected {
111                message: e.to_string(),
112                context: Some("text formatting".to_string()),
113            }
114        })?;
115
116        if !analysis.query_types.is_empty() {
117            writeln!(
118                output,
119                "\n{}",
120                bold("Query Types:", Some("yellow"), self.enable_color)
121            )
122            .map_err(|e| PgLogstatsError::Unexpected {
123                message: e.to_string(),
124                context: Some("text formatting".to_string()),
125            })?;
126            for (query_type, count) in &analysis.query_types {
127                writeln!(output, "  {:>8}: {}", query_type, count).map_err(|e| {
128                    PgLogstatsError::Unexpected {
129                        message: e.to_string(),
130                        context: Some("text formatting".to_string()),
131                    }
132                })?;
133            }
134        }
135
136        if !analysis.slowest_queries.is_empty() {
137            writeln!(
138                output,
139                "\n{}",
140                bold("Slowest Queries:", Some("red"), self.enable_color)
141            )
142            .map_err(|e| PgLogstatsError::Unexpected {
143                message: e.to_string(),
144                context: Some("text formatting".to_string()),
145            })?;
146            writeln!(output, "  {:>4}  {:>12}  Query", "#", "Duration (ms)").map_err(|e| {
147                PgLogstatsError::Unexpected {
148                    message: e.to_string(),
149                    context: Some("text formatting".to_string()),
150                }
151            })?;
152            for (i, (query, duration)) in analysis.slowest_queries.iter().enumerate() {
153                writeln!(output, "  {:>4}  {:>12.2}  {}", i + 1, duration, query).map_err(|e| {
154                    PgLogstatsError::Unexpected {
155                        message: e.to_string(),
156                        context: Some("text formatting".to_string()),
157                    }
158                })?;
159            }
160        }
161
162        if !analysis.most_frequent_queries.is_empty() {
163            writeln!(
164                output,
165                "\n{}",
166                bold("Most Frequent Queries:", Some("green"), self.enable_color)
167            )
168            .map_err(|e| PgLogstatsError::Unexpected {
169                message: e.to_string(),
170                context: Some("text formatting".to_string()),
171            })?;
172            writeln!(output, "  {:>4}  {:>8}  Query", "#", "Count").map_err(|e| {
173                PgLogstatsError::Unexpected {
174                    message: e.to_string(),
175                    context: Some("text formatting".to_string()),
176                }
177            })?;
178            for (i, (query, count)) in analysis.most_frequent_queries.iter().enumerate() {
179                writeln!(output, "  {:>4}  {:>8}  {}", i + 1, count, query).map_err(|e| {
180                    PgLogstatsError::Unexpected {
181                        message: e.to_string(),
182                        context: Some("text formatting".to_string()),
183                    }
184                })?;
185            }
186        }
187
188        Ok(output)
189    }
190
191    /// Format timing analysis results as text
192    pub fn format_timing_analysis(&self, analysis: &TimingAnalysis) -> Result<String> {
193        let mut output = String::new();
194
195        writeln!(
196            output,
197            "{}",
198            bold("Timing Analysis Report", Some("cyan"), self.enable_color)
199        )
200        .map_err(|e| PgLogstatsError::Unexpected {
201            message: e.to_string(),
202            context: Some("text formatting".to_string()),
203        })?;
204        writeln!(
205            output,
206            "{}",
207            bold("====================", Some("cyan"), self.enable_color)
208        )
209        .map_err(|e| PgLogstatsError::Unexpected {
210            message: e.to_string(),
211            context: Some("text formatting".to_string()),
212        })?;
213        writeln!(
214            output,
215            "Average Response Time: {}ms",
216            analysis.average_response_time.num_milliseconds()
217        )
218        .map_err(|e| PgLogstatsError::Unexpected {
219            message: e.to_string(),
220            context: Some("text formatting".to_string()),
221        })?;
222        writeln!(
223            output,
224            "95th Percentile: {}ms",
225            analysis.p95_response_time.num_milliseconds()
226        )
227        .map_err(|e| PgLogstatsError::Unexpected {
228            message: e.to_string(),
229            context: Some("text formatting".to_string()),
230        })?;
231        writeln!(
232            output,
233            "99th Percentile: {}ms",
234            analysis.p99_response_time.num_milliseconds()
235        )
236        .map_err(|e| PgLogstatsError::Unexpected {
237            message: e.to_string(),
238            context: Some("text formatting".to_string()),
239        })?;
240
241        Ok(output)
242    }
243
244    /// Format structured findings as a compact human-readable view.
245    pub fn format_findings(&self, findings: &FindingSet) -> Result<String> {
246        let mut output = String::new();
247
248        writeln!(
249            output,
250            "{}",
251            bold("Findings", Some("cyan"), self.enable_color)
252        )
253        .map_err(|e| PgLogstatsError::Unexpected {
254            message: e.to_string(),
255            context: Some("text formatting".to_string()),
256        })?;
257        writeln!(output, "Schema Version: {}", findings.schema_version).map_err(|e| {
258            PgLogstatsError::Unexpected {
259                message: e.to_string(),
260                context: Some("text formatting".to_string()),
261            }
262        })?;
263
264        for finding in &findings.findings {
265            writeln!(
266                output,
267                "\n#{} [{}] {}",
268                finding.rank, finding.finding_id, finding.title
269            )
270            .map_err(|e| PgLogstatsError::Unexpected {
271                message: e.to_string(),
272                context: Some("text formatting".to_string()),
273            })?;
274            writeln!(output, "Reason: {}", finding.reason).map_err(|e| {
275                PgLogstatsError::Unexpected {
276                    message: e.to_string(),
277                    context: Some("text formatting".to_string()),
278                }
279            })?;
280            writeln!(
281                output,
282                "Score: {:.3}  Confidence: {:?}",
283                finding.score, finding.confidence
284            )
285            .map_err(|e| PgLogstatsError::Unexpected {
286                message: e.to_string(),
287                context: Some("text formatting".to_string()),
288            })?;
289
290            if let Some(query_family) = &finding.query_family {
291                writeln!(output, "Query Family: {}", query_family.query_family_id).map_err(
292                    |e| PgLogstatsError::Unexpected {
293                        message: e.to_string(),
294                        context: Some("text formatting".to_string()),
295                    },
296                )?;
297                writeln!(output, "SQL: {}", query_family.normalized_sql).map_err(|e| {
298                    PgLogstatsError::Unexpected {
299                        message: e.to_string(),
300                        context: Some("text formatting".to_string()),
301                    }
302                })?;
303            }
304        }
305
306        Ok(output)
307    }
308
309    /// Format log entries as text
310    pub fn format_log_entries(&self, entries: &[LogEntry]) -> Result<String> {
311        let mut output = String::new();
312
313        writeln!(
314            output,
315            "{}",
316            bold(
317                &format!("Log Entries ({} total)", entries.len()),
318                Some("magenta"),
319                self.enable_color
320            )
321        )
322        .map_err(|e| PgLogstatsError::Unexpected {
323            message: e.to_string(),
324            context: Some("text formatting".to_string()),
325        })?;
326        writeln!(
327            output,
328            "{}",
329            bold("================", Some("magenta"), self.enable_color)
330        )
331        .map_err(|e| PgLogstatsError::Unexpected {
332            message: e.to_string(),
333            context: Some("text formatting".to_string()),
334        })?;
335
336        for (i, entry) in entries.iter().enumerate() {
337            writeln!(
338                output,
339                "[{}] {} {}: {}",
340                i + 1,
341                entry.timestamp.format("%Y-%m-%d %H:%M:%S"),
342                entry.message_type,
343                entry.message
344            )
345            .map_err(|e| PgLogstatsError::Unexpected {
346                message: e.to_string(),
347                context: Some("text formatting".to_string()),
348            })?;
349        }
350
351        Ok(output)
352    }
353}
354
355impl Default for TextFormatter {
356    fn default() -> Self {
357        Self::new()
358    }
359}