Skip to main content

pg_logstats/output/
json.rs

1//! JSON output formatter for pg-logstats results
2
3use crate::{AnalysisResult, FindingSet, PgLogstatsError, Result, TimingAnalysis};
4use chrono::Utc;
5use serde_json::json;
6use std::collections::HashMap;
7
8/// JSON formatter for analysis results
9pub struct JsonFormatter {
10    // Configuration for JSON formatting
11    pretty: bool,
12    tool_version: String,
13    log_files_processed: Vec<String>,
14    total_log_entries: usize,
15}
16
17impl JsonFormatter {
18    /// Create a new JSON formatter
19    pub fn new() -> Self {
20        Self {
21            pretty: false,
22            tool_version: env!("CARGO_PKG_VERSION").to_string(),
23            log_files_processed: Vec::new(),
24            total_log_entries: 0,
25        }
26    }
27
28    /// Enable or disable pretty printing
29    pub fn with_pretty(mut self, pretty: bool) -> Self {
30        self.pretty = pretty;
31        self
32    }
33
34    /// Set metadata values for output
35    pub fn with_metadata(
36        mut self,
37        tool_version: impl Into<String>,
38        log_files_processed: Vec<String>,
39        total_log_entries: usize,
40    ) -> Self {
41        self.tool_version = tool_version.into();
42        self.log_files_processed = log_files_processed;
43        self.total_log_entries = total_log_entries;
44        self
45    }
46
47    /// Get whether pretty printing is enabled
48    pub fn is_pretty(&self) -> bool {
49        self.pretty
50    }
51
52    /// Get the tool version
53    pub fn tool_version(&self) -> &str {
54        &self.tool_version
55    }
56
57    /// Get the log files processed
58    pub fn log_files_processed(&self) -> &[String] {
59        &self.log_files_processed
60    }
61
62    /// Get the total log entries count
63    pub fn total_log_entries(&self) -> usize {
64        self.total_log_entries
65    }
66
67    /// Get metadata object (made public for testing)
68    pub fn metadata_object(&self) -> serde_json::Value {
69        json!({
70            "analysis_timestamp": Utc::now().to_rfc3339(),
71            "tool_version": self.tool_version,
72            "log_files_processed": self.log_files_processed,
73            "total_log_entries": self.total_log_entries,
74        })
75    }
76
77    /// Format a single AnalysisResult as structured JSON
78    pub fn format(&self, analysis: &AnalysisResult) -> Result<String> {
79        let summary = json!({
80            "total_queries": analysis.total_queries,
81            "total_duration_ms": analysis.total_duration,
82            "avg_duration_ms": analysis.average_duration,
83            "error_count": analysis.error_count,
84            "connection_count": analysis.connection_count,
85        });
86
87        let by_type =
88            serde_json::to_value(&analysis.query_types).map_err(PgLogstatsError::Serialization)?;
89
90        // Build a map from query -> count to enrich slowest queries
91        let mut freq_map: HashMap<String, u64> = HashMap::new();
92        for (q, c) in &analysis.most_frequent_queries {
93            freq_map.insert(q.clone(), *c);
94        }
95
96        let slowest_queries = analysis
97            .slowest_queries
98            .iter()
99            .map(|(q, d)| {
100                json!({
101                    "query": q,
102                    "duration_ms": d,
103                    "count": freq_map.get(q).cloned().unwrap_or(1),
104                })
105            })
106            .collect::<Vec<_>>();
107
108        let most_frequent = analysis
109            .most_frequent_queries
110            .iter()
111            .map(|(q, c)| {
112                json!({
113                    "query": q,
114                    "count": c,
115                    // Without per-query duration distribution, fall back to overall average
116                    "avg_duration_ms": analysis.average_duration,
117                })
118            })
119            .collect::<Vec<_>>();
120
121        let root = json!({
122            "metadata": self.metadata_object(),
123            "summary": summary,
124            "query_analysis": {
125                "by_type": by_type,
126                "slowest_queries": slowest_queries,
127                "most_frequent": most_frequent,
128            },
129        });
130
131        if self.pretty {
132            serde_json::to_string_pretty(&root).map_err(PgLogstatsError::Serialization)
133        } else {
134            serde_json::to_string(&root).map_err(PgLogstatsError::Serialization)
135        }
136    }
137
138    /// Format with timing analysis included
139    pub fn format_with_timing(
140        &self,
141        analysis: &AnalysisResult,
142        timing: &TimingAnalysis,
143    ) -> Result<String> {
144        let mut base: serde_json::Value = serde_json::from_str(&self.format(analysis)?)
145            .map_err(PgLogstatsError::Serialization)?;
146
147        // Build temporal analysis section from TimingAnalysis
148        let hourly_stats = timing
149            .hourly_patterns
150            .iter()
151            .map(|(hour, total_ms)| {
152                json!({
153                    "hour": hour,
154                    "total_duration_ms": total_ms,
155                })
156            })
157            .collect::<Vec<_>>();
158
159        let temporal = json!({
160            "hourly_stats": hourly_stats,
161            "average_response_time_ms": timing.average_response_time.num_milliseconds(),
162            "p95_response_time_ms": timing.p95_response_time.num_milliseconds(),
163            "p99_response_time_ms": timing.p99_response_time.num_milliseconds(),
164        });
165
166        if let Some(obj) = base.as_object_mut() {
167            obj.insert("temporal_analysis".to_string(), temporal);
168        }
169
170        if self.pretty {
171            serde_json::to_string_pretty(&base).map_err(PgLogstatsError::Serialization)
172        } else {
173            serde_json::to_string(&base).map_err(PgLogstatsError::Serialization)
174        }
175    }
176
177    /// Format structured findings as compact, versioned JSON.
178    pub fn format_findings(&self, findings: &FindingSet) -> Result<String> {
179        let root = json!({
180            "metadata": self.metadata_object(),
181            "schema_version": findings.schema_version,
182            "findings": findings.findings,
183        });
184
185        if self.pretty {
186            serde_json::to_string_pretty(&root).map_err(PgLogstatsError::Serialization)
187        } else {
188            serde_json::to_string(&root).map_err(PgLogstatsError::Serialization)
189        }
190    }
191}
192
193impl Default for JsonFormatter {
194    fn default() -> Self {
195        Self::new()
196    }
197}