use crate::error::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub struct ReportGenerator {
config: ReportConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportConfig {
pub report_name: String,
pub recipients: Vec<String>,
pub schedule: ReportSchedule,
pub include_sections: Vec<ReportSection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReportSchedule {
Hourly,
Daily,
Weekly,
Monthly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReportSection {
ExecutiveSummary,
PerformanceMetrics,
SloCompliance,
AnomalyDetection,
ResourceUtilization,
ErrorAnalysis,
Trends,
Recommendations,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
pub id: String,
pub name: String,
pub generated_at: DateTime<Utc>,
pub period_start: DateTime<Utc>,
pub period_end: DateTime<Utc>,
pub sections: HashMap<String, ReportData>,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReportData {
Text(String),
Metrics(Vec<MetricSummary>),
Charts(Vec<ChartData>),
Tables(Vec<TableData>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricSummary {
pub name: String,
pub value: f64,
pub unit: String,
pub change_percent: f64,
pub trend: Trend,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Trend {
Up,
Down,
Stable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChartData {
pub title: String,
pub chart_type: ChartType,
pub data_points: Vec<(String, f64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChartType {
Line,
Bar,
Pie,
Area,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableData {
pub title: String,
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
}
impl ReportGenerator {
pub fn new(config: ReportConfig) -> Self {
Self { config }
}
pub fn generate(
&self,
period_start: DateTime<Utc>,
period_end: DateTime<Utc>,
) -> Result<Report> {
let mut sections = HashMap::new();
for section in &self.config.include_sections {
let data = match section {
ReportSection::ExecutiveSummary => self.generate_executive_summary()?,
ReportSection::PerformanceMetrics => self.generate_performance_metrics()?,
ReportSection::SloCompliance => self.generate_slo_compliance()?,
ReportSection::AnomalyDetection => self.generate_anomaly_section()?,
ReportSection::ResourceUtilization => self.generate_resource_utilization()?,
ReportSection::ErrorAnalysis => self.generate_error_analysis()?,
ReportSection::Trends => self.generate_trends()?,
ReportSection::Recommendations => self.generate_recommendations()?,
};
sections.insert(format!("{:?}", section), data);
}
Ok(Report {
id: uuid::Uuid::new_v4().to_string(),
name: self.config.report_name.clone(),
generated_at: Utc::now(),
period_start,
period_end,
sections,
summary: self.generate_summary()?,
})
}
fn generate_executive_summary(&self) -> Result<ReportData> {
let summary =
"System performance has been within expected parameters during the reporting period.";
Ok(ReportData::Text(summary.to_string()))
}
fn generate_performance_metrics(&self) -> Result<ReportData> {
let metrics = vec![
MetricSummary {
name: "Request Rate".to_string(),
value: 1000.0,
unit: "req/s".to_string(),
change_percent: 5.2,
trend: Trend::Up,
},
MetricSummary {
name: "Average Latency".to_string(),
value: 45.3,
unit: "ms".to_string(),
change_percent: -2.1,
trend: Trend::Down,
},
];
Ok(ReportData::Metrics(metrics))
}
fn generate_slo_compliance(&self) -> Result<ReportData> {
let rows = vec![
vec![
"Availability".to_string(),
"99.95%".to_string(),
"Pass".to_string(),
],
vec![
"Latency P95".to_string(),
"98ms".to_string(),
"Pass".to_string(),
],
];
Ok(ReportData::Tables(vec![TableData {
title: "SLO Compliance".to_string(),
headers: vec![
"Metric".to_string(),
"Value".to_string(),
"Status".to_string(),
],
rows,
}]))
}
fn generate_anomaly_section(&self) -> Result<ReportData> {
Ok(ReportData::Text(
"No significant anomalies detected.".to_string(),
))
}
fn generate_resource_utilization(&self) -> Result<ReportData> {
let data_points = vec![
("CPU".to_string(), 45.0),
("Memory".to_string(), 62.0),
("Disk".to_string(), 38.0),
];
Ok(ReportData::Charts(vec![ChartData {
title: "Resource Utilization".to_string(),
chart_type: ChartType::Bar,
data_points,
}]))
}
fn generate_error_analysis(&self) -> Result<ReportData> {
Ok(ReportData::Text("Error rate: 0.03%".to_string()))
}
fn generate_trends(&self) -> Result<ReportData> {
let data_points = vec![
("Mon".to_string(), 100.0),
("Tue".to_string(), 105.0),
("Wed".to_string(), 110.0),
("Thu".to_string(), 108.0),
("Fri".to_string(), 115.0),
];
Ok(ReportData::Charts(vec![ChartData {
title: "Request Volume Trend".to_string(),
chart_type: ChartType::Line,
data_points,
}]))
}
fn generate_recommendations(&self) -> Result<ReportData> {
Ok(ReportData::Text(
"Consider scaling up compute resources to handle peak load.".to_string(),
))
}
fn generate_summary(&self) -> Result<String> {
Ok("System operating normally with all SLOs met.".to_string())
}
pub fn export_html(&self, report: &Report) -> Result<String> {
let mut html = format!(
r#"<!DOCTYPE html>
<html>
<head>
<title>{}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
h1 {{ color: #333; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
</style>
</head>
<body>
<h1>{}</h1>
<p>Generated: {}</p>
<p>Period: {} to {}</p>
<h2>Summary</h2>
<p>{}</p>
"#,
report.name,
report.name,
report.generated_at.format("%Y-%m-%d %H:%M:%S"),
report.period_start.format("%Y-%m-%d %H:%M:%S"),
report.period_end.format("%Y-%m-%d %H:%M:%S"),
report.summary
);
for (section_name, data) in &report.sections {
html.push_str(&format!("<h2>{}</h2>", section_name));
match data {
ReportData::Text(text) => {
html.push_str(&format!("<p>{}</p>", text));
}
ReportData::Metrics(metrics) => {
html.push_str("<table><tr><th>Metric</th><th>Value</th><th>Change</th></tr>");
for metric in metrics {
html.push_str(&format!(
"<tr><td>{}</td><td>{} {}</td><td>{:+.1}%</td></tr>",
metric.name, metric.value, metric.unit, metric.change_percent
));
}
html.push_str("</table>");
}
ReportData::Tables(tables) => {
for table in tables {
html.push_str("<table>");
html.push_str("<tr>");
for header in &table.headers {
html.push_str(&format!("<th>{}</th>", header));
}
html.push_str("</tr>");
for row in &table.rows {
html.push_str("<tr>");
for cell in row {
html.push_str(&format!("<td>{}</td>", cell));
}
html.push_str("</tr>");
}
html.push_str("</table>");
}
}
ReportData::Charts(_) => {
html.push_str("<p>[Chart visualization would be rendered here]</p>");
}
}
}
html.push_str("</body></html>");
Ok(html)
}
pub fn export_json(&self, report: &Report) -> Result<String> {
serde_json::to_string_pretty(report)
.map_err(crate::error::ObservabilityError::Serialization)
}
}
pub struct NotificationSystem {
channels: Vec<NotificationChannel>,
}
pub enum NotificationChannel {
Email {
addresses: Vec<String>,
},
Slack {
webhook_url: String,
},
PagerDuty {
integration_key: String,
},
Webhook {
url: String,
},
}
impl NotificationSystem {
pub fn new() -> Self {
Self {
channels: Vec::new(),
}
}
pub fn add_channel(&mut self, channel: NotificationChannel) {
self.channels.push(channel);
}
pub async fn send(
&self,
#[cfg(feature = "http-exporter")] message: &str,
#[cfg(not(feature = "http-exporter"))] _message: &str,
) -> Result<()> {
for channel in &self.channels {
match channel {
#[cfg(feature = "http-exporter")]
NotificationChannel::Webhook { url } => {
let client = reqwest::Client::new();
let _ = client
.post(url)
.json(&serde_json::json!({ "message": message }))
.send()
.await?;
}
#[cfg(feature = "http-exporter")]
NotificationChannel::Slack { webhook_url } => {
let client = reqwest::Client::new();
let _ = client
.post(webhook_url)
.json(&serde_json::json!({ "text": message }))
.send()
.await?;
}
_ => {
}
}
}
Ok(())
}
}
impl Default for NotificationSystem {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_report_generation() {
let config = ReportConfig {
report_name: "Test Report".to_string(),
recipients: vec!["test@example.com".to_string()],
schedule: ReportSchedule::Daily,
include_sections: vec![
ReportSection::ExecutiveSummary,
ReportSection::PerformanceMetrics,
],
};
let generator = ReportGenerator::new(config);
let report = generator.generate(Utc::now(), Utc::now());
assert!(report.is_ok());
}
#[test]
fn test_html_export() {
let config = ReportConfig {
report_name: "Test Report".to_string(),
recipients: vec![],
schedule: ReportSchedule::Daily,
include_sections: vec![ReportSection::ExecutiveSummary],
};
let generator = ReportGenerator::new(config);
let report = generator
.generate(Utc::now(), Utc::now())
.expect("Failed to generate report");
let html = generator.export_html(&report);
assert!(html.is_ok());
assert!(html.expect("No HTML").contains("<!DOCTYPE html>"));
}
}