use crate::config::Config;
use crate::correlate;
use crate::detect;
use crate::detect::DetectConfig;
use crate::event::SpanEvent;
use crate::normalize;
use crate::report::{Analysis, Report};
use crate::score;
#[must_use]
pub fn analyze(events: Vec<SpanEvent>, config: &Config) -> Report {
analyze_with_traces(events, config).0
}
#[must_use]
pub fn analyze_with_traces(
events: Vec<SpanEvent>,
config: &Config,
) -> (Report, Vec<correlate::Trace>) {
let start = std::time::Instant::now();
let event_count = events.len();
let normalized = normalize::normalize_all(events);
let traces = correlate::correlate(normalized);
let trace_count = traces.len();
let detect_config = DetectConfig::from(config);
let mut findings = detect::detect(&traces, &detect_config);
let cross_trace = detect::slow::detect_slow_cross_trace(
&traces,
detect_config.slow_threshold_ms,
detect_config.slow_min_occurrences,
);
findings.extend(cross_trace);
let (mut findings, green_summary) = if config.green_enabled {
score::score_green(&traces, findings, config.green_region.as_deref())
} else {
let total_io_ops = traces.iter().map(|t| t.spans.len()).sum();
(
findings,
crate::report::GreenSummary::disabled(total_io_ops),
)
};
detect::sort_findings(&mut findings);
let quality_gate = crate::quality_gate::evaluate(&findings, &green_summary, config);
let report = Report {
analysis: Analysis {
duration_ms: start.elapsed().as_millis() as u64,
events_processed: event_count,
traces_analyzed: trace_count,
},
findings,
green_summary,
quality_gate,
};
(report, traces)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::SpanEvent;
#[test]
fn empty_pipeline_produces_empty_report() {
let config = Config::default();
let report = analyze(vec![], &config);
assert!(report.findings.is_empty());
assert_eq!(report.analysis.events_processed, 0);
assert_eq!(report.analysis.traces_analyzed, 0);
assert!(report.quality_gate.passed);
}
#[test]
fn waste_dedup_no_double_count() {
use crate::test_helpers::make_sql_event;
let mut events: Vec<SpanEvent> = (1..=5)
.map(|i| {
make_sql_event(
"trace-1",
&format!("span-{i}"),
&format!("SELECT * FROM order_item WHERE order_id = {i}"),
&format!("2025-07-10T14:32:01.{:03}Z", i * 50),
)
})
.collect();
for i in 6..=7 {
events.push(make_sql_event(
"trace-1",
&format!("span-{i}"),
"SELECT * FROM order_item WHERE order_id = 1",
&format!("2025-07-10T14:32:01.{:03}Z", i * 40),
));
}
let config = Config::default();
let report = analyze(events, &config);
assert!(!report.findings.is_empty());
assert_eq!(report.green_summary.avoidable_io_ops, 6);
}
#[test]
fn zero_events_waste_ratio_is_zero() {
let config = Config::default();
let report = analyze(vec![], &config);
assert!((report.green_summary.io_waste_ratio - 0.0).abs() < f64::EPSILON);
assert_eq!(report.green_summary.total_io_ops, 0);
assert_eq!(report.green_summary.avoidable_io_ops, 0);
}
#[test]
fn clean_events_zero_waste_ratio() {
use crate::test_helpers::make_sql_event;
let events = vec![
make_sql_event(
"trace-1",
"span-1",
"SELECT * FROM users WHERE id = 1",
"2025-07-10T14:32:01.000Z",
),
make_sql_event(
"trace-1",
"span-2",
"SELECT * FROM orders WHERE id = 2",
"2025-07-10T14:32:01.050Z",
),
make_sql_event(
"trace-1",
"span-3",
"SELECT * FROM products WHERE id = 3",
"2025-07-10T14:32:01.100Z",
),
make_sql_event(
"trace-1",
"span-4",
"INSERT INTO logs (msg) VALUES ('ok')",
"2025-07-10T14:32:01.150Z",
),
];
let config = Config::default();
let report = analyze(events, &config);
assert!(report.findings.is_empty());
assert_eq!(report.green_summary.total_io_ops, 4);
assert_eq!(report.green_summary.avoidable_io_ops, 0);
assert!((report.green_summary.io_waste_ratio - 0.0).abs() < f64::EPSILON);
}
#[test]
fn pipeline_with_findings_computes_green_summary() {
use crate::test_helpers::make_sql_event;
let events: Vec<SpanEvent> = (1..=6)
.map(|i| {
make_sql_event(
"trace-1",
&format!("span-{i}"),
&format!("SELECT * FROM order_item WHERE order_id = {i}"),
&format!("2025-07-10T14:32:01.{:03}Z", i * 50),
)
})
.collect();
let config = Config::default();
let report = analyze(events, &config);
assert!(!report.findings.is_empty());
assert_eq!(report.green_summary.avoidable_io_ops, 5);
assert!((report.green_summary.io_waste_ratio - 5.0_f64 / 6.0).abs() < f64::EPSILON);
assert_eq!(report.green_summary.total_io_ops, 6);
}
#[test]
fn dedup_across_traces() {
use crate::test_helpers::make_sql_event;
let mut events = Vec::new();
for i in 1..=3 {
events.push(make_sql_event(
"trace-A",
&format!("span-a{i}"),
"SELECT * FROM order_item WHERE order_id = 42",
&format!("2025-07-10T14:32:01.{:03}Z", i * 50),
));
}
for i in 1..=3 {
events.push(make_sql_event(
"trace-B",
&format!("span-b{i}"),
"SELECT * FROM orders WHERE user_id = 7",
&format!("2025-07-10T14:32:02.{:03}Z", i * 50),
));
}
let config = Config::default();
let report = analyze(events, &config);
assert_eq!(report.green_summary.avoidable_io_ops, 4);
assert_eq!(report.green_summary.total_io_ops, 6);
}
#[test]
fn pipeline_with_green_region_produces_co2() {
use crate::test_helpers::make_sql_event;
let events: Vec<SpanEvent> = (1..=6)
.map(|i| {
make_sql_event(
"trace-1",
&format!("span-{i}"),
&format!("SELECT * FROM order_item WHERE order_id = {i}"),
&format!("2025-07-10T14:32:01.{:03}Z", i * 50),
)
})
.collect();
let config = Config {
green_region: Some("eu-west-3".to_string()),
..Config::default()
};
let report = analyze(events, &config);
assert!(report.green_summary.estimated_co2_grams.is_some());
assert!(report.green_summary.avoidable_co2_grams.is_some());
assert!(report.green_summary.estimated_co2_grams.unwrap() > 0.0);
}
#[test]
fn pipeline_without_region_no_co2() {
let config = Config::default();
let report = analyze(vec![], &config);
assert!(report.green_summary.estimated_co2_grams.is_none());
assert!(report.green_summary.avoidable_co2_grams.is_none());
}
#[test]
fn green_disabled_skips_scoring() {
use crate::test_helpers::make_sql_event;
let events: Vec<SpanEvent> = (1..=6)
.map(|i| {
make_sql_event(
"trace-1",
&format!("span-{i}"),
&format!("SELECT * FROM order_item WHERE order_id = {i}"),
&format!("2025-07-10T14:32:01.{:03}Z", i * 50),
)
})
.collect();
let config = Config {
green_enabled: false,
..Config::default()
};
let report = analyze(events, &config);
assert!(!report.findings.is_empty());
assert_eq!(report.green_summary.avoidable_io_ops, 0);
assert!((report.green_summary.io_waste_ratio - 0.0).abs() < f64::EPSILON);
assert!(report.green_summary.top_offenders.is_empty());
assert!(report.green_summary.estimated_co2_grams.is_none());
assert!(report.green_summary.avoidable_co2_grams.is_none());
assert_eq!(report.green_summary.total_io_ops, 6);
for f in &report.findings {
assert!(f.green_impact.is_none());
}
}
#[test]
fn green_disabled_with_region_still_no_co2() {
let config = Config {
green_enabled: false,
green_region: Some("eu-west-3".to_string()),
..Config::default()
};
let report = analyze(vec![], &config);
assert!(report.green_summary.estimated_co2_grams.is_none());
}
}