#[derive(Debug, Clone)]
pub struct SpanEvent {
pub name: String,
pub timestamp: SystemTime,
pub attributes: HashMap<String, AttributeValue>,
}
impl SpanEvent {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
timestamp: SystemTime::now(),
attributes: HashMap::new(),
}
}
pub fn with_attributes(name: &str, attributes: HashMap<String, AttributeValue>) -> Self {
Self {
name: name.to_string(),
timestamp: SystemTime::now(),
attributes,
}
}
pub fn to_json(&self) -> String {
let mut json = format!(
"{{\"name\": \"{}\", \"timeUnixNano\": {}",
escape_json(&self.name),
time_to_nanos(self.timestamp)
);
if !self.attributes.is_empty() {
json.push_str(", \"attributes\": {");
let attrs: Vec<String> = self
.attributes
.iter()
.map(|(k, v)| format!("\"{}\": {}", escape_json(k), v.to_json()))
.collect();
json.push_str(&attrs.join(", "));
json.push('}');
}
json.push('}');
json
}
}
#[derive(Debug, Clone, Default)]
pub enum TraceExporter {
#[default]
Stdout,
File(PathBuf),
Otlp(String),
Memory,
}
#[derive(Debug)]
pub struct TracingContext {
trace_id: String,
root_span: Option<Span>,
span_stack: Vec<Span>,
completed_spans: Vec<Span>,
level: TraceLevel,
exporter: TraceExporter,
service_name: String,
service_version: String,
}
impl TracingContext {
pub fn new(service_name: &str, service_version: &str) -> Self {
Self {
trace_id: generate_trace_id(),
root_span: None,
span_stack: Vec::new(),
completed_spans: Vec::new(),
level: TraceLevel::Info,
exporter: TraceExporter::default(),
service_name: service_name.to_string(),
service_version: service_version.to_string(),
}
}
pub fn with_level(mut self, level: TraceLevel) -> Self {
self.level = level;
self
}
pub fn with_exporter(mut self, exporter: TraceExporter) -> Self {
self.exporter = exporter;
self
}
pub fn trace_id(&self) -> &str {
&self.trace_id
}
pub fn service_name(&self) -> &str {
&self.service_name
}
#[allow(clippy::expect_used)] pub fn start_root(&mut self, name: &str) -> &Span {
let mut span = Span::new(name, &self.trace_id);
span.set_string("service.name", &self.service_name);
span.set_string("service.version", &self.service_version);
self.root_span = Some(span);
self.root_span.as_ref().expect("just set")
}
pub fn start_span(&mut self, name: &str) -> String {
let parent_id = self.current_span_id();
let mut span = Span::new(name, &self.trace_id);
if let Some(pid) = parent_id {
span.parent_id = Some(pid);
}
let span_id = span.span_id.clone();
self.span_stack.push(span);
span_id
}
pub fn start_step_span(&mut self, step_id: &str, step_name: &str) -> String {
let span_id = self.start_span(&format!("step:{}", step_id));
if let Some(span) = self.span_stack.last_mut() {
span.set_string("step.id", step_id);
span.set_string("step.name", step_name);
}
span_id
}
pub fn start_artifact_span(&mut self, artifact_id: &str) -> String {
let span_id = self.start_span(&format!("artifact:{}", artifact_id));
if let Some(span) = self.span_stack.last_mut() {
span.set_string("artifact.id", artifact_id);
span.kind = SpanKind::Client;
}
span_id
}
pub fn start_precondition_span(&mut self, condition: &str) -> String {
let span_id = self.start_span("precondition");
if let Some(span) = self.span_stack.last_mut() {
span.set_string("condition", condition);
}
span_id
}
pub fn start_postcondition_span(&mut self, condition: &str) -> String {
let span_id = self.start_span("postcondition");
if let Some(span) = self.span_stack.last_mut() {
span.set_string("condition", condition);
}
span_id
}
pub fn current_span_id(&self) -> Option<String> {
self.span_stack
.last()
.map(|s| s.span_id.clone())
.or_else(|| self.root_span.as_ref().map(|s| s.span_id.clone()))
}
pub fn set_attribute(&mut self, key: &str, value: AttributeValue) {
if let Some(span) = self.span_stack.last_mut() {
span.set_attribute(key, value);
} else if let Some(span) = self.root_span.as_mut() {
span.set_attribute(key, value);
}
}
pub fn add_event(&mut self, name: &str) {
if let Some(span) = self.span_stack.last_mut() {
span.add_event(name);
} else if let Some(span) = self.root_span.as_mut() {
span.add_event(name);
}
}
pub fn end_span_ok(&mut self) {
if let Some(mut span) = self.span_stack.pop() {
span.end_ok();
self.completed_spans.push(span);
}
}
pub fn end_span_error(&mut self, message: &str) {
if let Some(mut span) = self.span_stack.pop() {
span.end_error(message);
self.completed_spans.push(span);
}
}
pub fn end_root_ok(&mut self) {
if let Some(ref mut span) = self.root_span {
span.end_ok();
}
}
pub fn end_root_error(&mut self, message: &str) {
if let Some(ref mut span) = self.root_span {
span.end_error(message);
}
}
pub fn completed_count(&self) -> usize {
self.completed_spans.len()
}
pub fn all_spans(&self) -> Vec<&Span> {
let mut spans: Vec<&Span> = self.completed_spans.iter().collect();
if let Some(ref root) = self.root_span {
spans.push(root);
}
spans
}
pub fn export(&self) -> String {
let spans = self.all_spans();
let mut json = String::from("{\n");
json.push_str(" \"resourceSpans\": [{\n");
json.push_str(" \"resource\": {\n");
json.push_str(" \"attributes\": {\n");
json.push_str(&format!(
" \"service.name\": \"{}\",\n",
escape_json(&self.service_name)
));
json.push_str(&format!(
" \"service.version\": \"{}\"\n",
escape_json(&self.service_version)
));
json.push_str(" }\n");
json.push_str(" },\n");
json.push_str(" \"scopeSpans\": [{\n");
json.push_str(" \"scope\": {\n");
json.push_str(" \"name\": \"bashrs-installer\",\n");
json.push_str(" \"version\": \"2.0.0\"\n");
json.push_str(" },\n");
json.push_str(" \"spans\": [\n");
let span_jsons: Vec<String> = spans
.iter()
.map(|s| format!(" {}", s.to_json().replace('\n', "\n ")))
.collect();
json.push_str(&span_jsons.join(",\n"));
json.push_str("\n ]\n");
json.push_str(" }]\n");
json.push_str(" }]\n");
json.push_str("}\n");
json
}
pub fn summary(&self) -> TraceSummary {
let spans = self.all_spans();
let total = spans.len();
let ok_count = spans.iter().filter(|s| s.status == SpanStatus::Ok).count();
let error_count = spans
.iter()
.filter(|s| s.status == SpanStatus::Error)
.count();
let total_duration = self
.root_span
.as_ref()
.and_then(|s| s.duration())
.unwrap_or(Duration::ZERO);
TraceSummary {
trace_id: self.trace_id.clone(),
total_spans: total,
ok_spans: ok_count,
error_spans: error_count,
total_duration,
}
}
}
#[derive(Debug, Clone)]
pub struct TraceSummary {
pub trace_id: String,
pub total_spans: usize,
pub ok_spans: usize,
pub error_spans: usize,
pub total_duration: Duration,
}
impl TraceSummary {
pub fn format(&self) -> String {
let duration = if self.total_duration.as_secs() >= 60 {
format!(
"{}m {:02}s",
self.total_duration.as_secs() / 60,
self.total_duration.as_secs() % 60
)
} else {
format!("{:.2}s", self.total_duration.as_secs_f64())
};
format!(
"Trace Summary\n\
─────────────────────────────────\n\
Trace ID: {}\n\
Spans: {} total, {} ok, {} error\n\
Duration: {}\n",
truncate(&self.trace_id, 16),
self.total_spans,
self.ok_spans,
self.error_spans,
duration
)
}
}
include!("tracing_logger.rs");