1use crate::{ReportingError, Result};
4use chrono::{DateTime, Utc};
5use printpdf::*;
6use serde::{Deserialize, Serialize};
7use std::fs::File;
8use std::io::BufWriter;
9
10#[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#[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#[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#[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
68pub struct PdfReportGenerator {
70 config: PdfConfig,
71}
72
73impl PdfReportGenerator {
74 pub fn new(config: PdfConfig) -> Self {
76 Self { config }
77 }
78
79 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 current_layer.use_text(&self.config.title, 24.0, Mm(20.0), Mm(270.0), &font_bold);
95
96 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 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 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 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; }
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 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 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 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}