sentinel_core/report/
json.rs1use crate::report::{Report, ReportSink};
4
5pub struct JsonReportSink;
7
8impl ReportSink for JsonReportSink {
9 type Error = JsonReportError;
10
11 fn emit(&self, report: &Report) -> Result<(), Self::Error> {
12 use std::io::Write as _;
13 let stdout = std::io::stdout();
14 let mut lock = stdout.lock();
15 serde_json::to_writer_pretty(&mut lock, report)
16 .map_err(|e| JsonReportError(e.to_string()))?;
17 lock.write_all(b"\n")
18 .map_err(|e| JsonReportError(e.to_string()))?;
19 Ok(())
20 }
21}
22
23#[derive(Debug, thiserror::Error)]
25#[error("JSON report error: {0}")]
26pub struct JsonReportError(String);
27
28#[cfg(test)]
29mod tests {
30 use super::*;
31 use crate::report::{Analysis, QualityGate, Report};
32 use crate::test_helpers::empty_report;
33
34 #[test]
35 fn emit_empty_report() {
36 let sink = JsonReportSink;
37 let report = empty_report();
38 assert!(sink.emit(&report).is_ok());
39 }
40
41 #[test]
42 fn error_display() {
43 let err = JsonReportError("test".to_string());
44 assert_eq!(format!("{err}"), "JSON report error: test");
45 }
46
47 #[test]
48 fn emit_report_with_findings() {
49 use crate::detect::{Finding, FindingType, Pattern, Severity};
50
51 let report = Report {
52 analysis: Analysis {
53 duration_ms: 42,
54 events_processed: 10,
55 traces_analyzed: 1,
56 },
57 findings: vec![Finding {
58 finding_type: FindingType::NPlusOneSql,
59 severity: Severity::Warning,
60 trace_id: "trace-1".to_string(),
61 service: "order-svc".to_string(),
62 source_endpoint: "POST /api/orders/42/submit".to_string(),
63 pattern: Pattern {
64 template: "SELECT * FROM order_item WHERE order_id = ?".to_string(),
65 occurrences: 6,
66 window_ms: 250,
67 distinct_params: 6,
68 ..Default::default()
69 },
70 suggestion: "Use WHERE ... IN (?) to batch 6 queries into one".to_string(),
71 first_timestamp: "2025-07-10T14:32:01.000Z".to_string(),
72 last_timestamp: "2025-07-10T14:32:01.250Z".to_string(),
73 green_impact: None,
74 confidence: crate::detect::Confidence::default(),
75 classification_method: None,
76 code_location: None,
77 instrumentation_scopes: Vec::new(),
78 suggested_fix: None,
79 signature: String::new(),
80 }],
81 green_summary: crate::test_helpers::make_test_green_summary(10, 5, 0.5),
82 quality_gate: QualityGate {
83 passed: true,
84 rules: vec![],
85 },
86 per_endpoint_io_ops: vec![],
87 correlations: vec![],
88 warnings: vec![],
89 warning_details: vec![],
90 acknowledged_findings: vec![],
91 binary_version: String::new(),
92 disclosure_waste: None,
93 };
94
95 let json = serde_json::to_string_pretty(&report).unwrap();
96 assert!(json.contains("n_plus_one_sql"));
97 assert!(json.contains("trace-1"));
98 assert!(json.contains("order_id"));
99 assert!(json.contains("\"occurrences\": 6"));
100 assert!(json.contains("\"io_waste_ratio\": 0.5"));
101 assert!(json.contains("\"first_timestamp\""));
102 assert!(json.contains("\"last_timestamp\""));
103
104 assert!(
113 json.contains("\"io_waste_ratio_band\": \"critical\""),
114 "io_waste_ratio_band missing or not `critical` for 0.5 waste: {json}"
115 );
116 }
117}