1use crate::cli::types::OutputFormat;
6use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::Path;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum SpanStatus {
15 Ok,
17 Error,
19 Unset,
21}
22
23impl SpanStatus {
24 pub fn from_str_case_insensitive(s: &str) -> Option<Self> {
26 match s.to_lowercase().as_str() {
27 "ok" => Some(SpanStatus::Ok),
28 "error" => Some(SpanStatus::Error),
29 "unset" => Some(SpanStatus::Unset),
30 _ => None,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct OtelSpan {
38 pub name: String,
40 #[serde(default)]
42 pub service_name: Option<String>,
43 #[serde(default)]
45 pub duration_ns: Option<u64>,
46 #[serde(default)]
48 pub status: Option<SpanStatus>,
49 #[serde(default)]
51 pub attributes: serde_json::Map<String, serde_json::Value>,
52 #[serde(default)]
54 pub events: Vec<SpanEvent>,
55 #[serde(default)]
57 pub trace_id: Option<String>,
58 #[serde(default)]
60 pub span_id: Option<String>,
61 #[serde(default)]
63 pub parent_span_id: Option<String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SpanEvent {
69 pub name: String,
71 #[serde(default)]
73 pub timestamp: Option<u64>,
74 #[serde(default)]
76 pub attributes: serde_json::Map<String, serde_json::Value>,
77}
78
79#[derive(Debug, Serialize, Deserialize)]
81pub struct TraceData {
82 #[serde(rename = "resourceSpans", default)]
84 pub resource_spans: Vec<ResourceSpan>,
85 #[serde(default)]
87 pub spans: Vec<OtelSpan>,
88}
89
90#[derive(Debug, Serialize, Deserialize)]
92pub struct ResourceSpan {
93 #[serde(default)]
95 pub resource: Option<Resource>,
96 #[serde(rename = "scopeSpans", default)]
98 pub scope_spans: Vec<ScopeSpan>,
99}
100
101#[derive(Debug, Serialize, Deserialize)]
103pub struct Resource {
104 #[serde(default)]
106 pub attributes: Vec<Attribute>,
107}
108
109#[derive(Debug, Serialize, Deserialize)]
111pub struct ScopeSpan {
112 #[serde(default)]
114 pub spans: Vec<OtlpSpan>,
115}
116
117#[derive(Debug, Serialize, Deserialize)]
119pub struct OtlpSpan {
120 pub name: String,
122 #[serde(rename = "traceId", default)]
124 pub trace_id: Option<String>,
125 #[serde(rename = "spanId", default)]
127 pub span_id: Option<String>,
128 #[serde(rename = "parentSpanId", default)]
130 pub parent_span_id: Option<String>,
131 #[serde(rename = "startTimeUnixNano", default)]
133 pub start_time_unix_nano: Option<String>,
134 #[serde(rename = "endTimeUnixNano", default)]
136 pub end_time_unix_nano: Option<String>,
137 #[serde(default)]
139 pub attributes: Vec<Attribute>,
140 #[serde(default)]
142 pub events: Vec<OtlpEvent>,
143 #[serde(default)]
145 pub status: Option<OtlpStatus>,
146}
147
148#[derive(Debug, Serialize, Deserialize)]
150pub struct Attribute {
151 pub key: String,
153 pub value: AttributeValue,
155}
156
157#[derive(Debug, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct AttributeValue {
161 #[serde(default)]
163 pub string_value: Option<String>,
164 #[serde(default)]
166 pub int_value: Option<i64>,
167 #[serde(default)]
169 pub double_value: Option<f64>,
170 #[serde(default)]
172 pub bool_value: Option<bool>,
173}
174
175#[derive(Debug, Serialize, Deserialize)]
177pub struct OtlpEvent {
178 pub name: String,
180 #[serde(rename = "timeUnixNano", default)]
182 pub time_unix_nano: Option<String>,
183 #[serde(default)]
185 pub attributes: Vec<Attribute>,
186}
187
188#[derive(Debug, Serialize, Deserialize)]
190pub struct OtlpStatus {
191 #[serde(default)]
193 pub code: Option<u32>,
194 #[serde(default)]
196 pub message: Option<String>,
197}
198
199pub fn filter_spans(
217 trace: &Path,
218 grep: Option<&str>,
219 format: &OutputFormat,
220 show_attrs: bool,
221 show_events: bool,
222) -> Result<()> {
223 let trace_data = load_trace(trace)?;
225
226 let pattern = if let Some(grep_str) = grep {
228 Some(regex::Regex::new(grep_str).map_err(|e| {
229 CleanroomError::validation_error(format!("Invalid regex pattern '{}': {}", grep_str, e))
230 })?)
231 } else {
232 None
233 };
234
235 let filtered_spans: Vec<&OtelSpan> = trace_data
237 .spans
238 .iter()
239 .filter(|span| {
240 if let Some(ref regex) = pattern {
242 if !regex.is_match(&span.name) {
243 return false;
244 }
245 }
246 true
247 })
248 .collect();
249
250 match format {
252 OutputFormat::Json => output_json(&filtered_spans, show_attrs, show_events)?,
253 OutputFormat::Human | OutputFormat::Auto => {
254 output_table(&filtered_spans, show_attrs, show_events)?
255 }
256 _ => {
257 return Err(CleanroomError::validation_error(format!(
258 "Unsupported output format for spans: {:?}",
259 format
260 )))
261 }
262 }
263
264 Ok(())
265}
266
267fn load_trace(trace_path: &Path) -> Result<TraceData> {
271 let content = fs::read_to_string(trace_path).map_err(|e| {
272 CleanroomError::io_error(format!(
273 "Failed to read trace file '{}': {}",
274 trace_path.display(),
275 e
276 ))
277 })?;
278
279 let mut trace_data: TraceData = serde_json::from_str(&content).map_err(|e| {
280 CleanroomError::validation_error(format!(
281 "Failed to parse trace JSON from '{}': {}",
282 trace_path.display(),
283 e
284 ))
285 })?;
286
287 if trace_data.spans.is_empty() && !trace_data.resource_spans.is_empty() {
289 trace_data.spans = convert_otlp_to_spans(&trace_data)?;
290 }
291
292 if trace_data.spans.is_empty() {
293 return Err(CleanroomError::validation_error(format!(
294 "No spans found in trace file '{}'",
295 trace_path.display()
296 )));
297 }
298
299 Ok(trace_data)
300}
301
302fn convert_otlp_to_spans(trace_data: &TraceData) -> Result<Vec<OtelSpan>> {
304 let mut spans = Vec::new();
305
306 for resource_span in &trace_data.resource_spans {
307 let service_name = resource_span.resource.as_ref().and_then(|r| {
309 r.attributes.iter().find_map(|attr| {
310 if attr.key == "service.name" {
311 attr.value.string_value.clone()
312 } else {
313 None
314 }
315 })
316 });
317
318 for scope_span in &resource_span.scope_spans {
319 for otlp_span in &scope_span.spans {
320 spans.push(convert_otlp_span(otlp_span, service_name.clone())?);
321 }
322 }
323 }
324
325 Ok(spans)
326}
327
328fn convert_otlp_span(otlp_span: &OtlpSpan, service_name: Option<String>) -> Result<OtelSpan> {
330 let duration_ns = if let (Some(start), Some(end)) = (
332 &otlp_span.start_time_unix_nano,
333 &otlp_span.end_time_unix_nano,
334 ) {
335 let start_ns = start
336 .parse::<u64>()
337 .map_err(|e| CleanroomError::validation_error(format!("Invalid start time: {}", e)))?;
338 let end_ns = end
339 .parse::<u64>()
340 .map_err(|e| CleanroomError::validation_error(format!("Invalid end time: {}", e)))?;
341 Some(end_ns.saturating_sub(start_ns))
342 } else {
343 None
344 };
345
346 let status = otlp_span.status.as_ref().and_then(|s| {
348 s.code.and_then(|code| match code {
349 0 => Some(SpanStatus::Unset),
350 1 => Some(SpanStatus::Ok),
351 2 => Some(SpanStatus::Error),
352 _ => None,
353 })
354 });
355
356 let mut attributes = serde_json::Map::new();
358 for attr in &otlp_span.attributes {
359 let value = if let Some(ref s) = attr.value.string_value {
360 serde_json::Value::String(s.clone())
361 } else if let Some(i) = attr.value.int_value {
362 serde_json::Value::Number(i.into())
363 } else if let Some(d) = attr.value.double_value {
364 serde_json::Number::from_f64(d)
365 .map(serde_json::Value::Number)
366 .unwrap_or(serde_json::Value::Null)
367 } else if let Some(b) = attr.value.bool_value {
368 serde_json::Value::Bool(b)
369 } else {
370 serde_json::Value::Null
371 };
372 attributes.insert(attr.key.clone(), value);
373 }
374
375 let events = otlp_span
377 .events
378 .iter()
379 .map(|e| {
380 let mut event_attrs = serde_json::Map::new();
381 for attr in &e.attributes {
382 let value = if let Some(ref s) = attr.value.string_value {
383 serde_json::Value::String(s.clone())
384 } else {
385 serde_json::Value::Null
386 };
387 event_attrs.insert(attr.key.clone(), value);
388 }
389 SpanEvent {
390 name: e.name.clone(),
391 timestamp: e.time_unix_nano.as_ref().and_then(|t| t.parse().ok()),
392 attributes: event_attrs,
393 }
394 })
395 .collect();
396
397 Ok(OtelSpan {
398 name: otlp_span.name.clone(),
399 service_name,
400 duration_ns,
401 status,
402 attributes,
403 events,
404 trace_id: otlp_span.trace_id.clone(),
405 span_id: otlp_span.span_id.clone(),
406 parent_span_id: otlp_span.parent_span_id.clone(),
407 })
408}
409
410fn output_json(spans: &[&OtelSpan], show_attrs: bool, show_events: bool) -> Result<()> {
412 let output: Vec<serde_json::Value> = spans
413 .iter()
414 .map(|span| {
415 let mut obj = serde_json::Map::new();
416 obj.insert("name".to_string(), serde_json::json!(span.name));
417 if let Some(ref service) = span.service_name {
418 obj.insert("service".to_string(), serde_json::json!(service));
419 }
420 if let Some(duration) = span.duration_ns {
421 obj.insert("duration_ns".to_string(), serde_json::json!(duration));
422 }
423 if let Some(ref status) = span.status {
424 obj.insert("status".to_string(), serde_json::json!(status));
425 }
426 if show_attrs && !span.attributes.is_empty() {
427 obj.insert(
428 "attributes".to_string(),
429 serde_json::Value::Object(span.attributes.clone()),
430 );
431 }
432 if show_events && !span.events.is_empty() {
433 obj.insert("events".to_string(), serde_json::json!(span.events));
434 }
435 serde_json::Value::Object(obj)
436 })
437 .collect();
438
439 let json = serde_json::to_string_pretty(&output).map_err(|e| {
440 CleanroomError::internal_error(format!("Failed to serialize JSON output: {}", e))
441 })?;
442
443 println!("{}", json);
444 Ok(())
445}
446
447fn output_table(spans: &[&OtelSpan], show_attrs: bool, show_events: bool) -> Result<()> {
449 if spans.is_empty() {
450 println!("No spans found matching filter criteria.");
451 return Ok(());
452 }
453
454 println!(
456 "{:<40} {:<20} {:<12} {:<10}",
457 "SPAN NAME", "SERVICE", "DURATION", "STATUS"
458 );
459 println!("{}", "-".repeat(84));
460
461 for span in spans {
463 let service = span
464 .service_name
465 .as_deref()
466 .unwrap_or("unknown")
467 .to_string();
468 let duration = format_duration(span.duration_ns);
469 let status = format_status(span.status.as_ref());
470
471 println!(
472 "{:<40} {:<20} {:<12} {:<10}",
473 truncate(&span.name, 40),
474 truncate(&service, 20),
475 duration,
476 status
477 );
478
479 if show_attrs && !span.attributes.is_empty() {
481 println!(" Attributes:");
482 for (key, value) in &span.attributes {
483 println!(" {} = {}", key, value);
484 }
485 }
486
487 if show_events && !span.events.is_empty() {
489 println!(" Events:");
490 for event in &span.events {
491 println!(" - {}", event.name);
492 }
493 }
494 }
495
496 println!("\nTotal spans: {}", spans.len());
497 Ok(())
498}
499
500fn format_duration(duration_ns: Option<u64>) -> String {
502 match duration_ns {
503 Some(ns) => {
504 if ns < 1_000 {
505 format!("{}ns", ns)
506 } else if ns < 1_000_000 {
507 format!("{:.1}μs", ns as f64 / 1_000.0)
508 } else if ns < 1_000_000_000 {
509 format!("{:.1}ms", ns as f64 / 1_000_000.0)
510 } else {
511 format!("{:.2}s", ns as f64 / 1_000_000_000.0)
512 }
513 }
514 None => "N/A".to_string(),
515 }
516}
517
518fn format_status(status: Option<&SpanStatus>) -> String {
520 match status {
521 Some(SpanStatus::Ok) => "ok".to_string(),
522 Some(SpanStatus::Error) => "error".to_string(),
523 Some(SpanStatus::Unset) => "unset".to_string(),
524 None => "unknown".to_string(),
525 }
526}
527
528fn truncate(s: &str, max_len: usize) -> String {
530 if s.len() <= max_len {
531 s.to_string()
532 } else {
533 format!("{}...", &s[..max_len - 3])
534 }
535}