pub mod html;
pub mod interpret;
pub mod json;
pub mod metrics;
pub mod periodic;
pub mod sarif;
pub mod warnings;
pub use self::warnings::Warning;
use crate::correlate::Trace;
use crate::detect::Finding;
use crate::detect::correlate_cross::CrossTraceCorrelation;
use crate::report::interpret::InterpretationLevel;
use crate::score::carbon::{CarbonReport, RegionBreakdown, ScoringConfig};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
pub analysis: Analysis,
pub findings: Vec<Finding>,
pub green_summary: GreenSummary,
pub quality_gate: QualityGate,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub per_endpoint_io_ops: Vec<PerEndpointIoOps>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub correlations: Vec<CrossTraceCorrelation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warning_details: Vec<Warning>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub acknowledged_findings: Vec<AcknowledgedFinding>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub binary_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disclosure_waste: Option<DisclosureWaste>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcknowledgedFinding {
pub finding: Finding,
pub acknowledgment: crate::acknowledgments::Acknowledgment,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AvoidableTier {
pub n_plus_one_threshold: u32,
pub avoidable_io_ops: usize,
pub avoidable_kwh: f64,
pub avoidable_gco2: f64,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DisclosureWaste {
pub canonical: AvoidableTier,
pub operational: AvoidableTier,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Analysis {
pub duration_ms: u64,
pub events_processed: usize,
pub traces_analyzed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GreenSummary {
pub total_io_ops: usize,
pub avoidable_io_ops: usize,
#[serde(skip)]
pub accounted_io_ops: usize,
pub io_waste_ratio: f64,
pub io_waste_ratio_band: InterpretationLevel,
pub top_offenders: Vec<TopOffender>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub co2: Option<CarbonReport>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub regions: Vec<RegionBreakdown>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport_gco2: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scoring_config: Option<ScoringConfig>,
#[serde(default)]
pub energy_kwh: f64,
#[serde(default)]
pub energy_model: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub per_service_carbon_kgco2eq: BTreeMap<String, f64>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub per_service_energy_kwh: BTreeMap<String, f64>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub per_service_region: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub per_service_energy_model: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub per_service_measured_ratio: BTreeMap<String, f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PerEndpointIoOps {
pub service: String,
pub endpoint: String,
pub io_ops: usize,
}
#[must_use]
pub fn compute_per_endpoint_io_ops(traces: &[Trace]) -> Vec<PerEndpointIoOps> {
let mut counts: BTreeMap<(&str, &str), usize> = BTreeMap::new();
for trace in traces {
for span in &trace.spans {
let key = (
span.event.service.as_ref(),
span.event.source.endpoint.as_str(),
);
*counts.entry(key).or_insert(0) += 1;
}
}
counts
.into_iter()
.map(|((service, endpoint), io_ops)| PerEndpointIoOps {
service: service.to_string(),
endpoint: endpoint.to_string(),
io_ops,
})
.collect()
}
impl GreenSummary {
#[must_use]
pub fn disabled(total_io_ops: usize) -> Self {
Self {
total_io_ops,
avoidable_io_ops: 0,
accounted_io_ops: total_io_ops,
io_waste_ratio: 0.0,
io_waste_ratio_band: InterpretationLevel::Healthy,
top_offenders: vec![],
co2: None,
regions: vec![],
transport_gco2: None,
scoring_config: None,
energy_kwh: 0.0,
energy_model: String::new(),
per_service_carbon_kgco2eq: BTreeMap::new(),
per_service_energy_kwh: BTreeMap::new(),
per_service_region: BTreeMap::new(),
per_service_energy_model: BTreeMap::new(),
per_service_measured_ratio: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TopOffender {
pub endpoint: String,
pub service: String,
pub io_intensity_score: f64,
pub io_intensity_band: InterpretationLevel,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub co2_grams: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGate {
pub passed: bool,
pub rules: Vec<QualityRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityRule {
pub rule: String,
pub threshold: f64,
pub actual: f64,
pub passed: bool,
}
pub trait ReportSink {
type Error: std::error::Error;
fn emit(&self, report: &Report) -> Result<(), Self::Error>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn green_summary_pre_0512_baseline_loads_without_scoring_config() {
let json = r#"{
"total_io_ops": 0,
"avoidable_io_ops": 0,
"io_waste_ratio": 0.0,
"io_waste_ratio_band": "healthy",
"top_offenders": []
}"#;
let summary: GreenSummary = serde_json::from_str(json).expect("backward-compat parse");
assert!(summary.scoring_config.is_none());
}
#[test]
fn green_summary_disabled_factory_has_no_scoring_config() {
let summary = GreenSummary::disabled(0);
assert!(summary.scoring_config.is_none());
}
#[test]
fn green_summary_skips_scoring_config_when_none() {
let summary = GreenSummary::disabled(42);
let json = serde_json::to_string(&summary).unwrap();
assert!(
!json.contains("scoring_config"),
"scoring_config should be skipped when None, got: {json}"
);
}
fn minimal_report_json_without_warning_details() -> String {
r#"{
"analysis": {"duration_ms": 0, "events_processed": 0, "traces_analyzed": 0},
"findings": [],
"green_summary": {
"total_io_ops": 0,
"avoidable_io_ops": 0,
"io_waste_ratio": 0.0,
"io_waste_ratio_band": "healthy",
"top_offenders": []
},
"quality_gate": {"passed": true, "rules": []},
"warnings": ["legacy warning text"]
}"#
.to_string()
}
#[test]
fn report_warning_details_default_empty_when_absent() {
let report: Report =
serde_json::from_str(&minimal_report_json_without_warning_details()).expect("parse");
assert!(report.warning_details.is_empty());
}
#[test]
fn report_legacy_warnings_field_still_parses() {
let report: Report =
serde_json::from_str(&minimal_report_json_without_warning_details()).expect("parse");
assert_eq!(report.warnings, vec!["legacy warning text".to_string()]);
assert!(report.warning_details.is_empty());
}
#[test]
fn report_warning_details_skipped_in_serialize_when_empty() {
let report = crate::test_helpers::empty_report();
let json = serde_json::to_string(&report).expect("serialize");
assert!(
!json.contains("warning_details"),
"warning_details should be skipped when empty, got: {json}"
);
}
#[test]
fn report_warning_details_serialized_when_present() {
let mut report = crate::test_helpers::empty_report();
report.warning_details = vec![
Warning::new("cold_start", "msg one"),
Warning::new("ingestion_drops", "msg two"),
];
let json = serde_json::to_string(&report).expect("serialize");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse");
let array = parsed
.get("warning_details")
.and_then(|v| v.as_array())
.expect("warning_details array");
assert_eq!(array.len(), 2);
assert_eq!(array[0]["kind"], "cold_start");
assert_eq!(array[1]["kind"], "ingestion_drops");
}
#[test]
fn green_summary_roundtrip_with_new_carbon_attribution_fields() {
let mut per_service_carbon = BTreeMap::new();
per_service_carbon.insert("checkout".to_string(), 0.42);
per_service_carbon.insert("catalog".to_string(), 0.11);
let mut per_service_energy = BTreeMap::new();
per_service_energy.insert("checkout".to_string(), 0.0021);
per_service_energy.insert("catalog".to_string(), 0.0005);
let mut per_service_region = BTreeMap::new();
per_service_region.insert("checkout".to_string(), "eu-west-3".to_string());
per_service_region.insert("catalog".to_string(), "unknown".to_string());
let mut per_service_energy_model = BTreeMap::new();
per_service_energy_model.insert("checkout".to_string(), "scaphandre_rapl".to_string());
per_service_energy_model.insert("catalog".to_string(), "io_proxy_v3+cal".to_string());
let mut per_service_measured_ratio = BTreeMap::new();
per_service_measured_ratio.insert("checkout".to_string(), 0.75);
per_service_measured_ratio.insert("catalog".to_string(), 0.0);
let summary = GreenSummary {
energy_kwh: 0.0026,
energy_model: "scaphandre_rapl+cal".to_string(),
per_service_carbon_kgco2eq: per_service_carbon.clone(),
per_service_energy_kwh: per_service_energy.clone(),
per_service_region: per_service_region.clone(),
per_service_energy_model: per_service_energy_model.clone(),
per_service_measured_ratio: per_service_measured_ratio.clone(),
..GreenSummary::disabled(0)
};
let json = serde_json::to_string(&summary).expect("serialize");
let parsed: GreenSummary = serde_json::from_str(&json).expect("deserialize");
assert!((parsed.energy_kwh - 0.0026).abs() < 1e-12);
assert_eq!(parsed.energy_model, "scaphandre_rapl+cal");
assert_eq!(parsed.per_service_carbon_kgco2eq, per_service_carbon);
assert_eq!(parsed.per_service_energy_kwh, per_service_energy);
assert_eq!(parsed.per_service_region, per_service_region);
assert_eq!(parsed.per_service_energy_model, per_service_energy_model);
assert_eq!(
parsed.per_service_measured_ratio,
per_service_measured_ratio
);
}
#[test]
fn green_summary_legacy_baseline_deserializes_with_default_carbon_attribution() {
let legacy = serde_json::json!({
"total_io_ops": 100,
"avoidable_io_ops": 5,
"io_waste_ratio": 0.05,
"io_waste_ratio_band": "healthy",
"top_offenders": []
});
let parsed: GreenSummary = serde_json::from_value(legacy).expect("deserialize legacy");
assert!(parsed.energy_kwh.abs() < f64::EPSILON);
assert!(parsed.energy_model.is_empty());
assert!(parsed.per_service_carbon_kgco2eq.is_empty());
assert!(parsed.per_service_energy_kwh.is_empty());
assert!(parsed.per_service_region.is_empty());
assert!(parsed.per_service_energy_model.is_empty());
assert!(parsed.per_service_measured_ratio.is_empty());
}
}