use anyhow::Result;
use colored::Colorize;
use crate::commands::graph::{
CoverageContext, CoverageNode, Location, NodeRole, PathHop, SpanSignal, UnobservedPaths,
build_coverage_context, build_graph_with_spinner,
};
use crate::exit_codes::*;
use unfault_analysis::graph::traversal::node_file_path_pub;
use unfault_analysis::graph::{
CodeGraph, GraphEdgeKind, GraphNode, GraphNodeIndex, ModuleCategory,
};
use petgraph::Direction;
use petgraph::visit::EdgeRef;
#[derive(Debug)]
pub struct TelemetryArgs {
pub target: String,
pub workspace_path: Option<String>,
pub json: bool,
pub compact: bool,
pub summary: bool,
pub verbose: bool,
pub offline: bool,
pub refresh_cache: bool,
}
pub async fn execute(args: TelemetryArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Building code graph...", "→".cyan());
}
let graph = match build_graph_with_spinner(&workspace_path, args.verbose, args.json) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let target = args.target.trim();
let target_kind = resolve_target(target, &graph);
let report = match target_kind {
TargetKind::Route {
ref method,
ref path,
} => analyze_route(&graph, path, method.as_deref(), args.verbose),
TargetKind::Function => analyze_function(&graph, target, args.verbose),
TargetKind::File => analyze_file(&graph, target, args.verbose),
TargetKind::Directory => analyze_directory(&graph, target, args.verbose),
};
let report = match report {
Some(r) => r,
None => {
eprintln!(
"{} Could not resolve '{}' in the code graph.",
"Error:".red().bold(),
target
);
return Ok(EXIT_ERROR);
}
};
if args.json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else if args.compact {
render_compact(&report);
} else if args.summary {
match target_kind {
TargetKind::Directory => render_summary(&report),
_ => match target_kind {
TargetKind::Route { .. } | TargetKind::Function => render_merged(&report),
_ => render_sections(&report),
},
}
} else {
match target_kind {
TargetKind::Directory => render_catalog(&report),
TargetKind::Route { .. } | TargetKind::Function => render_merged(&report),
_ => render_sections(&report),
}
}
Ok(EXIT_SUCCESS)
}
pub struct CriticalUnobservedArgs {
pub dir: String,
pub workspace_path: Option<String>,
pub role: String,
pub limit: usize,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CriticalUnobservedEntry {
pub name: String,
pub location: Location,
pub role: String,
pub statement_kind: Option<StatementKind>,
pub route_count: usize,
pub routes: Vec<UnobservedRouteRef>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UnobservedRouteRef {
pub method: String,
pub path: String,
pub handler: String,
}
pub fn rank_unobserved_boundaries(
report: &TelemetryReport,
role_filter: &str,
) -> Vec<CriticalUnobservedEntry> {
let include_db = role_filter == "all" || role_filter == "db" || role_filter == "database";
let include_http = role_filter == "all" || role_filter == "http";
let include_remote = role_filter == "all" || role_filter == "remote";
let mut index: std::collections::HashMap<
(String, String, Option<u32>),
(CriticalUnobservedEntry, std::collections::HashSet<String>),
> = std::collections::HashMap::new();
for route in &report.routes {
let route_key = if route.method.is_empty() {
route.path.clone()
} else {
format!("{} {}", route.method, route.path)
};
let route_ref = UnobservedRouteRef {
method: route.method.clone(),
path: route.path.clone(),
handler: route.handler.clone(),
};
let by_role = &route.unobserved.by_role;
let mut candidates: Vec<(&super::graph::UnobservedCallee, &str)> = Vec::new();
if include_db {
candidates.extend(by_role.database.iter().map(|c| (c, "database")));
}
if include_http {
candidates.extend(by_role.http.iter().map(|c| (c, "http")));
}
if include_remote {
candidates.extend(by_role.remote.iter().map(|c| (c, "remote")));
}
for (callee, role_str) in candidates {
let key = (
callee.name.clone(),
callee.location.file.clone(),
callee.location.line,
);
let (entry, seen_routes) = index.entry(key).or_insert_with(|| {
let stmt_kind = if role_str == "database" {
Some(infer_statement_kind(&callee.name))
} else {
None
};
(
CriticalUnobservedEntry {
name: callee.name.clone(),
location: callee.location.clone(),
role: role_str.to_string(),
statement_kind: stmt_kind,
route_count: 0,
routes: Vec::new(),
},
std::collections::HashSet::new(),
)
});
if seen_routes.insert(route_key.clone()) {
entry.route_count += 1;
entry.routes.push(route_ref.clone());
}
}
}
let mut ranked: Vec<CriticalUnobservedEntry> = index
.into_values()
.map(|(mut e, _)| {
e.routes
.sort_by(|a, b| a.method.cmp(&b.method).then(a.path.cmp(&b.path)));
e
})
.collect();
ranked.sort_by(|a, b| b.route_count.cmp(&a.route_count).then(a.name.cmp(&b.name)));
ranked
}
pub async fn execute_critical_unobserved(args: CriticalUnobservedArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Building code graph...", "→".cyan());
}
let graph = match build_graph_with_spinner(&workspace_path, args.verbose, args.json) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let dir = args.dir.trim();
let report = match analyze_directory(&graph, dir, args.verbose) {
Some(r) => r,
None => {
if args.json {
println!("[]");
} else {
println!("No routes found in '{}'.", dir);
}
return Ok(EXIT_SUCCESS);
}
};
let role_lower = args.role.to_lowercase();
let mut ranked = rank_unobserved_boundaries(&report, &role_lower);
ranked.truncate(args.limit);
if args.json {
println!("{}", serde_json::to_string_pretty(&ranked)?);
return Ok(EXIT_SUCCESS);
}
if ranked.is_empty() {
println!("No unobserved boundaries found in '{}'.", dir);
return Ok(EXIT_SUCCESS);
}
println!();
println!(
" {} {}",
"Critical unobserved boundaries".bold(),
format!("({} shown, ranked by route exposure)", ranked.len()).bright_black()
);
println!();
for (i, entry) in ranked.iter().enumerate() {
let rank_str = format!("{}.", i + 1);
let loc = if entry.location.line.is_some() {
format!("{}:{}", entry.location.file, entry.location.line.unwrap())
} else {
entry.location.file.clone()
};
let role_badge = match entry.role.as_str() {
"database" => "db".cyan().bold(),
"http" => "http".yellow().bold(),
"remote" => "remote".magenta().bold(),
_ => entry.role.as_str().normal(),
};
let stmt = entry
.statement_kind
.as_ref()
.map(|s| {
let label = serde_json::to_value(s)
.ok()
.and_then(|v| v.as_str().map(|s| s.to_string()))
.unwrap_or_default();
format!(" {}", label.bright_black())
})
.unwrap_or_default();
println!(
" {} {} {} {}{}",
rank_str.bright_black(),
entry.name.bright_white().bold(),
format!(
"({} route{})",
entry.route_count,
if entry.route_count == 1 { "" } else { "s" }
)
.bright_black(),
role_badge,
stmt,
);
println!(" {}", loc.bright_black());
for r in &entry.routes {
let method_colored = match r.method.as_str() {
"GET" => r.method.green(),
"POST" => r.method.yellow(),
"PUT" | "PATCH" => r.method.cyan(),
"DELETE" => r.method.red(),
_ => r.method.normal(),
};
println!(
" {} {} {}",
method_colored,
r.path.bright_white(),
format!("→ {}", r.handler).bright_black()
);
}
println!();
}
Ok(EXIT_SUCCESS)
}
enum TargetKind {
Route {
method: Option<String>,
path: String,
},
File,
Directory,
Function,
}
fn resolve_target(target: &str, graph: &CodeGraph) -> TargetKind {
if target.starts_with('/') {
return TargetKind::Route {
method: None,
path: target.to_string(),
};
}
let upper = target.to_uppercase();
if let Some(rest) = upper
.strip_prefix("GET ")
.or_else(|| upper.strip_prefix("POST "))
.or_else(|| upper.strip_prefix("PUT "))
.or_else(|| upper.strip_prefix("PATCH "))
.or_else(|| upper.strip_prefix("DELETE "))
.or_else(|| upper.strip_prefix("HEAD "))
.or_else(|| upper.strip_prefix("OPTIONS "))
{
if rest.starts_with('/') {
return TargetKind::Route {
method: Some(target[..target.find(' ').unwrap_or(0)].to_string()),
path: rest.to_string(),
};
}
}
if target.contains('.') && graph.find_file_by_path(target).is_some() {
return TargetKind::File;
}
if target.ends_with('/') || has_file_with_prefix(graph, target) {
return TargetKind::Directory;
}
if target.contains('.') {
return TargetKind::File;
}
TargetKind::Function
}
fn has_file_with_prefix(graph: &CodeGraph, prefix: &str) -> bool {
for idx in graph.graph.node_indices() {
if let GraphNode::File { path, .. } = &graph.graph[idx] {
if path.starts_with(prefix) || path.contains(&format!("/{}", prefix)) {
return true;
}
}
}
false
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnchorKind {
Explicit,
FrameworkAuto,
None,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LoggingQuality {
Structured,
Plain,
#[serde(rename = "none")]
None_,
}
impl LoggingQuality {
fn icon(&self) -> &'static str {
match self {
LoggingQuality::Structured => "◉",
LoggingQuality::Plain => "○",
LoggingQuality::None_ => "·",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CalleeKind {
Function,
Method,
Builtin,
Construct,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StatementKind {
Select,
Insert,
Update,
Delete,
Commit,
Rollback,
Raw,
Other,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RouteLogs {
pub kind: LoggingQuality,
pub library: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RouteMetrics {
pub present: bool,
pub library: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CalleeInfo {
pub name: String,
pub location: Location,
pub role: NodeRole,
pub kind: CalleeKind,
pub depth: i32,
pub anchor_kind: AnchorKind,
#[serde(default)]
pub anchor_attributes: Vec<String>,
#[serde(default)]
pub via: Vec<PathHop>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RouteTelemetry {
pub method: String,
pub path: String,
pub handler: String,
pub location: Location,
pub anchor_kind: AnchorKind,
#[serde(default)]
pub anchor_attributes: Vec<String>,
pub total_callees: usize,
pub instrumented_callees: usize,
pub callees: Vec<CalleeInfo>,
pub unobserved: UnobservedPaths,
pub logs: RouteLogs,
pub metrics: RouteMetrics,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FileLogging {
pub file: String,
#[serde(rename = "kind")]
pub quality: LoggingQuality,
pub library: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FileMetrics {
pub file: String,
pub present: bool,
pub library: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LoggingCluster {
pub path_prefix: String,
pub file_count: usize,
pub quality_breakdown: LoggingBreakdown,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LoggingBreakdown {
pub structured: usize,
pub plain: usize,
pub none: usize,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LoggingSection {
pub files: Vec<FileLogging>,
pub clusters: Vec<LoggingCluster>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MetricsCluster {
pub path_prefix: String,
pub file_count: usize,
pub present_count: usize,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MetricsSection {
pub files: Vec<FileMetrics>,
pub clusters: Vec<MetricsCluster>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BoundaryCallSite {
pub name: String,
pub kind: CalleeKind,
pub location: Location,
pub in_route: String,
pub in_function: String,
pub anchor_kind: AnchorKind,
#[serde(default)]
pub anchor_attributes: Vec<String>,
pub statement_kind: Option<StatementKind>,
#[serde(default)]
pub via: Vec<PathHop>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Corpus {
pub files: usize,
pub routes_total: usize,
pub routes_read: usize,
pub routes_write: usize,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Boundaries {
#[serde(default)]
pub db: Vec<BoundaryCallSite>,
#[serde(default)]
pub http: Vec<BoundaryCallSite>,
#[serde(default)]
pub remote: Vec<BoundaryCallSite>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TelemetryReport {
pub target: String,
pub target_kind: String,
pub corpus: Corpus,
pub routes: Vec<RouteTelemetry>,
pub logging: LoggingSection,
pub metrics: MetricsSection,
pub boundaries: Boundaries,
}
fn analyze_directory(graph: &CodeGraph, dir: &str, verbose: bool) -> Option<TelemetryReport> {
let target = dir.trim_end_matches('/');
let files: Vec<String> = graph
.graph
.node_indices()
.filter_map(|idx| {
if let GraphNode::File { path, .. } = &graph.graph[idx] {
if path.starts_with(target) || path.contains(&format!("/{}", target)) {
Some(path.clone())
} else {
None
}
} else {
None
}
})
.collect();
if files.is_empty() {
return None;
}
analyze_file_list(graph, dir, "directory", &files, verbose)
}
fn analyze_file(graph: &CodeGraph, file_path: &str, verbose: bool) -> Option<TelemetryReport> {
let files = vec![file_path.to_string()];
analyze_file_list(graph, file_path, "file", &files, verbose)
}
fn analyze_file_list(
graph: &CodeGraph,
target: &str,
kind: &str,
files: &[String],
verbose: bool,
) -> Option<TelemetryReport> {
let mut routes: Vec<RouteTelemetry> = Vec::new();
for file_path in files {
let handlers: Vec<(String, String, String, Option<u32>)> = graph
.graph
.node_indices()
.filter_map(|idx| {
if let GraphNode::Function {
is_handler: true,
http_method: Some(method),
http_path: Some(path),
name,
line,
..
} = &graph.graph[idx]
{
let f = node_file_path_pub(graph, &graph.graph[idx]).unwrap_or_default();
if f == *file_path || f.ends_with(file_path.as_str()) {
Some((method.clone(), path.clone(), name.clone(), *line))
} else {
None
}
} else {
None
}
})
.collect();
let fl = file_logging_quality(graph, file_path);
let fm = file_metrics(graph, file_path);
for (method, path, handler, line) in &handlers {
if routes.iter().any(|r: &RouteTelemetry| {
r.method == *method && r.path == *path && r.handler == *handler
}) {
continue;
}
if let Some(rt) = analyze_single_route(graph, path, Some(method), verbose) {
routes.push(RouteTelemetry {
method: method.clone(),
path: path.clone(),
handler: handler.clone(),
location: Location::new(file_path.clone(), *line),
anchor_kind: rt.anchor_kind,
anchor_attributes: rt.anchor_attributes,
total_callees: rt.total_callees,
instrumented_callees: rt.instrumented_callees,
callees: rt.callees,
unobserved: rt.unobserved,
logs: RouteLogs {
kind: fl.quality.clone(),
library: if fl.library.is_empty() {
None
} else {
Some(fl.library.clone())
},
},
metrics: RouteMetrics {
present: fm.present,
library: fm.library.clone(),
},
});
}
}
}
let logging_files: Vec<FileLogging> = files
.iter()
.map(|f| file_logging_quality(graph, f))
.collect();
let metrics_files: Vec<FileMetrics> = files.iter().map(|f| file_metrics(graph, f)).collect();
let boundaries = build_boundaries(&routes);
let corpus = Corpus {
files: logging_files.len(),
routes_total: routes.len(),
routes_read: routes
.iter()
.filter(|r| !is_write_method(&r.method))
.count(),
routes_write: routes.iter().filter(|r| is_write_method(&r.method)).count(),
};
Some(TelemetryReport {
target: target.to_string(),
target_kind: kind.to_string(),
corpus,
routes,
logging: build_logging_section(logging_files),
metrics: build_metrics_section(metrics_files),
boundaries,
})
}
struct RouteTraversal {
anchor_kind: AnchorKind,
anchor_attributes: Vec<String>,
total_callees: usize,
instrumented_callees: usize,
callees: Vec<CalleeInfo>,
unobserved: UnobservedPaths,
}
fn analyze_route(
graph: &CodeGraph,
path: &str,
method: Option<&str>,
verbose: bool,
) -> Option<TelemetryReport> {
let ctx = build_coverage_context(graph, path, method, None, verbose)?;
let rt = traversal_from_context(&ctx);
let anchor_idx = find_anchor(graph, path, method)?;
let anchor_node = &graph.graph[anchor_idx];
let anchor_file = node_file_path_pub(graph, anchor_node).unwrap_or_default();
let fl = file_logging_quality(graph, &anchor_file);
let fm = file_metrics(graph, &anchor_file);
let routes_vec = vec![RouteTelemetry {
method: ctx.anchor.role.method_str().unwrap_or_default().to_string(),
path: ctx.anchor.role.path_str().unwrap_or_default().to_string(),
handler: ctx.anchor.name.clone(),
location: Location::new(anchor_file.clone(), ctx.anchor.line),
anchor_kind: rt.anchor_kind,
anchor_attributes: rt.anchor_attributes,
total_callees: rt.total_callees,
instrumented_callees: rt.instrumented_callees,
callees: rt.callees,
unobserved: rt.unobserved,
logs: RouteLogs {
kind: fl.quality,
library: if fl.library.is_empty() {
None
} else {
Some(fl.library)
},
},
metrics: RouteMetrics {
present: fm.present,
library: fm.library,
},
}];
let logging_file = file_logging_quality(graph, &anchor_file);
let metrics_file = file_metrics(graph, &anchor_file);
let corpus = Corpus {
files: 1,
routes_total: routes_vec.len(),
routes_read: routes_vec
.iter()
.filter(|r| !is_write_method(&r.method))
.count(),
routes_write: routes_vec
.iter()
.filter(|r| is_write_method(&r.method))
.count(),
};
let report = TelemetryReport {
target: format!("{} {}", ctx.anchor.role.method_str().unwrap_or(""), path)
.trim()
.to_string(),
target_kind: "module".to_string(),
corpus,
logging: build_logging_section(vec![logging_file]),
metrics: build_metrics_section(vec![metrics_file]),
boundaries: build_boundaries(&routes_vec),
routes: routes_vec,
};
Some(report)
}
fn analyze_function(graph: &CodeGraph, name: &str, verbose: bool) -> Option<TelemetryReport> {
let ctx = build_coverage_context(graph, name, None, None, verbose)?;
let rt = traversal_from_context(&ctx);
let anchor_idx = find_function_anchor(graph, name)?;
let anchor_node = &graph.graph[anchor_idx];
let anchor_file = node_file_path_pub(graph, anchor_node).unwrap_or_default();
let fl = file_logging_quality(graph, &anchor_file);
let fm = file_metrics(graph, &anchor_file);
let routes_vec = vec![RouteTelemetry {
method: ctx.anchor.role.method_str().unwrap_or_default().to_string(),
path: ctx.anchor.role.path_str().unwrap_or_default().to_string(),
handler: ctx.anchor.name.clone(),
location: Location::new(anchor_file.clone(), ctx.anchor.line),
anchor_kind: rt.anchor_kind,
anchor_attributes: rt.anchor_attributes,
total_callees: rt.total_callees,
instrumented_callees: rt.instrumented_callees,
callees: rt.callees,
unobserved: rt.unobserved,
logs: RouteLogs {
kind: fl.quality,
library: if fl.library.is_empty() {
None
} else {
Some(fl.library)
},
},
metrics: RouteMetrics {
present: fm.present,
library: fm.library,
},
}];
let logging_file = file_logging_quality(graph, &anchor_file);
let metrics_file = file_metrics(graph, &anchor_file);
let corpus = Corpus {
files: 1,
routes_total: routes_vec.len(),
routes_read: routes_vec
.iter()
.filter(|r| !is_write_method(&r.method))
.count(),
routes_write: routes_vec
.iter()
.filter(|r| is_write_method(&r.method))
.count(),
};
let report = TelemetryReport {
target: name.to_string(),
target_kind: "module".to_string(),
corpus,
logging: build_logging_section(vec![logging_file]),
metrics: build_metrics_section(vec![metrics_file]),
boundaries: build_boundaries(&routes_vec),
routes: routes_vec,
};
Some(report)
}
fn analyze_single_route(
graph: &CodeGraph,
path: &str,
method: Option<&str>,
verbose: bool,
) -> Option<RouteTraversal> {
let ctx = build_coverage_context(graph, path, method, None, verbose)?;
Some(traversal_from_context(&ctx))
}
fn traversal_from_context(ctx: &CoverageContext) -> RouteTraversal {
let anchor_file = ctx.anchor.file.clone();
let mut all_nodes: Vec<(&CoverageNode, Vec<PathHop>)> = Vec::new();
let mut path_buf: Vec<PathHop> = Vec::new();
collect_nodes_with_path(&ctx.anchor, &mut path_buf, &mut all_nodes);
for c in &ctx.callers {
path_buf.clear();
collect_nodes_with_path(c, &mut path_buf, &mut all_nodes);
}
let callees: Vec<CalleeInfo> = all_nodes
.iter()
.filter(|(n, _)| n.depth != 0)
.map(|(n, via)| {
let ck = callee_kind_from_name(&n.name);
let file = if n.file.is_empty() {
anchor_file.clone()
} else {
n.file.clone()
};
CalleeInfo {
name: n.name.clone(),
location: Location::new(file, n.line),
role: n.role.clone(),
kind: ck,
depth: n.depth,
anchor_kind: span_to_anchor_kind(&n.span),
anchor_attributes: span_to_attributes(&n.span),
via: via.clone(),
}
})
.collect();
let countable: Vec<&CalleeInfo> = callees
.iter()
.filter(|c| matches!(c.kind, CalleeKind::Function | CalleeKind::Method))
.collect();
let total_callees = countable.len();
let instrumented_callees = countable
.iter()
.filter(|c| c.anchor_kind != AnchorKind::None)
.count();
RouteTraversal {
anchor_kind: span_to_anchor_kind(&ctx.anchor.span),
anchor_attributes: span_to_attributes(&ctx.anchor.span),
total_callees,
instrumented_callees,
callees,
unobserved: ctx.unobserved_paths.clone(),
}
}
fn span_to_anchor_kind(span: &SpanSignal) -> AnchorKind {
match span {
SpanSignal::Decorator { .. } | SpanSignal::SdkImported { .. } => AnchorKind::Explicit,
SpanSignal::AutoInstrumented { .. } => AnchorKind::FrameworkAuto,
SpanSignal::None => AnchorKind::None,
}
}
fn span_to_attributes(span: &SpanSignal) -> Vec<String> {
match span {
SpanSignal::Decorator { name: Some(n), .. } => vec![n.clone()],
SpanSignal::SdkImported { library, .. } => vec![library.clone()],
_ => vec![],
}
}
fn callee_kind_from_name(name: &str) -> CalleeKind {
if name
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
{
return CalleeKind::Construct;
}
const BUILTINS: &[&str] = &[
"any",
"all",
"map",
"filter",
"zip",
"sorted",
"reversed",
"enumerate",
"range",
"iter",
"next",
"len",
"str",
"int",
"float",
"bool",
"list",
"dict",
"tuple",
"set",
"type",
"print",
"repr",
"hash",
"id",
"callable",
"getattr",
"setattr",
"hasattr",
"delattr",
"super",
"vars",
"dir",
"console",
"promise",
"object",
"array",
"json",
];
if BUILTINS.contains(&name.to_lowercase().as_str()) {
return CalleeKind::Builtin;
}
if name.contains('.') {
return CalleeKind::Method;
}
CalleeKind::Function
}
fn infer_http_method_kind(call_expr: &str) -> Option<StatementKind> {
let last = call_expr
.split('.')
.last()
.unwrap_or(call_expr)
.to_lowercase();
match last.as_str() {
"get" | "head" | "options" => Some(StatementKind::Select),
"post" => Some(StatementKind::Insert),
"put" | "patch" => Some(StatementKind::Update),
"delete" => Some(StatementKind::Delete),
_ => None,
}
}
fn infer_statement_kind(call_expr: &str) -> StatementKind {
let lower = call_expr.to_lowercase();
let last = lower.split('.').last().unwrap_or(lower.as_str());
if lower.contains(".objects.") {
return match last {
"get" | "filter" | "exclude" | "all" | "first" | "last" | "count" | "exists"
| "values" | "values_list" | "select_related" | "prefetch_related" | "annotate"
| "aggregate" | "earliest" | "latest" | "in_bulk" | "raw" | "only" | "defer" => {
StatementKind::Select
}
"create" | "get_or_create" | "bulk_create" => StatementKind::Insert,
"update" | "update_or_create" | "bulk_update" => StatementKind::Update,
"delete" => StatementKind::Delete,
_ => StatementKind::Other,
};
}
match last {
"select" | "scalar" | "scalars" | "scalar_one" | "scalar_one_or_none" | "fetchall"
| "fetchone" | "fetchmany" | "all" | "first" | "one" | "one_or_none" | "get" | "query"
| "filter" | "filter_by" | "count" | "exists" | "refresh" => StatementKind::Select,
"insert" | "add" | "add_all" | "bulk_insert_mappings" | "bulk_save_objects" | "create" => {
StatementKind::Insert
}
"update" | "bulk_update_mappings" | "merge" | "save" => StatementKind::Update,
"delete" | "remove" => StatementKind::Delete,
"commit" | "flush" => StatementKind::Commit,
"rollback" => StatementKind::Rollback,
"execute" | "exec" | "executemany" | "raw" | "text" => StatementKind::Raw,
_ => StatementKind::Other,
}
}
fn build_boundaries(routes: &[RouteTelemetry]) -> Boundaries {
let mut db: Vec<BoundaryCallSite> = Vec::new();
let mut http: Vec<BoundaryCallSite> = Vec::new();
let mut remote: Vec<BoundaryCallSite> = Vec::new();
for route in routes {
let route_label = if route.method.is_empty() {
route.path.clone()
} else {
format!("{} {}", route.method, route.path)
};
for callee in &route.callees {
let kind = match callee.name.contains('.') {
true => CalleeKind::Method,
false => CalleeKind::Function,
};
let site = BoundaryCallSite {
name: callee.name.clone(),
kind,
location: callee.location.clone(),
in_route: route_label.clone(),
in_function: route.handler.clone(),
anchor_kind: callee.anchor_kind.clone(),
anchor_attributes: callee.anchor_attributes.clone(),
statement_kind: None, via: callee.via.clone(),
};
match &callee.role {
NodeRole::Database => {
let mut s = site;
s.statement_kind = Some(infer_statement_kind(&callee.name));
db.push(s);
}
NodeRole::HttpClient => {
let mut s = site;
s.statement_kind = infer_http_method_kind(&callee.name);
http.push(s);
}
NodeRole::RemoteCall { .. } => remote.push(site),
_ => {}
}
}
}
Boundaries { db, http, remote }
}
fn build_logging_section(files: Vec<FileLogging>) -> LoggingSection {
let clusters = compute_logging_clusters(&files, 5);
LoggingSection { files, clusters }
}
fn compute_logging_clusters(files: &[FileLogging], min_size: usize) -> Vec<LoggingCluster> {
let mut prefix_map: std::collections::HashMap<String, Vec<&FileLogging>> =
std::collections::HashMap::new();
for f in files {
let prefix = std::path::Path::new(&f.file)
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
if !prefix.is_empty() {
prefix_map.entry(prefix).or_default().push(f);
}
}
let mut clusters: Vec<LoggingCluster> = prefix_map
.into_iter()
.filter(|(_, v)| v.len() >= min_size)
.map(|(prefix, entries)| {
let structured = entries
.iter()
.filter(|e| e.quality == LoggingQuality::Structured)
.count();
let plain = entries
.iter()
.filter(|e| e.quality == LoggingQuality::Plain)
.count();
let none = entries
.iter()
.filter(|e| e.quality == LoggingQuality::None_)
.count();
LoggingCluster {
path_prefix: prefix,
file_count: entries.len(),
quality_breakdown: LoggingBreakdown {
structured,
plain,
none,
},
}
})
.collect();
clusters.sort_by(|a, b| b.file_count.cmp(&a.file_count));
clusters
}
fn build_metrics_section(files: Vec<FileMetrics>) -> MetricsSection {
let clusters = compute_metrics_clusters(&files, 5);
MetricsSection { files, clusters }
}
fn compute_metrics_clusters(files: &[FileMetrics], min_size: usize) -> Vec<MetricsCluster> {
let mut prefix_map: std::collections::HashMap<String, Vec<&FileMetrics>> =
std::collections::HashMap::new();
for f in files {
let prefix = std::path::Path::new(&f.file)
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
if !prefix.is_empty() {
prefix_map.entry(prefix).or_default().push(f);
}
}
let mut clusters: Vec<MetricsCluster> = prefix_map
.into_iter()
.filter(|(_, v)| v.len() >= min_size)
.map(|(prefix, entries)| {
let present_count = entries.iter().filter(|e| e.present).count();
MetricsCluster {
path_prefix: prefix,
file_count: entries.len(),
present_count,
}
})
.collect();
clusters.sort_by(|a, b| b.file_count.cmp(&a.file_count));
clusters
}
fn find_anchor(graph: &CodeGraph, path: &str, method: Option<&str>) -> Option<GraphNodeIndex> {
let normalized = crate::slo::matcher::normalize_route_path(path);
graph.graph.node_indices().find(|&idx| {
if let GraphNode::Function {
is_handler: true,
http_path: Some(p),
http_method,
..
} = &graph.graph[idx]
{
let path_match = crate::slo::matcher::normalize_route_path(p) == normalized;
let method_match = method
.map(|m| {
http_method
.as_deref()
.map(|hm| hm.eq_ignore_ascii_case(m))
.unwrap_or(false)
})
.unwrap_or(true);
path_match && method_match
} else {
false
}
})
}
fn find_function_anchor(graph: &CodeGraph, name: &str) -> Option<GraphNodeIndex> {
let lower = name.to_lowercase();
graph
.graph
.node_indices()
.find(|&idx| {
if let GraphNode::Function { name: fn_name, .. } = &graph.graph[idx] {
fn_name.to_lowercase() == lower
} else {
false
}
})
.or_else(|| {
graph.graph.node_indices().find(|&idx| {
if let GraphNode::Function { name: fn_name, .. } = &graph.graph[idx] {
fn_name.to_lowercase().contains(&lower)
} else {
false
}
})
})
}
fn collect_nodes_with_path<'a>(
node: &'a CoverageNode,
path: &mut Vec<PathHop>,
out: &mut Vec<(&'a CoverageNode, Vec<PathHop>)>,
) {
out.push((node, path.clone()));
path.push(PathHop {
name: node.name.clone(),
location: Location::new(node.file.clone(), node.line),
});
for child in &node.children {
collect_nodes_with_path(child, path, out);
}
path.pop();
}
const STRUCTURED_LOGGING_LIBS: &[&str] = &[
"structlog",
"loguru",
"zap",
"zerolog",
"logrus",
"winston",
"bunyan",
"slog",
];
fn file_logging_quality(graph: &CodeGraph, file_path: &str) -> FileLogging {
let file_idx = graph.find_file_by_path(file_path);
let file_idx = match file_idx {
Some(i) => i,
None => {
return FileLogging {
file: file_path.to_string(),
quality: LoggingQuality::None_,
library: String::new(),
};
}
};
for edge in graph.graph.edges_directed(file_idx, Direction::Outgoing) {
if matches!(edge.weight(), GraphEdgeKind::UsesLibrary) {
if let GraphNode::ExternalModule {
name,
category: ModuleCategory::Logging,
..
} = &graph.graph[edge.target()]
{
let (quality, lib) = classify_logging_lib(name);
return FileLogging {
file: file_path.to_string(),
quality,
library: lib,
};
}
}
}
for edge in graph.graph.edges_directed(file_idx, Direction::Outgoing) {
if matches!(edge.weight(), GraphEdgeKind::ImportsFrom { .. }) {
let target_idx = edge.target();
for inner in graph.graph.edges_directed(target_idx, Direction::Outgoing) {
if matches!(inner.weight(), GraphEdgeKind::UsesLibrary) {
if let GraphNode::ExternalModule {
name,
category: ModuleCategory::Logging,
..
} = &graph.graph[inner.target()]
{
let (quality, lib) = classify_logging_lib(name);
return FileLogging {
file: file_path.to_string(),
quality,
library: lib,
};
}
}
}
}
}
FileLogging {
file: file_path.to_string(),
quality: LoggingQuality::None_,
library: String::new(),
}
}
fn classify_logging_lib(name: &str) -> (LoggingQuality, String) {
let lower = name.to_lowercase();
if STRUCTURED_LOGGING_LIBS.iter().any(|l| lower.contains(l)) {
(LoggingQuality::Structured, name.to_string())
} else if lower.contains("logging") || lower.contains("log") {
(LoggingQuality::Plain, name.to_string())
} else {
(LoggingQuality::None_, name.to_string())
}
}
const METRIC_LIBS: &[&str] = &[
"prometheus",
"prometheus_client",
"statsd",
"dogstatsd",
"datadog",
"influxdb",
"graphite",
"metric",
];
fn file_metrics(graph: &CodeGraph, file_path: &str) -> FileMetrics {
let file_idx = graph.find_file_by_path(file_path);
let file_idx = match file_idx {
Some(i) => i,
None => {
return FileMetrics {
file: file_path.to_string(),
present: false,
library: None,
};
}
};
for edge in graph.graph.edges_directed(file_idx, Direction::Outgoing) {
if matches!(edge.weight(), GraphEdgeKind::UsesLibrary) {
if let GraphNode::ExternalModule { name, .. } = &graph.graph[edge.target()] {
let lower = name.to_lowercase();
if METRIC_LIBS.iter().any(|m| lower.contains(m)) {
return FileMetrics {
file: file_path.to_string(),
present: true,
library: Some(name.clone()),
};
}
}
}
}
FileMetrics {
file: file_path.to_string(),
present: false,
library: None,
}
}
impl NodeRole {
fn method_str(&self) -> Option<&str> {
match self {
NodeRole::HttpHandler { method, .. } => Some(method.as_str()),
_ => None,
}
}
fn path_str(&self) -> Option<&str> {
match self {
NodeRole::HttpHandler { path, .. } => Some(path.as_str()),
_ => None,
}
}
}
fn render_sections(report: &TelemetryReport) {
println!();
println!(
"Telemetry Coverage for {}",
report.target.bright_white().bold()
);
println!();
let total = report.routes.len();
if total == 0 {
println!(" {}Traces{}", "──".cyan(), "──".cyan());
println!(" No routes in scope.");
} else if report.target_kind == "directory" {
println!(" {}Traces by Method{}", "──".cyan(), "──".cyan());
let reads: Vec<&RouteTelemetry> = report
.routes
.iter()
.filter(|r| !is_write_method(&r.method))
.collect();
let writes: Vec<&RouteTelemetry> = report
.routes
.iter()
.filter(|r| is_write_method(&r.method))
.collect();
for (routes, label, methods) in [
(&reads, "read ", "(GET)"),
(&writes, "write", "(POST/PUT/PATCH/DELETE)"),
] {
let n = routes.len();
if n == 0 {
continue;
}
let explicit = routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::Explicit)
.count();
let framework = routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::FrameworkAuto)
.count();
let unobserved = routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::None)
.count();
println!(
" {} ({:>3}) {} {} ({:>3}%) {} {} ({:>3}%) {} {} ({:>3}%) {}",
label,
n,
"●".green(),
fmt_count(explicit, n),
pct(explicit, n),
"◐".yellow(),
fmt_count(framework, n),
pct(framework, n),
"○".normal(),
fmt_count(unobserved, n),
pct(unobserved, n),
methods.bright_black(),
);
}
} else {
println!(" {}Traces{}", "──".cyan(), "──".cyan());
let explicit = report
.routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::Explicit)
.count();
let framework = report
.routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::FrameworkAuto)
.count();
let unobserved = report
.routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::None)
.count();
println!(
" {} explicit {} ({:>3}%)",
"●".green(),
fmt_count(explicit, total),
pct(explicit, total)
);
println!(
" {} framework auto {} ({:>3}%)",
"◐".yellow(),
fmt_count(framework, total),
pct(framework, total)
);
println!(
" {} unobserved {} ({:>3}%)",
"○".normal(),
fmt_count(unobserved, total),
pct(unobserved, total)
);
println!();
for r in &report.routes {
let quality = match r.anchor_kind {
AnchorKind::Explicit => "● explicit".green().to_string(),
AnchorKind::FrameworkAuto => "◐ framework".yellow().to_string(),
AnchorKind::None => "○ unobserved".normal().to_string(),
};
let loc = match r.location.line {
Some(l) => format!("{}:{}", r.location.file, l).bright_black(),
None => r.location.file.bright_black(),
};
println!(
" {:<8} {} {} {}",
format!("{}{}", r.method.magenta().bold(), ":").dimmed(),
r.path.bright_yellow(),
quality,
loc,
);
}
}
println!();
println!(" {}Logging{}", "──".cyan(), "──".cyan());
let structured = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::Structured)
.count();
let plain = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::Plain)
.count();
let none = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::None_)
.count();
if structured > 0 {
println!(
" {} structured {} file{}",
"◉".green(),
structured,
if structured == 1 { "" } else { "s" }
);
}
if plain > 0 {
println!(
" {} plain {} file{}",
"○".normal(),
plain,
if plain == 1 { "" } else { "s" }
);
}
if none > 0 {
println!(
" {} none {} file{}",
"·".dimmed(),
none,
if none == 1 { "" } else { "s" }
);
}
if report.target_kind != "directory" {
for fl in &report.logging.files {
let icon = fl.quality.icon();
let quality_str = match fl.quality {
LoggingQuality::Structured => format!("{} {}", icon.green(), "structured".green()),
LoggingQuality::Plain => format!("{} {}", icon.normal(), "plain".normal()),
LoggingQuality::None_ => format!("{} {}", icon.dimmed(), "none".dimmed()),
};
let lib_str = if !fl.library.is_empty() {
format!(" ({})", fl.library.bright_black())
} else {
String::new()
};
println!(" {} {}{}", fl.file.bright_blue(), quality_str, lib_str);
}
}
println!();
println!(" {}Metrics{}", "──".cyan(), "──".cyan());
let has_metrics = report.metrics.files.iter().any(|m| m.present);
let present_count = report.metrics.files.iter().filter(|m| m.present).count();
let absent_count = report.metrics.files.iter().filter(|m| !m.present).count();
if has_metrics {
println!(
" ◉ present {} file{}",
present_count,
if present_count == 1 { "" } else { "s" }
);
}
if absent_count > 0 {
println!(
" ○ none {} file{}",
absent_count,
if absent_count == 1 { "" } else { "s" }
);
}
if report.target_kind != "directory" {
for m in &report.metrics.files {
let status = if m.present {
format!(
"{} {}",
"◉".green(),
m.library.as_deref().unwrap_or("present").green()
)
} else {
format!("{} {}", "○".normal(), "none".normal())
};
println!(" {} {}", m.file.bright_blue(), status);
}
}
println!();
render_boundaries_section(report);
render_legend();
}
fn render_merged(report: &TelemetryReport) {
println!();
if let Some(r) = report.routes.first() {
let header = if !r.method.is_empty() {
format!("{} {}", r.method.magenta().bold(), r.path.bright_yellow())
} else {
report.target.bright_white().bold().to_string()
};
println!(" Telemetry Coverage for {}", header);
let loc_str = match r.location.line {
Some(l) => format!("{}:{}", r.location.file, l),
None => r.location.file.clone(),
};
if !loc_str.is_empty() {
println!(" {} {}", "at".bright_black(), loc_str.bright_black());
}
let trace_str = match r.anchor_kind {
AnchorKind::Explicit => "● explicit".green(),
AnchorKind::FrameworkAuto => "◐ framework auto".yellow(),
AnchorKind::None => "○ unobserved".normal(),
};
println!("\n trace: {}", trace_str);
}
if report.target_kind == "module" && report.routes.len() <= 1 {
}
if let Some(r) = report.routes.first() {
let quality_str = match r.logs.kind {
LoggingQuality::Structured => format!("{} structured", "◉".green()),
LoggingQuality::Plain => format!("{} plain", "○".normal()),
LoggingQuality::None_ => format!("{} none", "·".dimmed()),
};
let lib_str = r
.logs
.library
.as_deref()
.map(|l| format!(" ({})", l.bright_black()))
.unwrap_or_default();
println!(" logging: {}{}", quality_str, lib_str);
let metrics_status = if r.metrics.present {
format!(
"{} {}",
"◉".green(),
r.metrics.library.as_deref().unwrap_or("present").green()
)
} else {
format!("{} {}", "○".normal(), "none".normal())
};
println!(" metrics: {}", metrics_status);
}
println!();
render_boundaries_section(report);
render_legend();
}
fn render_catalog(report: &TelemetryReport) {
let total_files = report.corpus.files;
let total_routes = report.corpus.routes_total;
let reads = report.corpus.routes_read;
let writes = report.corpus.routes_write;
let log_structured = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::Structured)
.count();
let log_plain = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::Plain)
.count();
let log_none_count = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::None_)
.count();
let metrics_present = report.metrics.files.iter().filter(|m| m.present).count();
let metrics_total = report.metrics.files.len();
let read_deep = report
.routes
.iter()
.filter(|r| !is_write_method(&r.method) && r.anchor_kind == AnchorKind::Explicit)
.count();
let read_shallow = report
.routes
.iter()
.filter(|r| !is_write_method(&r.method) && r.anchor_kind == AnchorKind::FrameworkAuto)
.count();
let read_none = report
.routes
.iter()
.filter(|r| !is_write_method(&r.method) && r.anchor_kind == AnchorKind::None)
.count();
let write_deep = report
.routes
.iter()
.filter(|r| is_write_method(&r.method) && r.anchor_kind == AnchorKind::Explicit)
.count();
let write_shallow = report
.routes
.iter()
.filter(|r| is_write_method(&r.method) && r.anchor_kind == AnchorKind::FrameworkAuto)
.count();
let write_none = report
.routes
.iter()
.filter(|r| is_write_method(&r.method) && r.anchor_kind == AnchorKind::None)
.count();
let db_total = report.boundaries.db.len();
let db_covered = report
.boundaries
.db
.iter()
.filter(|s| s.anchor_kind != AnchorKind::None)
.count();
let http_total = report.boundaries.http.len();
let http_covered = report
.boundaries
.http
.iter()
.filter(|s| s.anchor_kind != AnchorKind::None)
.count();
println!();
println!("Telemetry in {}", report.target.bright_white().bold());
println!(
"{} files, {} routes ({} read, {} write)",
total_files.to_string().yellow(),
total_routes.to_string().yellow(),
reads.to_string().yellow(),
writes.to_string().yellow(),
);
println!();
let fine_total = read_deep + write_deep;
let shallow_total = read_shallow + write_shallow;
let none_total = read_none + write_none;
{
let mut parts: Vec<String> = Vec::new();
if log_structured > 0 {
parts.push(format!(
"structured logging in {} of {} file{}",
log_structured,
total_files,
if total_files == 1 { "" } else { "s" }
));
} else if log_plain > 0 {
parts.push(format!(
"plain logging in {} of {} file{}",
log_plain,
total_files,
if total_files == 1 { "" } else { "s" }
));
}
if log_none_count > 0 {
parts.push(format!(
"{} file{} silent",
log_none_count,
if log_none_count == 1 { "" } else { "s" }
));
}
if metrics_total > 0 {
if metrics_present == 0 {
parts.push("no metrics".to_string());
} else if metrics_present < metrics_total {
parts.push(format!(
"metrics in {} of {} file{}",
metrics_present,
metrics_total,
if metrics_total == 1 { "" } else { "s" }
));
}
}
if parts.is_empty() {
println!("No logging or metrics detected.");
} else {
println!("Logging: {}.", parts.join(", "));
}
}
if total_routes > 0 {
println!();
if none_total == total_routes {
println!("Spans: none. No route is instrumented.");
} else if fine_total == total_routes {
println!(
"Spans: all {} route{} carry explicit attributes.",
total_routes,
if total_routes == 1 { "" } else { "s" }
);
} else {
let mut parts: Vec<String> = Vec::new();
if fine_total > 0 {
parts.push(format!("{} fine (explicit attributes)", fine_total));
}
if shallow_total > 0 {
parts.push(format!("{} coarse (framework only)", shallow_total));
}
if none_total > 0 {
parts.push(format!("{} unobserved", none_total));
}
println!(
"Spans: {} of {} routes — {}.",
fine_total + shallow_total,
total_routes,
parts.join(", ")
);
}
let has_boundaries = db_total > 0 || http_total > 0;
if has_boundaries {
let mut bound_parts: Vec<String> = Vec::new();
if db_total > 0 {
if db_covered == db_total {
bound_parts.push(format!("db {}/{}", db_covered, db_total));
} else {
bound_parts.push(format!("db {}/{}", db_covered, db_total));
}
}
if http_total > 0 {
bound_parts.push(format!("http {}/{}", http_covered, http_total));
}
println!("Boundaries: {}.", bound_parts.join(", "));
}
}
println!();
println!(
"{}",
format!(
"To see the same as an aggregate dashboard: unfault telemetry {} --summary",
report.target
)
.bright_black()
);
println!();
}
fn render_summary(report: &TelemetryReport) {
println!();
println!("Telemetry summary, {}", report.target.bright_white().bold());
println!();
let total = report.routes.len();
if total > 0 {
println!(" Traces by method");
let reads: Vec<&RouteTelemetry> = report
.routes
.iter()
.filter(|r| !is_write_method(&r.method))
.collect();
let writes: Vec<&RouteTelemetry> = report
.routes
.iter()
.filter(|r| is_write_method(&r.method))
.collect();
for (routes, label, methods) in [
(&reads, "read", ""),
(&writes, "write", "(POST/PUT/PATCH/DELETE)"),
] {
let n = routes.len();
if n == 0 {
continue;
}
let explicit = routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::Explicit)
.count();
let framework = routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::FrameworkAuto)
.count();
let unobserved = routes
.iter()
.filter(|r| r.anchor_kind == AnchorKind::None)
.count();
println!(
" {} ({:>3}) explicit attrs {:>3} framework auto {:>3} no span {:>3} {}",
label,
n,
explicit,
framework,
unobserved,
methods.bright_black(),
);
}
println!();
}
let structured = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::Structured)
.count();
let plain = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::Plain)
.count();
let none = report
.logging
.files
.iter()
.filter(|l| l.quality == LoggingQuality::None_)
.count();
println!(" Logging");
if structured > 0 {
println!(
" structured {} file{}",
structured,
if structured == 1 { "" } else { "s" }
);
}
if plain > 0 {
println!(
" plain {} file{}",
plain,
if plain == 1 { "" } else { "s" }
);
}
if none > 0 {
println!(
" none {} file{}",
none,
if none == 1 { "" } else { "s" }
);
}
println!();
let has_metrics = report.metrics.files.iter().any(|m| m.present);
let absent_count = report.metrics.files.iter().filter(|m| !m.present).count();
println!(" Metrics");
if has_metrics {
let present_count = report.metrics.files.iter().filter(|m| m.present).count();
println!(
" present {} file{}",
present_count,
if present_count == 1 { "" } else { "s" }
);
}
if absent_count > 0 {
println!(
" none {} file{}",
absent_count,
if absent_count == 1 { "" } else { "s" }
);
}
println!();
}
fn render_compact(report: &TelemetryReport) {
println!();
println!(
" Telemetry Coverage for {}",
report.target.bright_white().bold()
);
println!();
for r in &report.routes {
let quality = match r.anchor_kind {
AnchorKind::Explicit => "● explicit".green(),
AnchorKind::FrameworkAuto => "◐ framework".yellow(),
AnchorKind::None => "○ none".normal(),
};
let loc: colored::ColoredString = match r.location.line {
Some(l) => format!(" {}:{}", r.location.file, l).bright_black(),
None => r.location.file.as_str().bright_black(),
};
println!(
" {:<8} {} {} {}",
format!("{}{}", r.method.magenta().bold(), ":").dimmed(),
r.path.bright_yellow(),
quality,
loc,
);
}
println!();
render_boundaries_line(report);
render_legend();
}
fn render_legend() {
println!();
println!("{}", " ── Legend ──".bright_black());
println!(
" {} {}",
"trace ● explicit / ◐ framework auto / ○ unobserved".dimmed(),
"— span kind".bright_black()
);
println!(
" {} {}",
"log ◉ structured / ○ plain / · none".dimmed(),
"— quality".bright_black()
);
println!();
}
fn render_boundaries_section(report: &TelemetryReport) {
let b = &report.boundaries;
let has_boundaries = !b.db.is_empty() || !b.http.is_empty() || !b.remote.is_empty();
println!(" {}Boundaries{}", "──".cyan(), "──".cyan());
if !has_boundaries {
println!(" No downstream calls detected.");
return;
}
render_boundary_group("db queries", &b.db);
render_boundary_group("http clients", &b.http);
render_boundary_group("remote calls", &b.remote);
}
fn render_boundary_group(label: &str, sites: &[BoundaryCallSite]) {
if sites.is_empty() {
return;
}
let total = sites.len();
let covered = sites
.iter()
.filter(|s| s.anchor_kind != AnchorKind::None)
.count();
let pct = (covered as f64 / total as f64 * 100.0) as usize;
let icon = if pct == 100 {
"●".green()
} else if pct > 0 {
"◐".yellow()
} else {
"○".normal()
};
let pct_str = format!("{}%", pct);
let colored_pct = if pct == 100 {
pct_str.green()
} else if pct >= 50 {
pct_str.yellow()
} else {
pct_str.red()
};
println!(
" {} {:<16} {:>2} / {:>2} {}",
icon, label, covered, total, colored_pct
);
}
fn render_boundaries_line(report: &TelemetryReport) {
let b = &report.boundaries;
let db_total = b.db.len();
let db_covered =
b.db.iter()
.filter(|s| s.anchor_kind != AnchorKind::None)
.count();
let http_total = b.http.len();
let http_covered = b
.http
.iter()
.filter(|s| s.anchor_kind != AnchorKind::None)
.count();
let remote_total = b.remote.len();
let remote_covered = b
.remote
.iter()
.filter(|s| s.anchor_kind != AnchorKind::None)
.count();
let parts: Vec<String> = vec![
format!("db {} / {}", db_covered, db_total),
format!("http {} / {}", http_covered, http_total),
format!("remote {} / {}", remote_covered, remote_total),
];
println!(" boundaries: {}", parts.join(" ").bright_black());
}
fn is_write_method(method: &str) -> bool {
matches!(method, "POST" | "PUT" | "PATCH" | "DELETE")
}
fn fmt_count(n: usize, total: usize) -> String {
if total == 0 {
"0".to_string()
} else {
n.to_string()
}
}
fn pct(n: usize, total: usize) -> String {
if total == 0 {
"0".to_string()
} else {
format!("{}", ((n as f64 / total as f64) * 100.0) as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn statement_kind_sqlalchemy_session_get_is_select() {
assert_eq!(
infer_statement_kind("db_session.get"),
StatementKind::Select
);
assert_eq!(infer_statement_kind("session.get"), StatementKind::Select);
assert_eq!(
infer_statement_kind("self.db_session.get"),
StatementKind::Select
);
}
#[test]
fn statement_kind_sqlalchemy_query_chain_is_select() {
assert_eq!(infer_statement_kind("session.query"), StatementKind::Select);
assert_eq!(
infer_statement_kind("session.filter"),
StatementKind::Select
);
assert_eq!(
infer_statement_kind("session.filter_by"),
StatementKind::Select
);
assert_eq!(infer_statement_kind("session.count"), StatementKind::Select);
assert_eq!(
infer_statement_kind("session.exists"),
StatementKind::Select
);
}
#[test]
fn statement_kind_sqlalchemy_writes() {
assert_eq!(infer_statement_kind("session.add"), StatementKind::Insert);
assert_eq!(
infer_statement_kind("session.add_all"),
StatementKind::Insert
);
assert_eq!(
infer_statement_kind("session.bulk_save_objects"),
StatementKind::Insert
);
assert_eq!(infer_statement_kind("session.merge"), StatementKind::Update);
assert_eq!(
infer_statement_kind("session.delete"),
StatementKind::Delete
);
}
#[test]
fn statement_kind_transaction_control() {
assert_eq!(
infer_statement_kind("db_session.commit"),
StatementKind::Commit
);
assert_eq!(infer_statement_kind("session.flush"), StatementKind::Commit);
assert_eq!(
infer_statement_kind("session.rollback"),
StatementKind::Rollback
);
}
#[test]
fn statement_kind_raw_sql() {
assert_eq!(infer_statement_kind("session.execute"), StatementKind::Raw);
assert_eq!(infer_statement_kind("conn.executemany"), StatementKind::Raw);
assert_eq!(infer_statement_kind("text"), StatementKind::Raw);
assert_eq!(infer_statement_kind("cursor.exec"), StatementKind::Raw);
}
#[test]
fn statement_kind_django_queryset_methods() {
assert_eq!(
infer_statement_kind("User.objects.get"),
StatementKind::Select
);
assert_eq!(
infer_statement_kind("User.objects.filter"),
StatementKind::Select
);
assert_eq!(
infer_statement_kind("User.objects.all"),
StatementKind::Select
);
assert_eq!(
infer_statement_kind("User.objects.first"),
StatementKind::Select
);
assert_eq!(
infer_statement_kind("User.objects.exists"),
StatementKind::Select
);
assert_eq!(
infer_statement_kind("User.objects.count"),
StatementKind::Select
);
assert_eq!(
infer_statement_kind("User.objects.create"),
StatementKind::Insert
);
assert_eq!(
infer_statement_kind("User.objects.bulk_create"),
StatementKind::Insert
);
assert_eq!(
infer_statement_kind("User.objects.get_or_create"),
StatementKind::Insert
);
assert_eq!(
infer_statement_kind("User.objects.update"),
StatementKind::Update
);
assert_eq!(
infer_statement_kind("User.objects.bulk_update"),
StatementKind::Update
);
assert_eq!(
infer_statement_kind("User.objects.delete"),
StatementKind::Delete
);
}
#[test]
fn statement_kind_django_instance_save_classified_as_update() {
assert_eq!(infer_statement_kind("user.save"), StatementKind::Update);
}
#[test]
fn statement_kind_sqlalchemy_core_standalone() {
assert_eq!(infer_statement_kind("select"), StatementKind::Select);
assert_eq!(infer_statement_kind("insert"), StatementKind::Insert);
assert_eq!(infer_statement_kind("update"), StatementKind::Update);
assert_eq!(infer_statement_kind("delete"), StatementKind::Delete);
}
#[test]
fn statement_kind_unknown_falls_back_to_other() {
assert_eq!(
infer_statement_kind("session.some_custom_method"),
StatementKind::Other
);
assert_eq!(infer_statement_kind(""), StatementKind::Other);
}
#[test]
fn statement_kind_case_insensitive() {
assert_eq!(
infer_statement_kind("Session.COMMIT"),
StatementKind::Commit
);
assert_eq!(
infer_statement_kind("DB_Session.Get"),
StatementKind::Select
);
}
use crate::commands::graph::{
Location, NodeRole, UnobservedByRole, UnobservedCallee, UnobservedPaths,
};
fn make_unobserved_callee(name: &str, file: &str, role_tag: &str) -> UnobservedCallee {
UnobservedCallee {
name: name.to_string(),
location: Location::new(file, None),
role: match role_tag {
"database" => NodeRole::Database,
"http" => NodeRole::HttpClient,
"remote" => NodeRole::RemoteCall {
service: "svc".to_string(),
},
_ => NodeRole::Logic,
},
depth: 1,
path: vec![],
}
}
fn make_route(method: &str, path: &str, handler: &str, db: Vec<&str>) -> RouteTelemetry {
let database: Vec<UnobservedCallee> = db
.iter()
.map(|name| make_unobserved_callee(name, "app.py", "database"))
.collect();
RouteTelemetry {
method: method.to_string(),
path: path.to_string(),
handler: handler.to_string(),
location: Location::new("app.py", None),
anchor_kind: AnchorKind::FrameworkAuto,
anchor_attributes: vec![],
logs: RouteLogs {
kind: LoggingQuality::None_,
library: None,
},
metrics: RouteMetrics {
present: false,
library: None,
},
total_callees: 0,
instrumented_callees: 0,
callees: vec![],
unobserved: UnobservedPaths {
total: database.len(),
anchor_unobserved: !database.is_empty(),
by_role: UnobservedByRole {
database,
http: vec![],
remote: vec![],
logic: vec![],
},
deepest: vec![],
},
}
}
fn empty_report(routes: Vec<RouteTelemetry>) -> TelemetryReport {
TelemetryReport {
target: ".".to_string(),
target_kind: "directory".to_string(),
corpus: Corpus {
files: 1,
routes_total: routes.len(),
routes_read: 0,
routes_write: 0,
},
routes,
logging: LoggingSection {
files: vec![],
clusters: vec![],
},
metrics: MetricsSection {
files: vec![],
clusters: vec![],
},
boundaries: Boundaries {
db: vec![],
http: vec![],
remote: vec![],
},
}
}
#[test]
fn rank_unobserved_sorts_by_route_count_descending() {
let report = empty_report(vec![
make_route(
"POST",
"/a",
"h_a",
vec!["db_session.commit", "db_session.get"],
),
make_route(
"POST",
"/b",
"h_b",
vec!["db_session.commit", "db_session.get", "db_session.execute"],
),
make_route("POST", "/c", "h_c", vec!["db_session.commit"]),
]);
let ranked = rank_unobserved_boundaries(&report, "all");
assert_eq!(ranked[0].name, "db_session.commit");
assert_eq!(ranked[0].route_count, 3);
assert_eq!(ranked[1].name, "db_session.get");
assert_eq!(ranked[1].route_count, 2);
assert_eq!(ranked[2].name, "db_session.execute");
assert_eq!(ranked[2].route_count, 1);
}
#[test]
fn rank_unobserved_deduplicates_same_route() {
let report = empty_report(vec![make_route(
"POST",
"/a",
"h_a",
vec!["db_session.commit", "db_session.commit"],
)]);
let ranked = rank_unobserved_boundaries(&report, "all");
assert_eq!(ranked.len(), 1);
assert_eq!(ranked[0].route_count, 1, "same route must not double-count");
}
#[test]
fn rank_unobserved_role_filter_db_only() {
let db_callee = make_unobserved_callee("db_session.commit", "app.py", "database");
let http_callee = make_unobserved_callee("requests.post", "app.py", "http");
let mut route = make_route("POST", "/x", "h", vec![]);
route.unobserved.by_role.database = vec![db_callee];
route.unobserved.by_role.http = vec![http_callee];
let report = empty_report(vec![route]);
let db_ranked = rank_unobserved_boundaries(&report, "db");
assert_eq!(db_ranked.len(), 1);
assert_eq!(db_ranked[0].role, "database");
let http_ranked = rank_unobserved_boundaries(&report, "http");
assert_eq!(http_ranked.len(), 1);
assert_eq!(http_ranked[0].role, "http");
let all_ranked = rank_unobserved_boundaries(&report, "all");
assert_eq!(all_ranked.len(), 2);
}
#[test]
fn rank_unobserved_empty_routes_returns_empty() {
let report = empty_report(vec![]);
let ranked = rank_unobserved_boundaries(&report, "all");
assert!(ranked.is_empty());
}
#[test]
fn rank_unobserved_db_boundaries_get_statement_kind() {
let report = empty_report(vec![make_route(
"POST",
"/a",
"h",
vec!["db_session.commit", "db_session.get"],
)]);
let ranked = rank_unobserved_boundaries(&report, "db");
let commit = ranked
.iter()
.find(|e| e.name == "db_session.commit")
.unwrap();
assert_eq!(commit.statement_kind, Some(StatementKind::Commit));
let get = ranked.iter().find(|e| e.name == "db_session.get").unwrap();
assert_eq!(get.statement_kind, Some(StatementKind::Select));
}
#[test]
fn rank_unobserved_tie_broken_by_name() {
let report = empty_report(vec![make_route(
"POST",
"/a",
"h",
vec!["db_session.zebra", "db_session.alpha"],
)]);
let ranked = rank_unobserved_boundaries(&report, "all");
assert_eq!(ranked[0].name, "db_session.alpha");
assert_eq!(ranked[1].name, "db_session.zebra");
}
#[test]
fn rank_unobserved_routes_sorted_in_each_entry() {
let report = empty_report(vec![
make_route("POST", "/z", "h_z", vec!["db_session.commit"]),
make_route("GET", "/a", "h_a", vec!["db_session.commit"]),
]);
let ranked = rank_unobserved_boundaries(&report, "all");
assert_eq!(ranked[0].name, "db_session.commit");
assert_eq!(ranked[0].route_count, 2);
assert_eq!(ranked[0].routes[0].method, "GET");
assert_eq!(ranked[0].routes[1].method, "POST");
}
#[test]
fn rank_unobserved_same_name_different_line_stays_separate() {
fn callee_at(name: &str, file: &str, line: u32) -> UnobservedCallee {
UnobservedCallee {
name: name.to_string(),
location: Location::new(file, Some(line)),
role: NodeRole::Database,
depth: 1,
path: vec![],
}
}
fn route_with_callee(
method: &str,
path: &str,
handler: &str,
callee: UnobservedCallee,
) -> RouteTelemetry {
RouteTelemetry {
method: method.to_string(),
path: path.to_string(),
handler: handler.to_string(),
location: Location::new("router.py", None),
anchor_kind: AnchorKind::FrameworkAuto,
anchor_attributes: vec![],
logs: RouteLogs {
kind: LoggingQuality::None_,
library: None,
},
metrics: RouteMetrics {
present: false,
library: None,
},
total_callees: 1,
instrumented_callees: 0,
callees: vec![],
unobserved: UnobservedPaths {
total: 1,
anchor_unobserved: false,
by_role: UnobservedByRole {
database: vec![callee],
http: vec![],
remote: vec![],
logic: vec![],
},
deepest: vec![],
},
}
}
let report = empty_report(vec![
route_with_callee(
"POST",
"/publish",
"publish",
callee_at("db_session.commit", "router.py", 279),
),
route_with_callee(
"DELETE",
"/delete",
"delete",
callee_at("db_session.commit", "router.py", 338),
),
]);
let ranked = rank_unobserved_boundaries(&report, "all");
assert_eq!(
ranked.len(),
2,
"expected 2 distinct entries (one per line), got {}",
ranked.len()
);
for entry in &ranked {
assert_eq!(
entry.route_count, 1,
"each call site is reached by exactly one route"
);
}
}
fn make_node(
name: &str,
file: &str,
line: Option<u32>,
depth: i32,
children: Vec<CoverageNode>,
) -> CoverageNode {
CoverageNode {
name: name.to_string(),
file: file.to_string(),
line,
depth,
direction: "down".to_string(),
span: SpanSignal::None,
role: NodeRole::Logic,
children,
}
}
#[test]
fn collect_nodes_with_path_empty_for_root() {
let root = make_node("handler", "app.py", Some(1), 0, vec![]);
let mut path = Vec::new();
let mut out = Vec::new();
collect_nodes_with_path(&root, &mut path, &mut out);
assert_eq!(out.len(), 1);
assert_eq!(out[0].0.name, "handler");
assert!(
out[0].1.is_empty(),
"root node must have empty via, got {:?}",
out[0].1
);
}
#[test]
fn collect_nodes_with_path_records_ancestors() {
let tree = make_node(
"handler",
"app.py",
Some(10),
0,
vec![make_node(
"service",
"service.py",
Some(20),
1,
vec![make_node(
"helper",
"helper.py",
Some(30),
2,
vec![make_node(
"db_session.commit",
"helper.py",
Some(35),
3,
vec![],
)],
)],
)],
);
let mut path = Vec::new();
let mut out = Vec::new();
collect_nodes_with_path(&tree, &mut path, &mut out);
assert_eq!(out.len(), 4);
assert_eq!(out[0].0.name, "handler");
assert!(out[0].1.is_empty());
assert_eq!(out[1].0.name, "service");
assert_eq!(out[1].1.len(), 1);
assert_eq!(out[1].1[0].name, "handler");
assert_eq!(out[2].0.name, "helper");
assert_eq!(out[2].1.len(), 2);
assert_eq!(out[2].1[0].name, "handler");
assert_eq!(out[2].1[1].name, "service");
assert_eq!(out[3].0.name, "db_session.commit");
assert_eq!(out[3].1.len(), 3);
assert_eq!(out[3].1[0].name, "handler");
assert_eq!(out[3].1[1].name, "service");
assert_eq!(out[3].1[2].name, "helper");
}
#[test]
fn collect_nodes_with_path_invariant_via_len_equals_depth() {
let tree = make_node(
"h",
"f.py",
Some(1),
0,
vec![make_node(
"a",
"f.py",
Some(2),
1,
vec![make_node("b", "f.py", Some(3), 2, vec![])],
)],
);
let mut path = Vec::new();
let mut out = Vec::new();
collect_nodes_with_path(&tree, &mut path, &mut out);
for (node, via) in &out {
assert_eq!(
via.len() as i32,
node.depth,
"via.len() must equal depth for {}, got {} != {}",
node.name,
via.len(),
node.depth,
);
}
}
#[test]
fn collect_nodes_with_path_locations_preserved() {
let tree = make_node(
"outer",
"outer.py",
Some(100),
0,
vec![make_node("inner", "inner.py", Some(200), 1, vec![])],
);
let mut path = Vec::new();
let mut out = Vec::new();
collect_nodes_with_path(&tree, &mut path, &mut out);
let inner_via = &out[1].1;
assert_eq!(inner_via.len(), 1);
assert_eq!(inner_via[0].name, "outer");
assert_eq!(inner_via[0].location.file, "outer.py");
assert_eq!(inner_via[0].location.line, Some(100));
}
}