pg_logstats/output/
json.rs1use crate::{AnalysisResult, FindingSet, PgLogstatsError, Result, TimingAnalysis};
4use chrono::Utc;
5use serde_json::json;
6use std::collections::HashMap;
7
8pub struct JsonFormatter {
10 pretty: bool,
12 tool_version: String,
13 log_files_processed: Vec<String>,
14 total_log_entries: usize,
15}
16
17impl JsonFormatter {
18 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 pub fn with_pretty(mut self, pretty: bool) -> Self {
30 self.pretty = pretty;
31 self
32 }
33
34 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 pub fn is_pretty(&self) -> bool {
49 self.pretty
50 }
51
52 pub fn tool_version(&self) -> &str {
54 &self.tool_version
55 }
56
57 pub fn log_files_processed(&self) -> &[String] {
59 &self.log_files_processed
60 }
61
62 pub fn total_log_entries(&self) -> usize {
64 self.total_log_entries
65 }
66
67 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 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 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 "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 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 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 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}