use std::borrow::Cow;
use std::collections::HashMap;
use sentinel_core::detect::Severity;
use sentinel_core::report::json::JsonReportSink;
use sentinel_core::report::{Report, ReportSink};
use sentinel_core::score::carbon::IntensitySource;
use sentinel_core::text_safety::{safe_url, sanitize_for_terminal, strip_code_ticks};
use crate::OutputFormat;
pub(crate) fn emit_report_and_gate(
report: &mut Report,
format: Option<OutputFormat>,
ci: bool,
label: &str,
sort: Option<FindingsSort>,
embed_traces: Option<Vec<sentinel_core::correlate::Trace>>,
show_acknowledged: bool,
) {
let effective_format = effective_format(format, ci);
if let Some(mode) = sort {
sort_findings(&mut report.findings, mode);
}
if let Some(traces) = embed_traces
&& matches!(effective_format, OutputFormat::Json)
{
sentinel_core::report::embedded::embed_finding_traces(report, &traces);
}
let write_result: Result<(), String> = match effective_format {
OutputFormat::Text => {
format_colored_report_with_acks(report, label, false, show_acknowledged);
Ok(())
}
OutputFormat::Json => with_optional_acks_hidden(report, show_acknowledged, |r| {
let sink = JsonReportSink;
sink.emit(r)
.map_err(|e| format!("Error writing report: {e}"))
}),
OutputFormat::Sarif => with_optional_acks_hidden(report, show_acknowledged, |r| {
sentinel_core::report::sarif::emit_sarif(r)
.map_err(|e| format!("Error writing SARIF report: {e}"))
}),
};
let gate_failed = ci && !report.quality_gate.passed;
match exit_code_after_gate(gate_failed, write_result.is_err()) {
None => {}
Some(1) => {
eprintln!("Quality gate FAILED");
std::process::exit(1);
}
Some(code) => {
if let Err(msg) = write_result {
eprintln!("{msg}");
}
std::process::exit(code);
}
}
}
pub(crate) fn effective_format(format: Option<OutputFormat>, ci: bool) -> OutputFormat {
format.unwrap_or(if ci {
OutputFormat::Json
} else {
OutputFormat::Text
})
}
fn exit_code_after_gate(gate_failed: bool, write_failed: bool) -> Option<i32> {
if gate_failed {
Some(1)
} else if write_failed {
Some(crate::EXIT_TOOLING_ERROR)
} else {
None
}
}
fn with_optional_acks_hidden<F>(
report: &mut Report,
show_acknowledged: bool,
emit: F,
) -> Result<(), String>
where
F: FnOnce(&Report) -> Result<(), String>,
{
let stash = if show_acknowledged || report.acknowledged_findings.is_empty() {
Vec::new()
} else {
std::mem::take(&mut report.acknowledged_findings)
};
let result = emit(report);
if !stash.is_empty() {
report.acknowledged_findings = stash;
}
result
}
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 {
no_colors()
}
}
pub(crate) const fn no_colors() -> AnsiColors {
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,
}
}
const fn intensity_source_label(source: IntensitySource) -> &'static str {
match source {
IntensitySource::Annual => "annual",
IntensitySource::Hourly => "hourly",
IntensitySource::MonthlyHourly => "monthly_hourly",
IntensitySource::RealTime => "real_time",
}
}
pub(crate) fn format_span_timing(pattern: &sentinel_core::detect::Pattern) -> Option<String> {
let mut parts = Vec::with_capacity(3);
if let Some(p50) = pattern.span_duration_us_p50 {
parts.push(format!("p50 {}", format_duration_us(p50)));
}
if let Some(p99) = pattern.span_duration_us_p99 {
parts.push(format!("p99 {}", format_duration_us(p99)));
}
if let Some(cv) = pattern.span_duration_cv_x1000 {
parts.push(format!("CV {:.1}%", f64::from(cv) / 10.0));
}
(!parts.is_empty()).then(|| parts.join(" · "))
}
fn format_duration_us(us: u64) -> String {
#[allow(clippy::cast_precision_loss)]
let us_f = us as f64;
if us < 1_000 {
format!("{us} µs")
} else if us < 1_000_000 {
format!("{:.1} ms", us_f / 1_000.0)
} else {
format!("{:.2} s", us_f / 1_000_000.0)
}
}
pub(crate) fn classification_label(
finding: &sentinel_core::detect::Finding,
) -> Option<&'static str> {
if !finding.finding_type.as_str().starts_with("n_plus_one_") {
return None;
}
Some(match finding.classification_method {
Some(sentinel_core::detect::ClassificationMethod::SanitizerHeuristic) => {
"sanitizer heuristic"
}
_ => "direct",
})
}
fn format_estimation_suffix(
is_estimated: Option<bool>,
estimation_method: Option<&str>,
) -> Cow<'static, str> {
match (is_estimated, estimation_method) {
(Some(true), Some(method)) => Cow::Owned(format!(", estimated/{method}")),
(Some(true), None) => Cow::Borrowed(", estimated"),
(Some(false), _) => Cow::Borrowed(", measured"),
(None, _) => Cow::Borrowed(""),
}
}
fn format_scoring_config_line(
cfg: &sentinel_core::score::carbon::ScoringConfig,
) -> Cow<'static, str> {
use sentinel_core::score::electricity_maps::config::ApiVersion;
use sentinel_core::score::electricity_maps::config::EmissionFactorType;
use sentinel_core::score::electricity_maps::config::TemporalGranularity;
match (
cfg.api_version,
cfg.emission_factor_type,
cfg.temporal_granularity,
) {
(ApiVersion::V4, EmissionFactorType::Lifecycle, TemporalGranularity::Hourly) => {
Cow::Borrowed(" Carbon scoring: Electricity Maps v4, lifecycle, hourly")
}
(ApiVersion::V3, EmissionFactorType::Lifecycle, TemporalGranularity::Hourly) => {
Cow::Borrowed(" Carbon scoring: Electricity Maps v3, lifecycle, hourly")
}
(api, efm, tg) => Cow::Owned(format!(
" Carbon scoring: Electricity Maps {}, {}, {}",
api.as_chip_label(),
efm.as_query_value(),
tg.as_query_value(),
)),
}
}
pub(crate) fn format_colored_report(report: &Report, title: &str, force_color: bool) {
format_colored_report_with_acks(report, title, force_color, false);
}
pub(crate) fn format_colored_report_with_acks(
report: &Report,
title: &str,
force_color: bool,
show_acknowledged: 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
);
if let Some(ingest) = &report.analysis.ingest
&& ingest.spans_filtered > 0
{
let gaps = ingest.filtered_missing_db_statement + ingest.filtered_missing_http_url;
let gap_note = if gaps > 0 {
format!(", {gaps} I/O span(s) missing db.statement/http.url")
} else {
String::new()
};
println!(
"{dim}Ingested {} spans, {} filtered as non-analyzable{gap_note}{reset}",
ingest.spans_received, ingest.spans_filtered
);
}
println!();
print_warnings(report, force_color);
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,
report.analysis.traces_analyzed,
force_color,
);
print_quality_gate(&report.quality_gate, force_color, title == "demo");
print_acknowledged_summary(report, force_color, show_acknowledged);
}
#[cfg(feature = "tui")]
pub(crate) fn effective_warnings(report: &Report) -> Vec<sentinel_core::report::warnings::Warning> {
if report.warning_details.is_empty() {
report
.warnings
.iter()
.map(|m| sentinel_core::report::warnings::Warning::from_untrusted("warning", m))
.collect()
} else {
report.warning_details.clone()
}
}
fn print_warnings(report: &Report, force_color: bool) {
let colors = ansi_colors(force_color);
let AnsiColors {
bold,
yellow,
reset,
..
} = colors;
if !report.warning_details.is_empty() {
println!("{bold}{yellow}Warnings:{reset}");
for w in &report.warning_details {
let plain = strip_code_ticks(&w.message);
println!(
" [{}] {}",
sanitize_for_terminal(&w.kind),
sanitize_for_terminal(&plain),
);
}
println!();
} else if !report.warnings.is_empty() {
println!("{bold}{yellow}Warnings:{reset}");
for w in &report.warnings {
println!(" {}", sanitize_for_terminal(w));
}
println!();
}
}
fn print_acknowledged_summary(report: &Report, force_color: bool, show_acknowledged: bool) {
if report.acknowledged_findings.is_empty() {
return;
}
let colors = ansi_colors(force_color);
let AnsiColors {
bold, dim, reset, ..
} = colors;
let count = report.acknowledged_findings.len();
println!();
if show_acknowledged {
println!(
"{bold}{count} acknowledged finding{plural} suppressed.{reset}",
plural = if count == 1 { "" } else { "s" },
);
for ack_pair in &report.acknowledged_findings {
let f = &ack_pair.finding;
let a = &ack_pair.acknowledgment;
println!(
" {dim}[ack]{reset} {sig} ({by}, {at}): {reason}",
sig = sanitize_for_terminal(&f.signature),
by = sanitize_for_terminal(&a.acknowledged_by),
at = sanitize_for_terminal(&a.acknowledged_at),
reason = sanitize_for_terminal(&a.reason),
);
}
} else {
println!(
"{dim}{count} additional finding{plural} acknowledged. \
Run with --show-acknowledged to see them.{reset}",
plural = if count == 1 { "" } else { "s" },
);
}
}
pub(crate) fn print_findings(findings: &[sentinel_core::detect::Finding], force_color: bool) {
print_findings_with_recurrence(findings, force_color, None);
}
pub(crate) fn print_findings_with_recurrence(
findings: &[sentinel_core::detect::Finding],
force_color: bool,
external: Option<HashMap<String, RecurrenceStats>>,
) {
let colors = ansi_colors(force_color);
println!(
"{}Found {} finding(s):{}",
colors.bold,
findings.len(),
colors.reset
);
if let Some(line) = format_severity_breakdown(findings, colors) {
println!("{line}");
}
if let Some(line) = format_top_finding_types(findings) {
println!("{line}");
}
println!();
let recurrence = external.unwrap_or_else(|| build_recurrence_index(findings));
let mut first_seen: HashMap<String, usize> = HashMap::new();
let mut printed_worst: HashMap<String, Severity> = HashMap::new();
let mut last_was_stub = false;
for (i, finding) in findings.iter().enumerate() {
let key = recurrence_key(finding);
let outranks_printed = printed_worst
.get(&key)
.is_none_or(|worst| finding.severity < *worst);
if !outranks_printed {
let first = first_seen.get(&key).copied().unwrap_or(i + 1);
print_repeat_stub(i, finding, first, colors);
last_was_stub = true;
continue;
}
if last_was_stub {
println!();
last_was_stub = false;
}
printed_worst.insert(key.clone(), finding.severity.clone());
first_seen.entry(key.clone()).or_insert(i + 1);
print_finding_entry(i, finding, colors);
if let Some(stats) = recurrence.get(&key).filter(|s| s.count > 1) {
let AnsiColors { cyan, reset, .. } = colors;
let ops = if stats.total_ops > 0 {
format!(" \u{b7} ~{} avoidable ops in total", stats.total_ops)
} else {
String::new()
};
println!(
" {cyan}Recurrence:{reset} detected in {} traces{ops}",
stats.count
);
}
println!();
}
if last_was_stub {
println!();
}
}
pub(crate) struct RecurrenceStats {
pub(crate) count: usize,
pub(crate) total_ops: usize,
}
pub(crate) fn recurrence_key(f: &sentinel_core::detect::Finding) -> String {
let signature = if f.signature.is_empty() {
format!(
"{}|{}|{}|{}",
f.finding_type.as_str(),
f.service,
f.source_endpoint,
f.pattern.template
)
} else {
f.signature.clone()
};
if let Some(grouping) = f.effective_grouping() {
format!(
"{}:{}|{}:{}{}",
grouping.key.len(),
grouping.key,
grouping.value.len(),
grouping.value,
signature
)
} else {
signature
}
}
fn extra_ops_of(f: &sentinel_core::detect::Finding) -> usize {
f.green_impact
.as_ref()
.map_or(0, |gi| gi.estimated_extra_io_ops)
}
fn build_recurrence_index(
findings: &[sentinel_core::detect::Finding],
) -> HashMap<String, RecurrenceStats> {
let mut index: HashMap<String, RecurrenceStats> = HashMap::new();
for f in findings {
let entry = index.entry(recurrence_key(f)).or_insert(RecurrenceStats {
count: 0,
total_ops: 0,
});
entry.count += 1;
entry.total_ops += extra_ops_of(f);
}
index
}
fn print_repeat_stub(
index: usize,
finding: &sentinel_core::detect::Finding,
first_entry: usize,
colors: AnsiColors,
) {
let AnsiColors { dim, reset, .. } = colors;
let severity_color = severity_color(&finding.severity, colors);
let severity_label = severity_label(&finding.severity);
println!(
" {severity_color}[{severity_label}]{reset} {dim}#{} {} \u{b7} repeat of #{first_entry} \u{b7} trace {}{reset}",
index + 1,
finding.finding_type.display_label(),
sanitize_for_terminal(&finding.trace_id)
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
pub(crate) enum FindingsSort {
#[default]
Impact,
Severity,
}
pub(crate) fn compare_severity_impact(
mode: FindingsSort,
a: (&Severity, u64),
b: (&Severity, u64),
) -> std::cmp::Ordering {
let by_severity = a.0.cmp(b.0);
let by_impact = b.1.cmp(&a.1);
match mode {
FindingsSort::Severity => by_severity.then(by_impact),
FindingsSort::Impact => by_impact.then(by_severity),
}
}
pub(crate) fn sort_findings(findings: &mut [sentinel_core::detect::Finding], mode: FindingsSort) {
let index = build_recurrence_index(findings);
let ops: Vec<u64> = findings
.iter()
.map(|f| index.get(&recurrence_key(f)).map_or(0, |s| s.total_ops) as u64)
.collect();
let mut order: Vec<usize> = (0..findings.len()).collect();
order.sort_by(|&a, &b| {
compare_severity_impact(
mode,
(&findings[a].severity, ops[a]),
(&findings[b].severity, ops[b]),
)
});
apply_permutation(findings, &order);
}
fn apply_permutation<T>(items: &mut [T], order: &[usize]) {
let mut destination = vec![0usize; order.len()];
for (position, &source) in order.iter().enumerate() {
destination[source] = position;
}
for i in 0..destination.len() {
while destination[i] != i {
let target = destination[i];
items.swap(i, target);
destination.swap(i, target);
}
}
}
fn format_severity_breakdown(
findings: &[sentinel_core::detect::Finding],
colors: AnsiColors,
) -> Option<String> {
if findings.is_empty() {
return None;
}
let mut critical = 0usize;
let mut warning = 0usize;
let mut info = 0usize;
for f in findings {
match f.severity {
Severity::Critical => critical += 1,
Severity::Warning => warning += 1,
Severity::Info => info += 1,
}
}
let AnsiColors {
red,
yellow,
dim,
reset,
..
} = colors;
Some(format!(
" {red}{critical} critical{reset}, {yellow}{warning} warning{reset}, {dim}{info} info{reset}"
))
}
fn format_top_finding_types(findings: &[sentinel_core::detect::Finding]) -> Option<String> {
if findings.len() <= 5 {
return None;
}
let mut counts: HashMap<&'static str, usize> = HashMap::new();
for f in findings {
*counts.entry(f.finding_type.as_str()).or_insert(0) += 1;
}
let mut pairs: Vec<(&'static str, usize)> = counts.into_iter().collect();
pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
let take = if findings.len() > 10 { 3 } else { 2 };
let formatted: Vec<String> = pairs
.into_iter()
.take(take)
.map(|(k, v)| format!("{k} ({v})"))
.collect();
if formatted.is_empty() {
None
} else {
Some(format!(" Most common: {}", formatted.join(", ")))
}
}
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} {}",
sanitize_for_terminal(&finding.trace_id)
);
println!(
" {dim}Service:{reset} {}",
sanitize_for_terminal(&finding.service)
);
for attr in &finding.grouping {
println!(
" {dim}{}:{reset} {}",
sanitize_for_terminal(&attr.key),
sanitize_for_terminal(&attr.value)
);
}
println!(
" {dim}Endpoint:{reset} {}",
sanitize_for_terminal(&finding.source_endpoint)
);
if let Some(ref loc) = finding.code_location {
let src = loc.display_string();
if !src.is_empty() {
println!(" {dim}Source:{reset} {}", sanitize_for_terminal(&src));
}
}
println!(
" {dim}Template:{reset} {}",
sanitize_for_terminal(&finding.pattern.template)
);
println!(
" {dim}Hits:{reset} {} occurrences, {} distinct params, {}ms window",
finding.pattern.occurrences, finding.pattern.distinct_params, finding.pattern.window_ms
);
if let Some(timing) = format_span_timing(&finding.pattern) {
println!(" {dim}Timing:{reset} {timing}");
}
if let Some(label) = classification_label(finding) {
println!(" {dim}Class:{reset} {label}");
}
println!(
" {dim}Window:{reset} {} -> {}",
fmt_local_iso(&finding.first_timestamp, LOCAL_TIME_FORMAT),
fmt_local_iso(&finding.last_timestamp, LOCAL_TIME_FORMAT)
);
println!(
" {cyan}Suggestion:{reset} {}",
sanitize_for_terminal(&strip_code_ticks(&finding.suggestion))
);
if !finding.confidence.is_batch() {
println!(
" {dim}Confidence:{reset} {}",
finding.confidence.as_str()
);
}
if let Some(ref fix) = finding.suggested_fix {
let plain = strip_code_ticks(&fix.recommendation);
let recommendation = sanitize_for_terminal(&plain);
match fix.reference_url.as_deref().and_then(safe_url) {
Some(url) => println!(" {cyan}Suggested fix:{reset} {recommendation} (see: {url})"),
None => println!(" {cyan}Suggested fix:{reset} {recommendation}"),
}
}
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",
}
}
pub(crate) fn fmt_tiny(v: f64) -> String {
let v = if v == 0.0 { 0.0 } else { v };
if v == 0.0 || v >= 1e-5 {
format!("{v:.6}")
} else {
format!("{v:.3e}")
}
}
struct WasteLine<'a> {
label: &'a str,
ratio_label: &'a str,
waste_kwh: f64,
energy_kwh: f64,
ratio: f64,
model: &'a str,
region: Option<&'a str>,
waste_gco2: Option<f64>,
}
fn format_waste_line(w: &WasteLine<'_>) -> String {
let gco2 = w
.waste_gco2
.map(|g| format!(", {} gCO\u{2082}", fmt_tiny(g)))
.unwrap_or_default();
let region = w.region.map_or_else(String::new, |r| {
format!(", region {}", sanitize_for_terminal(r))
});
let model = if w.model.is_empty() {
"-".into()
} else {
sanitize_for_terminal(w.model)
};
let scope = if w.model == sentinel_core::report::DB_WASTE_MODEL_ESTIMATED {
"[within the report totals]"
} else {
"[excluded from totals]"
};
format!(
" {:<19}{} kWh of {} kWh ({:.0}% {} ratio, model {model}){gco2}{region} {scope}",
w.label,
fmt_tiny(w.waste_kwh),
fmt_tiny(w.energy_kwh),
w.ratio * 100.0,
w.ratio_label,
)
}
fn format_database_waste_line(db: &sentinel_core::report::DatabaseWaste) -> String {
format_waste_line(&WasteLine {
label: "Database waste:",
ratio_label: "SQL",
waste_kwh: db.waste_kwh,
energy_kwh: db.energy_kwh,
ratio: db.sql_waste_ratio,
model: &db.model,
region: db.region.as_deref(),
waste_gco2: db.waste_gco2,
})
}
fn format_messaging_waste_line(mw: &sentinel_core::report::MessagingWaste) -> String {
format_waste_line(&WasteLine {
label: "Broker waste:",
ratio_label: "messaging",
waste_kwh: mw.waste_kwh,
energy_kwh: mw.energy_kwh,
ratio: mw.messaging_waste_ratio,
model: &mw.model,
region: mw.region.as_deref(),
waste_gco2: mw.waste_gco2,
})
}
fn print_carbon_summary(carbon: &sentinel_core::score::carbon::CarbonReport) {
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)");
}
}
fn format_region_line(region: &sentinel_core::score::carbon::RegionBreakdown) -> String {
let region_label = sanitize_for_terminal(®ion.region);
if region.status == sentinel_core::score::carbon::REGION_STATUS_UNRESOLVED {
return format!(
" - {region_label}: {} I/O ops, {:.6} gCO\u{2082} (intensity: unresolved, source: -)",
region.io_ops, region.co2_gco2,
);
}
let source_str = intensity_source_label(region.intensity_source);
let raw_suffix = format_estimation_suffix(
region.intensity_estimated,
region.intensity_estimation_method.as_deref(),
);
let estimation_suffix = sanitize_for_terminal(&raw_suffix);
format!(
" - {region_label}: {} I/O ops, {:.6} gCO\u{2082} ({:.0} gCO\u{2082}/kWh, source: {source_str}{estimation_suffix})",
region.io_ops, region.co2_gco2, region.grid_intensity_gco2_kwh,
)
}
fn print_absent_aware_figures(
summary: &sentinel_core::report::GreenSummary,
traces_analyzed: usize,
dim: &str,
reset: &str,
) {
match summary.co2.as_ref() {
Some(carbon) => print_carbon_summary(carbon),
None if traces_analyzed == 0 => {
println!(" {dim}Carbon: not computed (no traces analyzed){reset}");
}
None => println!(" {dim}Carbon: not computed ([green] enabled = false){reset}"),
}
match &summary.database_waste {
Some(db) => println!("{}", format_database_waste_line(db)),
None => println!(
" {dim}Database waste: not measured (no SQL activity, a measured reading needs [green.alumet.database]){reset}"
),
}
match &summary.messaging_waste {
Some(mw) => println!("{}", format_messaging_waste_line(mw)),
None => println!(
" {dim}Broker waste: not measured (no broker publish spans, or no broker energy backend){reset}"
),
}
}
fn print_green_summary(
summary: &sentinel_core::report::GreenSummary,
traces_analyzed: usize,
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);
if summary.total_sql_io_ops > 0 {
println!(
" SQL share: {} of {} SQL ops avoidable",
summary.avoidable_sql_io_ops, summary.total_sql_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(),
);
print_absent_aware_figures(summary, traces_analyzed, dim, reset);
if let Some(scoring) = summary
.scoring_config
.as_ref()
.filter(|cfg| cfg.uses_electricity_maps())
{
println!();
println!("{}", format_scoring_config_line(scoring));
}
if !summary.regions.is_empty() {
println!();
println!(" {bold}Per-region breakdown:{reset}");
for region in &summary.regions {
println!("{}", format_region_line(region));
}
}
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}"));
let endpoint = sanitize_for_terminal(&offender.endpoint);
let service = sanitize_for_terminal(&offender.service);
println!(
" - {endpoint}: IIS {:.1} {level_color}({}){reset} (service: {service}){co2_str}",
offender.io_intensity_score,
level.short_label(),
);
}
}
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_min_occurrences` / \
`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,
demo_context: bool,
) {
let AnsiColors {
bold,
red,
green,
dim,
reset,
..
} = ansi_colors(force_color);
let gate_color = if gate.passed { green } else { red };
let gate_label = if gate.passed { "PASSED" } else { "FAILED" };
let demo_note = if demo_context && !gate.passed {
" (informational in demo, would exit 1 under analyze --ci)"
} else {
""
};
println!("{bold}Quality gate: {gate_color}{gate_label}{reset}{demo_note}");
for rule in &gate.rules {
let status_color = if rule.passed { green } else { red };
let status_label = if rule.passed { "PASS" } else { "FAIL" };
let rule_name = sentinel_core::quality_gate::rule_label(&rule.rule).map_or_else(
|| sanitize_for_terminal(&rule.rule).into_owned(),
str::to_string,
);
println!(
" {dim}-{reset} {rule_name}: {} (actual {}) {status_color}{status_label}{reset}",
rule.threshold, rule.actual,
);
}
println!();
}
pub(crate) fn emit_diff(
diff: &sentinel_core::diff::DiffReport,
format: Option<OutputFormat>,
output: Option<&std::path::Path>,
) -> std::io::Result<()> {
use std::io::Write;
let mut writer: Box<dyn Write> = match output {
Some(path) => {
use std::fs::OpenOptions;
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
opts.custom_flags(libc::O_NOFOLLOW);
}
Box::new(opts.open(path)?)
}
None => Box::new(std::io::stdout().lock()),
};
let colors = if output.is_some() {
no_colors()
} else {
ansi_colors(false)
};
let effective_format = format.unwrap_or(OutputFormat::Text);
match effective_format {
OutputFormat::Text => write_diff_text(&mut writer, diff, colors)?,
OutputFormat::Json => {
serde_json::to_writer_pretty(&mut writer, diff).map_err(std::io::Error::other)?;
writeln!(writer)?;
}
OutputFormat::Sarif => {
let sarif = sentinel_core::report::sarif::findings_to_sarif(&diff.new_findings);
serde_json::to_writer_pretty(&mut writer, &sarif).map_err(std::io::Error::other)?;
writeln!(writer)?;
}
}
Ok(())
}
fn write_diff_text(
writer: &mut dyn std::io::Write,
diff: &sentinel_core::diff::DiffReport,
colors: AnsiColors,
) -> std::io::Result<()> {
let AnsiColors {
bold,
cyan,
red,
yellow,
green,
reset,
..
} = colors;
let new_count = diff.new_findings.len();
let resolved_count = diff.resolved_findings.len();
let mutated_count = diff.mutated_findings.len();
let changed_count = diff.severity_changes.len();
let regression_count = diff
.severity_changes
.iter()
.filter(|c| c.is_regression())
.count();
let endpoint_change_count = diff.endpoint_metric_deltas.len();
writeln!(writer)?;
writeln!(writer, "{bold}{cyan}=== perf-sentinel diff ==={reset}")?;
writeln!(
writer,
" {red}{new_count} new{reset}, \
{green}{resolved_count} resolved{reset}, \
{yellow}{mutated_count} mutated{reset}, \
{yellow}{changed_count} severity changed{reset} ({regression_count} regression(s)), \
{endpoint_change_count} endpoint count change(s)"
)?;
writeln!(writer)?;
if !diff.warning_details.is_empty() {
writeln!(writer, "{bold}{yellow}Warnings (after run):{reset}")?;
for w in &diff.warning_details {
let plain = strip_code_ticks(&w.message);
writeln!(
writer,
" [{}] {}",
sanitize_for_terminal(&w.kind),
sanitize_for_terminal(&plain)
)?;
}
writeln!(writer)?;
}
write_new_findings_section(writer, &diff.new_findings, colors)?;
write_resolved_findings_section(writer, &diff.resolved_findings, colors)?;
write_mutated_findings_section(writer, &diff.mutated_findings, colors)?;
write_severity_changes_section(writer, &diff.severity_changes, colors)?;
write_endpoint_deltas_section(writer, &diff.endpoint_metric_deltas, colors)?;
if new_count == 0
&& resolved_count == 0
&& mutated_count == 0
&& changed_count == 0
&& endpoint_change_count == 0
{
writeln!(
writer,
"{green}No differences detected between the two trace sets.{reset}"
)?;
}
Ok(())
}
fn write_new_findings_section(
writer: &mut dyn std::io::Write,
findings: &[sentinel_core::detect::Finding],
colors: AnsiColors,
) -> std::io::Result<()> {
if findings.is_empty() {
return Ok(());
}
let AnsiColors {
bold, red, reset, ..
} = colors;
writeln!(
writer,
"{bold}{red}New findings ({}):{reset}",
findings.len()
)?;
for f in findings {
writeln!(
writer,
" {red}+{reset} [{}] {} on {} ({})",
severity_label(&f.severity),
f.finding_type.display_label(),
f.source_endpoint,
f.service,
)?;
write_finding_block(writer, f, colors)?;
}
writeln!(writer)
}
fn write_resolved_findings_section(
writer: &mut dyn std::io::Write,
findings: &[sentinel_core::detect::Finding],
colors: AnsiColors,
) -> std::io::Result<()> {
if findings.is_empty() {
return Ok(());
}
let AnsiColors {
bold, green, reset, ..
} = colors;
writeln!(
writer,
"{bold}{green}Resolved findings ({}):{reset}",
findings.len()
)?;
for f in findings {
writeln!(
writer,
" {green}-{reset} [{}] {} on {} ({})",
severity_label(&f.severity),
f.finding_type.display_label(),
f.source_endpoint,
f.service,
)?;
write_finding_block(writer, f, colors)?;
}
writeln!(writer)
}
fn write_mutated_findings_section(
writer: &mut dyn std::io::Write,
mutated: &[sentinel_core::diff::MutatedFinding],
colors: AnsiColors,
) -> std::io::Result<()> {
if mutated.is_empty() {
return Ok(());
}
let AnsiColors {
bold,
yellow,
red,
dim,
reset,
..
} = colors;
writeln!(
writer,
"{bold}{yellow}Mutated findings ({}):{reset}",
mutated.len()
)?;
for pair in mutated {
let worsened = pair.after.severity < pair.before.severity;
let marker_color = if worsened { red } else { yellow };
let severity = if pair.after.severity == pair.before.severity {
severity_label(&pair.after.severity).to_string()
} else {
format!(
"{}\u{2192}{}",
severity_label(&pair.before.severity),
severity_label(&pair.after.severity)
)
};
writeln!(
writer,
" {marker_color}~{reset} [{severity}] {} on {} ({})",
pair.after.finding_type.display_label(),
pair.after.source_endpoint,
pair.after.service,
)?;
writeln!(
writer,
" {dim}Before:{reset} {}",
sanitize_for_terminal(&pair.before.pattern.template)
)?;
writeln!(
writer,
" {dim}After:{reset} {}",
sanitize_for_terminal(&pair.after.pattern.template)
)?;
}
writeln!(writer)
}
fn write_finding_block(
writer: &mut dyn std::io::Write,
f: &sentinel_core::detect::Finding,
colors: AnsiColors,
) -> std::io::Result<()> {
let AnsiColors {
cyan, dim, reset, ..
} = colors;
for attr in &f.grouping {
writeln!(
writer,
" {dim}{:<12}{reset} {}",
format!("{}:", sanitize_for_terminal(&attr.key)),
sanitize_for_terminal(&attr.value)
)?;
}
writeln!(
writer,
" {dim}{:<12}{reset} {}",
"Template:",
sanitize_for_terminal(&f.pattern.template)
)?;
writeln!(
writer,
" {dim}{:<12}{reset} {}",
"Occurrences:", f.pattern.occurrences
)?;
writeln!(
writer,
" {dim}{:<12}{reset} {} -> {} ({})",
"Window:",
fmt_local_iso(&f.first_timestamp, "%Y-%m-%d %H:%M"),
fmt_local_iso(&f.last_timestamp, "%Y-%m-%d %H:%M"),
format_duration_compact(f.pattern.window_ms),
)?;
writeln!(
writer,
" {cyan}{:<12}{reset} {}",
"Suggestion:",
sanitize_for_terminal(&strip_code_ticks(&f.suggestion))
)?;
if let Some(ref fix) = f.suggested_fix {
let label = format!("Fix [{}]:", sanitize_for_terminal(&fix.framework));
let plain = strip_code_ticks(&fix.recommendation);
let recommendation = sanitize_for_terminal(&plain);
match fix.reference_url.as_deref().and_then(safe_url) {
Some(url) => writeln!(
writer,
" {cyan}{label:<12}{reset} {recommendation} ({url})"
)?,
None => writeln!(writer, " {cyan}{label:<12}{reset} {recommendation}")?,
}
}
if let Some(ref impact) = f.green_impact {
let level = impact.io_intensity_band;
let level_color = interpret_color(level, colors);
writeln!(
writer,
" {dim}{:<12}{reset} {:.1} {level_color}({}){reset}",
"IIS:",
impact.io_intensity_score,
level.short_label(),
)?;
writeln!(
writer,
" {dim}{:<12}{reset} {} avoidable ops",
"Extra I/O:", impact.estimated_extra_io_ops,
)?;
}
if let Some(ref loc) = f.code_location {
let s = loc.display_string();
if !s.is_empty() {
writeln!(
writer,
" {dim}{:<12}{reset} {}",
"Location:",
sanitize_for_terminal(&s)
)?;
}
}
if !f.confidence.is_batch() {
writeln!(
writer,
" {dim}{:<12}{reset} {}",
"Confidence:",
f.confidence.as_str()
)?;
}
Ok(())
}
pub(crate) const LOCAL_TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
pub(crate) fn fmt_local_iso(iso: &str, format: &str) -> String {
chrono::DateTime::parse_from_rfc3339(iso).map_or_else(
|_| sanitize_for_terminal(iso).into_owned(),
|t| to_local(&t).format(format).to_string(),
)
}
pub(crate) fn to_local<Z: chrono::TimeZone>(
t: &chrono::DateTime<Z>,
) -> chrono::DateTime<chrono::FixedOffset> {
static EMBEDDED: std::sync::OnceLock<Option<chrono_tz::Tz>> = std::sync::OnceLock::new();
let embedded = EMBEDDED.get_or_init(|| {
embedded_zone(std::env::var("TZ").ok().as_deref(), |name| {
std::path::Path::new("/usr/share/zoneinfo")
.join(name)
.is_file()
})
});
match embedded {
Some(tz) => t.with_timezone(tz).fixed_offset(),
None => t.with_timezone(&chrono::Local).fixed_offset(),
}
}
fn embedded_zone(tz: Option<&str>, system_has: impl Fn(&str) -> bool) -> Option<chrono_tz::Tz> {
let tz = tz?;
let name = tz.strip_prefix(':').unwrap_or(tz);
if name.is_empty() || name.starts_with('/') || system_has(name) {
return None;
}
name.parse().ok()
}
fn format_duration_compact(ms: u64) -> String {
if ms < 1_000 {
return format!("{ms}ms");
}
let total_secs = ms / 1_000;
if total_secs < 60 {
return format!("{total_secs}s");
}
let total_mins = total_secs / 60;
let secs = total_secs % 60;
if total_mins < 60 {
if secs == 0 {
return format!("{total_mins}m");
}
return format!("{total_mins}m{secs}s");
}
let hours = total_mins / 60;
let mins = total_mins % 60;
if mins == 0 {
format!("{hours}h")
} else {
format!("{hours}h{mins}m")
}
}
fn write_severity_changes_section(
writer: &mut dyn std::io::Write,
changes: &[sentinel_core::diff::SeverityChange],
colors: AnsiColors,
) -> std::io::Result<()> {
if changes.is_empty() {
return Ok(());
}
let AnsiColors {
bold,
yellow,
red,
green,
reset,
..
} = colors;
writeln!(
writer,
"{bold}{yellow}Severity changes ({}):{reset}",
changes.len()
)?;
for change in changes {
let arrow_color = if change.is_regression() { red } else { green };
writeln!(
writer,
" [{}] {arrow_color}->{reset} [{}] {} on {} ({})",
severity_label(&change.before_severity),
severity_label(&change.after_severity),
change.finding.finding_type.display_label(),
change.finding.source_endpoint,
change.finding.service,
)?;
}
writeln!(writer)
}
fn write_endpoint_deltas_section(
writer: &mut dyn std::io::Write,
deltas: &[sentinel_core::diff::EndpointDelta],
colors: AnsiColors,
) -> std::io::Result<()> {
if deltas.is_empty() {
return Ok(());
}
let AnsiColors {
bold,
cyan,
red,
green,
reset,
..
} = colors;
writeln!(
writer,
"{bold}{cyan}Endpoint I/O op deltas ({}):{reset}",
deltas.len()
)?;
for d in deltas {
let (color, sign) = if d.delta > 0 { (red, "+") } else { (green, "") };
writeln!(
writer,
" {color}{sign}{}{reset} {} on {} ({} -> {})",
d.delta, d.endpoint, d.service, d.before_io_ops, d.after_io_ops,
)?;
}
writeln!(writer)
}
#[cfg(test)]
mod tests;