1use crate::{AnalysisResult, FindingSet, LogEntry, PgLogstatsError, Result, TimingAnalysis};
4use std::fmt::Write;
5
6pub 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
23pub struct TextFormatter {
25 enable_color: bool,
27}
28
29impl TextFormatter {
30 pub fn new() -> Self {
32 Self {
33 enable_color: false,
34 }
35 }
36
37 pub fn with_color(mut self, enable: bool) -> Self {
39 self.enable_color = enable;
40 self
41 }
42
43 pub fn is_color_enabled(&self) -> bool {
45 self.enable_color
46 }
47
48 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 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 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 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}