mockforge_reporting/
pdf.rs

1//! PDF report generation for orchestration execution results
2
3use crate::{ReportingError, Result};
4use chrono::{DateTime, Utc};
5use printpdf::*;
6use serde::{Deserialize, Serialize};
7use std::fs::File;
8use std::io::BufWriter;
9
10/// PDF generation configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PdfConfig {
13    pub title: String,
14    pub author: String,
15    pub include_charts: bool,
16    pub include_metrics: bool,
17    pub include_recommendations: bool,
18}
19
20impl Default for PdfConfig {
21    fn default() -> Self {
22        Self {
23            title: "Chaos Orchestration Report".to_string(),
24            author: "MockForge".to_string(),
25            include_charts: true,
26            include_metrics: true,
27            include_recommendations: true,
28        }
29    }
30}
31
32/// Execution report data
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ExecutionReport {
35    pub orchestration_name: String,
36    pub start_time: DateTime<Utc>,
37    pub end_time: DateTime<Utc>,
38    pub duration_seconds: u64,
39    pub status: String,
40    pub total_steps: usize,
41    pub completed_steps: usize,
42    pub failed_steps: usize,
43    pub metrics: ReportMetrics,
44    pub failures: Vec<FailureDetail>,
45    pub recommendations: Vec<String>,
46}
47
48/// Report metrics
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ReportMetrics {
51    pub total_requests: u64,
52    pub successful_requests: u64,
53    pub failed_requests: u64,
54    pub avg_latency_ms: f64,
55    pub p95_latency_ms: f64,
56    pub p99_latency_ms: f64,
57    pub error_rate: f64,
58}
59
60/// Failure detail
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct FailureDetail {
63    pub step_name: String,
64    pub error_message: String,
65    pub timestamp: DateTime<Utc>,
66}
67
68/// PDF report generator
69pub struct PdfReportGenerator {
70    config: PdfConfig,
71}
72
73impl PdfReportGenerator {
74    /// Create a new PDF generator
75    pub fn new(config: PdfConfig) -> Self {
76        Self { config }
77    }
78
79    /// Generate PDF report from execution data
80    pub fn generate(&self, report: &ExecutionReport, output_path: &str) -> Result<()> {
81        let (doc, page1, layer1) =
82            PdfDocument::new(&self.config.title, Mm(210.0), Mm(297.0), "Layer 1");
83
84        let font = doc
85            .add_builtin_font(BuiltinFont::Helvetica)
86            .map_err(|e| ReportingError::Pdf(e.to_string()))?;
87        let font_bold = doc
88            .add_builtin_font(BuiltinFont::HelveticaBold)
89            .map_err(|e| ReportingError::Pdf(e.to_string()))?;
90
91        let current_layer = doc.get_page(page1).get_layer(layer1);
92
93        // Title
94        current_layer.use_text(&self.config.title, 24.0, Mm(20.0), Mm(270.0), &font_bold);
95
96        // Metadata
97        let mut y = 255.0;
98        current_layer.use_text(
99            format!("Orchestration: {}", report.orchestration_name),
100            12.0,
101            Mm(20.0),
102            Mm(y),
103            &font,
104        );
105
106        y -= 7.0;
107        current_layer.use_text(
108            format!("Start: {}", report.start_time.format("%Y-%m-%d %H:%M:%S UTC")),
109            10.0,
110            Mm(20.0),
111            Mm(y),
112            &font,
113        );
114
115        y -= 5.0;
116        current_layer.use_text(
117            format!("End: {}", report.end_time.format("%Y-%m-%d %H:%M:%S UTC")),
118            10.0,
119            Mm(20.0),
120            Mm(y),
121            &font,
122        );
123
124        y -= 5.0;
125        current_layer.use_text(
126            format!("Duration: {}s", report.duration_seconds),
127            10.0,
128            Mm(20.0),
129            Mm(y),
130            &font,
131        );
132
133        y -= 5.0;
134        current_layer.use_text(
135            format!("Status: {}", report.status),
136            10.0,
137            Mm(20.0),
138            Mm(y),
139            &font_bold,
140        );
141
142        // Summary section
143        y -= 15.0;
144        current_layer.use_text("Summary", 14.0, Mm(20.0), Mm(y), &font_bold);
145
146        y -= 7.0;
147        current_layer.use_text(
148            format!("Total Steps: {}", report.total_steps),
149            10.0,
150            Mm(20.0),
151            Mm(y),
152            &font,
153        );
154
155        y -= 5.0;
156        current_layer.use_text(
157            format!("Completed: {}", report.completed_steps),
158            10.0,
159            Mm(20.0),
160            Mm(y),
161            &font,
162        );
163
164        y -= 5.0;
165        current_layer.use_text(
166            format!("Failed: {}", report.failed_steps),
167            10.0,
168            Mm(20.0),
169            Mm(y),
170            &font,
171        );
172
173        // Metrics section
174        if self.config.include_metrics {
175            y -= 15.0;
176            current_layer.use_text("Metrics", 14.0, Mm(20.0), Mm(y), &font_bold);
177
178            y -= 7.0;
179            current_layer.use_text(
180                format!("Total Requests: {}", report.metrics.total_requests),
181                10.0,
182                Mm(20.0),
183                Mm(y),
184                &font,
185            );
186
187            y -= 5.0;
188            current_layer.use_text(
189                format!("Error Rate: {:.2}%", report.metrics.error_rate * 100.0),
190                10.0,
191                Mm(20.0),
192                Mm(y),
193                &font,
194            );
195
196            y -= 5.0;
197            current_layer.use_text(
198                format!("Avg Latency: {:.2}ms", report.metrics.avg_latency_ms),
199                10.0,
200                Mm(20.0),
201                Mm(y),
202                &font,
203            );
204
205            y -= 5.0;
206            current_layer.use_text(
207                format!("P95 Latency: {:.2}ms", report.metrics.p95_latency_ms),
208                10.0,
209                Mm(20.0),
210                Mm(y),
211                &font,
212            );
213        }
214
215        // Failures section
216        if !report.failures.is_empty() {
217            y -= 15.0;
218            current_layer.use_text("Failures", 14.0, Mm(20.0), Mm(y), &font_bold);
219
220            for failure in &report.failures {
221                y -= 7.0;
222                if y < 20.0 {
223                    break; // Page boundary - would need to add new page
224                }
225                current_layer.use_text(
226                    format!("• {}: {}", failure.step_name, failure.error_message),
227                    9.0,
228                    Mm(25.0),
229                    Mm(y),
230                    &font,
231                );
232            }
233        }
234
235        // Recommendations section
236        if self.config.include_recommendations && !report.recommendations.is_empty() {
237            y -= 15.0;
238            current_layer.use_text("Recommendations", 14.0, Mm(20.0), Mm(y), &font_bold);
239
240            for recommendation in &report.recommendations {
241                y -= 7.0;
242                if y < 20.0 {
243                    break;
244                }
245                current_layer.use_text(
246                    format!("• {}", recommendation),
247                    9.0,
248                    Mm(25.0),
249                    Mm(y),
250                    &font,
251                );
252            }
253        }
254
255        // Footer
256        current_layer.use_text(
257            format!("Generated by MockForge on {}", Utc::now().format("%Y-%m-%d %H:%M UTC")),
258            8.0,
259            Mm(20.0),
260            Mm(10.0),
261            &font,
262        );
263
264        // Save PDF
265        doc.save(&mut BufWriter::new(File::create(output_path)?))
266            .map_err(|e| ReportingError::Pdf(e.to_string()))?;
267
268        Ok(())
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use tempfile::tempdir;
276
277    #[test]
278    fn test_pdf_generation() {
279        let config = PdfConfig::default();
280        let generator = PdfReportGenerator::new(config);
281
282        let report = ExecutionReport {
283            orchestration_name: "test-orch".to_string(),
284            start_time: Utc::now(),
285            end_time: Utc::now(),
286            duration_seconds: 120,
287            status: "Completed".to_string(),
288            total_steps: 5,
289            completed_steps: 5,
290            failed_steps: 0,
291            metrics: ReportMetrics {
292                total_requests: 1000,
293                successful_requests: 980,
294                failed_requests: 20,
295                avg_latency_ms: 125.5,
296                p95_latency_ms: 250.0,
297                p99_latency_ms: 350.0,
298                error_rate: 0.02,
299            },
300            failures: vec![],
301            recommendations: vec!["Increase timeout thresholds".to_string()],
302        };
303
304        let temp_dir = tempdir().unwrap();
305        let output_path = temp_dir.path().join("report.pdf");
306
307        let result = generator.generate(&report, output_path.to_str().unwrap());
308        assert!(result.is_ok());
309        assert!(output_path.exists());
310    }
311}