use crate::BoxFuture;
use crate::observability::ObservabilitySpanKind;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SpanOutcome {
Success,
Error(String),
}
pub trait TracingBridge: Send + Sync {
fn begin_span(
&self,
name: &str,
kind: ObservabilitySpanKind,
run_id: Uuid,
attributes: &HashMap<String, Value>,
) -> BoxFuture<'_, Uuid>;
fn end_span(
&self,
span_id: Uuid,
outcome: SpanOutcome,
attributes: &HashMap<String, Value>,
) -> BoxFuture<'_, ()>;
}
pub struct SpanGuard<B: TracingBridge + 'static> {
bridge: std::sync::Arc<B>,
span_id: Uuid,
completed: std::sync::atomic::AtomicBool,
}
impl<B: TracingBridge + 'static> SpanGuard<B> {
pub const fn new(bridge: std::sync::Arc<B>, span_id: Uuid) -> Self {
Self {
bridge,
span_id,
completed: std::sync::atomic::AtomicBool::new(false),
}
}
pub const fn span_id(&self) -> Uuid {
self.span_id
}
pub async fn complete(self, outcome: SpanOutcome, attributes: &HashMap<String, Value>) {
self.completed
.store(true, std::sync::atomic::Ordering::Release);
self.bridge
.end_span(self.span_id, outcome, attributes)
.await;
}
}
impl<B: TracingBridge + 'static> Drop for SpanGuard<B> {
fn drop(&mut self) {
if !self.completed.load(std::sync::atomic::Ordering::Acquire) {
let bridge = self.bridge.clone();
let span_id = self.span_id;
let _ = std::thread::Builder::new()
.name("span-guard-drop".into())
.spawn(move || {
if let Ok(rt) = tokio::runtime::Handle::try_current() {
rt.block_on(bridge.end_span(
span_id,
SpanOutcome::Error("span dropped without completion".into()),
&HashMap::new(),
));
}
});
}
}
}
pub struct OTelTracingBridge {
_private: (),
}
impl std::fmt::Debug for OTelTracingBridge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OTelTracingBridge").finish()
}
}
impl Default for OTelTracingBridge {
fn default() -> Self {
Self::new()
}
}
impl OTelTracingBridge {
pub const fn new() -> Self {
Self { _private: () }
}
}
impl TracingBridge for OTelTracingBridge {
fn begin_span(
&self,
name: &str,
kind: ObservabilitySpanKind,
_run_id: Uuid,
_attributes: &HashMap<String, Value>,
) -> BoxFuture<'_, Uuid> {
let span_id = Uuid::new_v4();
tracing::info_span!("synwire.span", otel.name = %name, synwire.span_kind = %kind);
Box::pin(async move { span_id })
}
fn end_span(
&self,
_span_id: Uuid,
outcome: SpanOutcome,
_attributes: &HashMap<String, Value>,
) -> BoxFuture<'_, ()> {
Box::pin(async move {
match outcome {
SpanOutcome::Success => {
tracing::debug!("span completed successfully");
}
SpanOutcome::Error(ref msg) => {
tracing::warn!(error = %msg, "span completed with error");
}
}
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::sync::Arc;
#[tokio::test]
async fn begin_and_end_span() {
let bridge = OTelTracingBridge::new();
let span_id = bridge
.begin_span(
"test-span",
ObservabilitySpanKind::Llm,
Uuid::new_v4(),
&HashMap::new(),
)
.await;
bridge
.end_span(span_id, SpanOutcome::Success, &HashMap::new())
.await;
}
#[tokio::test]
async fn span_guard_completes() {
let bridge = Arc::new(OTelTracingBridge::new());
let span_id = bridge
.begin_span(
"guarded",
ObservabilitySpanKind::Tool,
Uuid::new_v4(),
&HashMap::new(),
)
.await;
let guard = SpanGuard::new(Arc::clone(&bridge), span_id);
assert_eq!(guard.span_id(), span_id);
guard.complete(SpanOutcome::Success, &HashMap::new()).await;
}
#[test]
fn span_outcome_serialization() {
let success = SpanOutcome::Success;
let json = serde_json::to_string(&success).unwrap();
assert!(json.contains("Success"));
let error = SpanOutcome::Error("timeout".into());
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("timeout"));
}
}