use crate::{AnalysisResult, FindingSet, PgLogstatsError, Result, TimingAnalysis};
use chrono::Utc;
use serde_json::json;
use std::collections::HashMap;
pub struct JsonFormatter {
pretty: bool,
tool_version: String,
log_files_processed: Vec<String>,
total_log_entries: usize,
}
impl JsonFormatter {
pub fn new() -> Self {
Self {
pretty: false,
tool_version: env!("CARGO_PKG_VERSION").to_string(),
log_files_processed: Vec::new(),
total_log_entries: 0,
}
}
pub fn with_pretty(mut self, pretty: bool) -> Self {
self.pretty = pretty;
self
}
pub fn with_metadata(
mut self,
tool_version: impl Into<String>,
log_files_processed: Vec<String>,
total_log_entries: usize,
) -> Self {
self.tool_version = tool_version.into();
self.log_files_processed = log_files_processed;
self.total_log_entries = total_log_entries;
self
}
pub fn is_pretty(&self) -> bool {
self.pretty
}
pub fn tool_version(&self) -> &str {
&self.tool_version
}
pub fn log_files_processed(&self) -> &[String] {
&self.log_files_processed
}
pub fn total_log_entries(&self) -> usize {
self.total_log_entries
}
pub fn metadata_object(&self) -> serde_json::Value {
json!({
"analysis_timestamp": Utc::now().to_rfc3339(),
"tool_version": self.tool_version,
"log_files_processed": self.log_files_processed,
"total_log_entries": self.total_log_entries,
})
}
pub fn format(&self, analysis: &AnalysisResult) -> Result<String> {
let summary = json!({
"total_queries": analysis.total_queries,
"total_duration_ms": analysis.total_duration,
"avg_duration_ms": analysis.average_duration,
"error_count": analysis.error_count,
"connection_count": analysis.connection_count,
});
let by_type =
serde_json::to_value(&analysis.query_types).map_err(PgLogstatsError::Serialization)?;
let mut freq_map: HashMap<String, u64> = HashMap::new();
for (q, c) in &analysis.most_frequent_queries {
freq_map.insert(q.clone(), *c);
}
let slowest_queries = analysis
.slowest_queries
.iter()
.map(|(q, d)| {
json!({
"query": q,
"duration_ms": d,
"count": freq_map.get(q).cloned().unwrap_or(1),
})
})
.collect::<Vec<_>>();
let most_frequent = analysis
.most_frequent_queries
.iter()
.map(|(q, c)| {
json!({
"query": q,
"count": c,
"avg_duration_ms": analysis.average_duration,
})
})
.collect::<Vec<_>>();
let root = json!({
"metadata": self.metadata_object(),
"summary": summary,
"query_analysis": {
"by_type": by_type,
"slowest_queries": slowest_queries,
"most_frequent": most_frequent,
},
});
if self.pretty {
serde_json::to_string_pretty(&root).map_err(PgLogstatsError::Serialization)
} else {
serde_json::to_string(&root).map_err(PgLogstatsError::Serialization)
}
}
pub fn format_with_timing(
&self,
analysis: &AnalysisResult,
timing: &TimingAnalysis,
) -> Result<String> {
let mut base: serde_json::Value = serde_json::from_str(&self.format(analysis)?)
.map_err(PgLogstatsError::Serialization)?;
let hourly_stats = timing
.hourly_patterns
.iter()
.map(|(hour, total_ms)| {
json!({
"hour": hour,
"total_duration_ms": total_ms,
})
})
.collect::<Vec<_>>();
let temporal = json!({
"hourly_stats": hourly_stats,
"average_response_time_ms": timing.average_response_time.num_milliseconds(),
"p95_response_time_ms": timing.p95_response_time.num_milliseconds(),
"p99_response_time_ms": timing.p99_response_time.num_milliseconds(),
});
if let Some(obj) = base.as_object_mut() {
obj.insert("temporal_analysis".to_string(), temporal);
}
if self.pretty {
serde_json::to_string_pretty(&base).map_err(PgLogstatsError::Serialization)
} else {
serde_json::to_string(&base).map_err(PgLogstatsError::Serialization)
}
}
pub fn format_findings(&self, findings: &FindingSet) -> Result<String> {
let root = json!({
"metadata": self.metadata_object(),
"schema_version": findings.schema_version,
"findings": findings.findings,
});
if self.pretty {
serde_json::to_string_pretty(&root).map_err(PgLogstatsError::Serialization)
} else {
serde_json::to_string(&root).map_err(PgLogstatsError::Serialization)
}
}
}
impl Default for JsonFormatter {
fn default() -> Self {
Self::new()
}
}