use std::collections::{HashMap, HashSet};
use clap::Args;
use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::TraceSpan};
use eyre::bail;
use crate::{
command::{Executable, default_tracing, trace::format_duration_us},
common::{CoordinatorOptions, expect_reply, send_control_request},
ws_client::WsSession,
};
#[derive(Debug, Args)]
#[clap(verbatim_doc_comment)]
pub struct View {
pub trace_id: String,
#[clap(flatten)]
coordinator: CoordinatorOptions,
}
impl Executable for View {
fn execute(self) -> eyre::Result<()> {
default_tracing()?;
let session = self.coordinator.connect()?;
let trace_id = resolve_trace_id(&session, &self.trace_id)?;
let reply = send_control_request(&session, &ControlRequest::GetTraceSpans { trace_id })?;
let spans = expect_reply!(reply, TraceSpans(s))?;
if spans.is_empty() {
println!("No spans found for this trace.");
return Ok(());
}
print_span_tree(&spans);
Ok(())
}
}
fn resolve_trace_id(session: &WsSession, prefix: &str) -> eyre::Result<String> {
if prefix.is_empty() {
bail!("trace ID prefix must not be empty");
}
if uuid::Uuid::parse_str(prefix).is_ok() {
return Ok(prefix.to_string());
}
let traces = super::fetch_traces(session)?;
let matches: Vec<_> = traces
.iter()
.filter(|t| t.trace_id.starts_with(prefix))
.collect();
match matches.len() {
0 => bail!("no trace found matching prefix `{prefix}`"),
1 => Ok(matches[0].trace_id.clone()),
n => bail!("prefix `{prefix}` is ambiguous ({n} matches). Use a longer prefix."),
}
}
fn print_span_tree(spans: &[TraceSpan]) {
let DisplayPlan { rows, partial } = plan_span_tree(spans);
if partial {
eprintln!(
"note: trace is partial — some spans reference a parent that is absent from this \
snapshot or form a cycle (shown as top-level roots)"
);
}
for (span, depth) in rows {
let indent = " ".repeat(depth);
let dur = format_duration_us(span.duration_us);
let fields = if span.fields.is_empty() {
String::new()
} else {
let pairs: Vec<_> = span
.fields
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
format!(" {{{}}}", pairs.join(", "))
};
println!("{indent}{} [{} {}]{fields}", span.name, span.level, dur);
}
}
struct DisplayPlan<'a> {
rows: Vec<(&'a TraceSpan, usize)>,
partial: bool,
}
fn plan_span_tree(spans: &[TraceSpan]) -> DisplayPlan<'_> {
let mut children: HashMap<Option<u64>, Vec<&TraceSpan>> = HashMap::new();
for span in spans {
children.entry(span.parent_span_id).or_default().push(span);
}
for list in children.values_mut() {
list.sort_by_key(|s| s.start_time);
}
let present: HashSet<u64> = spans.iter().map(|s| s.span_id).collect();
let mut roots: Vec<&TraceSpan> = spans
.iter()
.filter(|s| s.parent_span_id.is_none_or(|p| !present.contains(&p)))
.collect();
roots.sort_by_key(|s| s.start_time);
let orphaned = roots.iter().any(|r| r.parent_span_id.is_some());
let mut rows = Vec::new();
let mut visited = HashSet::new();
for root in &roots {
collect_rows(root, &children, 0, &mut visited, &mut rows);
}
let mut leftover: Vec<&TraceSpan> = spans
.iter()
.filter(|s| !visited.contains(&s.span_id))
.collect();
leftover.sort_by_key(|s| s.start_time);
let cyclic = !leftover.is_empty();
for span in leftover {
collect_rows(span, &children, 0, &mut visited, &mut rows);
}
DisplayPlan {
rows,
partial: orphaned || cyclic,
}
}
fn collect_rows<'a>(
span: &'a TraceSpan,
children: &HashMap<Option<u64>, Vec<&'a TraceSpan>>,
depth: usize,
visited: &mut HashSet<u64>,
rows: &mut Vec<(&'a TraceSpan, usize)>,
) {
if !visited.insert(span.span_id) {
return; }
rows.push((span, depth));
if let Some(kids) = children.get(&Some(span.span_id)) {
for child in kids {
collect_rows(child, children, depth + 1, visited, rows);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn span(span_id: u64, parent_span_id: Option<u64>, start_time: u64) -> TraceSpan {
TraceSpan {
trace_id: "t".to_string(),
span_id,
parent_span_id,
name: format!("span{span_id}"),
target: "test".to_string(),
level: "INFO".to_string(),
start_time,
duration_us: 0,
fields: Vec::new(),
}
}
fn rendered(spans: &[TraceSpan]) -> (Vec<(u64, usize)>, bool) {
let plan = plan_span_tree(spans);
let rows = plan.rows.iter().map(|(s, d)| (s.span_id, *d)).collect();
(rows, plan.partial)
}
#[test]
fn nests_children_under_their_root() {
let spans = vec![span(1, None, 0), span(2, Some(1), 1), span(3, Some(1), 2)];
let (rows, partial) = rendered(&spans);
assert_eq!(rows, vec![(1, 0), (2, 1), (3, 1)]);
assert!(!partial, "a complete trace is not partial");
}
#[test]
fn promotes_orphan_whose_parent_is_absent() {
let spans = vec![span(1, Some(99), 0), span(2, Some(1), 1)];
let (rows, partial) = rendered(&spans);
assert_eq!(rows, vec![(1, 0), (2, 1)]);
assert!(partial, "an orphaned span marks the trace partial");
}
#[test]
fn surfaces_all_present_cycle_instead_of_dropping_it() {
let spans = vec![span(1, Some(2), 0), span(2, Some(1), 1)];
let (rows, partial) = rendered(&spans);
let ids: HashSet<u64> = rows.iter().map(|(id, _)| *id).collect();
assert_eq!(ids, HashSet::from([1, 2]), "every cyclic span is shown");
assert_eq!(rows.len(), 2, "each span appears exactly once");
assert!(partial, "a cyclic trace marks the trace partial");
}
#[test]
fn real_tree_alongside_isolated_cycle_renders_both_once() {
let spans = vec![
span(1, None, 0),
span(2, Some(1), 1),
span(3, Some(4), 2),
span(4, Some(3), 3),
];
let (rows, partial) = rendered(&spans);
assert_eq!(rows.len(), 4, "each span appears exactly once");
let ids: HashSet<u64> = rows.iter().map(|(id, _)| *id).collect();
assert_eq!(ids, HashSet::from([1, 2, 3, 4]), "every span is shown");
assert_eq!(rows[0], (1, 0));
assert_eq!(rows[1], (2, 1));
assert!(partial);
}
}