use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::WyrdError;
use crate::ids::DataTenantId;
use crate::vala::ids::{SpanId, TraceId};
use crate::vala::trace::{InstrumentationScope, Resource, SpanKind, SpanStatus};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TraceSummaryRecord {
pub trace_id: TraceId,
pub data_tenant_id: DataTenantId,
pub bucket_time: DateTime<Utc>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub duration_ms: u64,
pub span_count: u32,
pub error_count: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_span_id: Option<SpanId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_kind: Option<SpanKind>,
pub root_status: SpanStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_scope: Option<InstrumentationScope>,
pub resource: Resource,
}
impl TraceSummaryRecord {
pub fn validate(&self) -> Result<(), WyrdError> {
if self.span_count == 0 {
return Err(WyrdError::Validation {
message:
"trace_summary.span_count must be >= 1 (zero-span trace is not summarizable)"
.to_string(),
details: serde_json::Value::Null,
});
}
if self.error_count > self.span_count {
return Err(WyrdError::Validation {
message: format!(
"trace_summary.error_count {} > span_count {}",
self.error_count, self.span_count
),
details: serde_json::Value::Null,
});
}
if self.end_time < self.start_time {
return Err(WyrdError::Validation {
message: format!(
"trace_summary.end_time {:?} < start_time {:?}",
self.end_time, self.start_time
),
details: serde_json::Value::Null,
});
}
let derived = super::duration_ms_from_timestamps(self.start_time, self.end_time);
if self.duration_ms != derived {
return Err(WyrdError::Validation {
message: format!(
"trace_summary.duration_ms {} != end_time - start_time = {}",
self.duration_ms, derived
),
details: serde_json::Value::Null,
});
}
if self.bucket_time > self.start_time {
return Err(WyrdError::Validation {
message: format!(
"trace_summary.bucket_time {:?} > start_time {:?} (bucket must be a floor)",
self.bucket_time, self.start_time
),
details: serde_json::Value::Null,
});
}
self.resource.validate()?;
if let Some(scope) = &self.root_scope {
scope.validate()?;
}
if let Some(name) = &self.root_name {
if name.is_empty() {
return Err(WyrdError::Validation {
message: "trace_summary.root_name must be non-empty when present".to_string(),
details: serde_json::Value::Null,
});
}
if name.len() > 256 {
return Err(WyrdError::Validation {
message: format!(
"trace_summary.root_name length must be <= 256, got {}",
name.len()
),
details: serde_json::Value::Null,
});
}
}
Ok(())
}
}