use sentinel_core::detect::Severity;
use sentinel_core::report::json::JsonReportSink;
use sentinel_core::report::{Report, ReportSink};
use crate::OutputFormat;
pub(crate) fn emit_report_and_gate(
report: &Report,
format: Option<OutputFormat>,
ci: bool,
label: &str,
) {
let effective_format = format.unwrap_or(if ci {
OutputFormat::Json
} else {
OutputFormat::Text
});
match effective_format {
OutputFormat::Text => {
print_colored_report(report, label);
}
OutputFormat::Json => {
let sink = JsonReportSink;
if let Err(e) = sink.emit(report) {
eprintln!("Error writing report: {e}");
std::process::exit(1);
}
}
OutputFormat::Sarif => {
if let Err(e) = sentinel_core::report::sarif::emit_sarif(report) {
eprintln!("Error writing SARIF report: {e}");
std::process::exit(1);
}
}
}
if ci && !report.quality_gate.passed {
eprintln!("Quality gate FAILED");
std::process::exit(1);
}
}
pub(crate) fn print_colored_report(report: &Report, title: &str) {
format_colored_report(report, title, false);
}
#[derive(Clone, Copy)]
pub(crate) struct AnsiColors {
pub(crate) bold: &'static str,
pub(crate) cyan: &'static str,
pub(crate) red: &'static str,
pub(crate) yellow: &'static str,
pub(crate) green: &'static str,
pub(crate) dim: &'static str,
pub(crate) reset: &'static str,
}
pub(crate) fn ansi_colors(force_color: bool) -> AnsiColors {
use std::io::IsTerminal;
if force_color || std::io::stdout().is_terminal() {
AnsiColors {
bold: "\x1b[1m",
cyan: "\x1b[36m",
red: "\x1b[31m",
yellow: "\x1b[33m",
green: "\x1b[32m",
dim: "\x1b[2m",
reset: "\x1b[0m",
}
} else {
AnsiColors {
bold: "",
cyan: "",
red: "",
yellow: "",
green: "",
dim: "",
reset: "",
}
}
}
pub(crate) fn interpret_color(
level: sentinel_core::InterpretationLevel,
colors: AnsiColors,
) -> &'static str {
use sentinel_core::InterpretationLevel::{Critical, Healthy, High, Moderate};
match level {
Critical => colors.red,
High => colors.yellow,
Moderate => "",
Healthy => colors.green,
}
}
pub(crate) fn format_colored_report(report: &Report, title: &str, force_color: bool) {
let colors = ansi_colors(force_color);
let AnsiColors {
bold,
cyan,
green,
dim,
reset,
..
} = colors;
println!();
println!("{bold}{cyan}=== perf-sentinel {title} ==={reset}");
println!(
"{dim}Analyzed {} events across {} traces in {}ms{reset}",
report.analysis.events_processed,
report.analysis.traces_analyzed,
report.analysis.duration_ms
);
println!();
if report.findings.is_empty() {
println!("{green}No performance anti-patterns detected.{reset}");
} else {
print_findings(&report.findings, force_color);
}
print_green_summary(&report.green_summary, force_color);
print_quality_gate(&report.quality_gate, force_color);
}
pub(crate) fn print_findings(findings: &[sentinel_core::detect::Finding], force_color: bool) {
let colors = ansi_colors(force_color);
println!(
"{}Found {} issue(s):{}",
colors.bold,
findings.len(),
colors.reset
);
println!();
for (i, finding) in findings.iter().enumerate() {
print_finding_entry(i, finding, colors);
println!();
}
}
fn print_finding_entry(index: usize, finding: &sentinel_core::detect::Finding, colors: AnsiColors) {
let AnsiColors {
bold,
cyan,
dim,
reset,
..
} = colors;
let severity_color = severity_color(&finding.severity, colors);
let severity_label = severity_label(&finding.severity);
let type_label = finding.finding_type.display_label();
println!(
" {bold}{severity_color}[{severity_label}] #{} {type_label}{reset}",
index + 1,
);
println!(" {dim}Trace:{reset} {}", finding.trace_id);
println!(" {dim}Service:{reset} {}", finding.service);
println!(" {dim}Endpoint:{reset} {}", finding.source_endpoint);
if let Some(ref loc) = finding.code_location {
let src = loc.display_string();
if !src.is_empty() {
println!(" {dim}Source:{reset} {src}");
}
}
println!(" {dim}Template:{reset} {}", finding.pattern.template);
println!(
" {dim}Hits:{reset} {} occurrences, {} distinct params, {}ms window",
finding.pattern.occurrences, finding.pattern.distinct_params, finding.pattern.window_ms
);
println!(
" {dim}Window:{reset} {} -> {}",
finding.first_timestamp, finding.last_timestamp
);
println!(" {cyan}Suggestion:{reset} {}", finding.suggestion);
if let Some(ref impact) = finding.green_impact {
print_finding_impact(impact, colors);
}
}
fn print_finding_impact(impact: &sentinel_core::detect::GreenImpact, colors: AnsiColors) {
let AnsiColors { dim, reset, .. } = colors;
println!(
" {dim}Extra I/O:{reset} {} avoidable ops",
impact.estimated_extra_io_ops
);
let level = impact.io_intensity_band;
let level_color = interpret_color(level, colors);
println!(
" {dim}IIS:{reset} {:.1} {level_color}({}){reset}",
impact.io_intensity_score,
level.short_label(),
);
}
fn severity_color(severity: &Severity, colors: AnsiColors) -> &'static str {
match severity {
Severity::Critical => colors.red,
Severity::Warning => colors.yellow,
Severity::Info => colors.dim,
}
}
fn severity_label(severity: &Severity) -> &'static str {
match severity {
Severity::Critical => "CRITICAL",
Severity::Warning => "WARNING",
Severity::Info => "INFO",
}
}
fn print_green_summary(summary: &sentinel_core::report::GreenSummary, force_color: bool) {
let colors = ansi_colors(force_color);
let AnsiColors {
bold,
cyan,
dim,
reset,
..
} = colors;
println!("{bold}{cyan}--- GreenOps Summary ---{reset}");
println!(" Total I/O ops: {}", summary.total_io_ops);
println!(" Avoidable I/O ops: {}", summary.avoidable_io_ops);
let waste_level = summary.io_waste_ratio_band;
let waste_color = interpret_color(waste_level, colors);
println!(
" I/O waste ratio: {:.1}% {waste_color}({}){reset}",
summary.io_waste_ratio * 100.0,
waste_level.short_label(),
);
if let Some(carbon) = summary.co2.as_ref() {
println!(
" Est. CO\u{2082}: {:.6} g (low {:.6}, high {:.6}, model {})",
carbon.total.mid, carbon.total.low, carbon.total.high, carbon.total.model,
);
println!(
" Avoidable CO\u{2082}: {:.6} g (low {:.6}, high {:.6})",
carbon.avoidable.mid, carbon.avoidable.low, carbon.avoidable.high,
);
println!(
" Operational: {:.6} g Embodied: {:.6} g Methodology: {}",
carbon.operational_gco2, carbon.embodied_gco2, carbon.total.methodology,
);
if let Some(transport) = carbon.transport_gco2 {
println!(" Transport: {transport:.6} g (cross-region network bytes)");
}
}
if summary.regions.len() > 1 {
println!();
println!(" {bold}Per-region breakdown:{reset}");
for region in &summary.regions {
println!(
" - {}: {} I/O ops, {:.6} gCO\u{2082}",
region.region, region.io_ops, region.co2_gco2,
);
}
}
if !summary.top_offenders.is_empty() {
println!();
println!(" {bold}Top offenders:{reset}");
for offender in &summary.top_offenders {
let level = offender.io_intensity_band;
let level_color = interpret_color(level, colors);
let co2_str = offender
.co2_grams
.map_or(String::new(), |co2| format!(", {co2:.6} gCO\u{2082}"));
println!(
" - {}: IIS {:.1} {level_color}({}){reset} (service: {}){co2_str}",
offender.endpoint,
offender.io_intensity_score,
level.short_label(),
offender.service,
);
}
}
if summary.co2.is_some() {
println!();
println!(
" {dim}Note: CO\u{2082} estimates have ~2\u{00d7} multiplicative uncertainty \
(low = mid/2, high = mid\u{00d7}2). See docs/LIMITATIONS.md.{reset}"
);
}
println!(
" {dim}Note: `(healthy/moderate/high/critical)` bands use fixed heuristic \
thresholds, independent of your `n_plus_one_threshold` / \
`io_waste_ratio_max` overrides. See README \"How to read the report\".{reset}"
);
println!();
}
fn print_quality_gate(gate: &sentinel_core::report::QualityGate, force_color: bool) {
let AnsiColors {
bold,
red,
green,
reset,
..
} = ansi_colors(force_color);
let gate_color = if gate.passed { green } else { red };
let gate_label = if gate.passed { "PASSED" } else { "FAILED" };
println!("{bold}Quality gate: {gate_color}{gate_label}{reset}");
println!();
}