use std::sync::Mutex;
use opentelemetry::{
KeyValue,
trace::{Span, SpanKind, Status, Tracer as OtelTracer},
};
use opentelemetry_sdk::trace::SdkTracerProvider;
use serde_json::Value;
use super::tracer::{TracerBackend, TracerFactory};
struct OtelSpanBackend {
span: Mutex<opentelemetry::global::BoxedSpan>,
}
impl TracerBackend for OtelSpanBackend {
fn emit(&self, key: &str, value: &Value) {
let mut span = self.span.lock().unwrap();
if key == "__end__" {
span.end();
return;
}
if key == "error" {
let msg = value_to_string(value);
span.set_status(Status::error(msg.clone()));
span.record_error(&OtelError(msg));
return;
}
flatten_to_attributes(&mut *span, key, value);
}
}
#[derive(Debug)]
struct OtelError(String);
impl std::fmt::Display for OtelError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for OtelError {}
fn flatten_to_attributes(span: &mut opentelemetry::global::BoxedSpan, key: &str, value: &Value) {
match value {
Value::String(s) => {
span.set_attribute(KeyValue::new(key.to_string(), s.clone()));
}
Value::Number(n) => {
if let Some(i) = n.as_i64() {
span.set_attribute(KeyValue::new(key.to_string(), i));
} else if let Some(f) = n.as_f64() {
span.set_attribute(KeyValue::new(key.to_string(), f));
}
}
Value::Bool(b) => {
span.set_attribute(KeyValue::new(key.to_string(), *b));
}
Value::Null => {
span.set_attribute(KeyValue::new(key.to_string(), "null".to_string()));
}
Value::Object(map) => {
for (k, v) in map {
let nested_key = format!("{key}.{k}");
flatten_to_attributes(span, &nested_key, v);
}
}
Value::Array(_) => {
span.set_attribute(KeyValue::new(key.to_string(), value.to_string()));
}
}
}
fn value_to_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
_ => value.to_string(),
}
}
struct OtelTracerFactory;
impl TracerFactory for OtelTracerFactory {
fn create(&self, signature: &str) -> Option<Box<dyn TracerBackend>> {
let tracer = opentelemetry::global::tracer("prompty");
let span = tracer
.span_builder(signature.to_string())
.with_kind(SpanKind::Internal)
.start(&tracer);
Some(Box::new(OtelSpanBackend {
span: Mutex::new(span),
}))
}
}
pub fn otel_tracer() -> impl TracerFactory {
OtelTracerFactory
}
pub fn init_otel_stdout() -> impl TracerFactory {
let provider = SdkTracerProvider::builder()
.with_simple_exporter(opentelemetry_stdout::SpanExporter::default())
.build();
opentelemetry::global::set_tracer_provider(provider);
OtelTracerFactory
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tracing::tracer::Tracer;
use serde_json::json;
fn setup_noop_provider() {
let provider = SdkTracerProvider::builder().build();
opentelemetry::global::set_tracer_provider(provider);
}
#[test]
fn test_otel_factory_creates_backend() {
setup_noop_provider();
let factory = OtelTracerFactory;
let backend = factory.create("test.span");
assert!(backend.is_some());
let backend = backend.unwrap();
backend.emit("key", &json!("value"));
backend.emit("number", &json!(42));
backend.emit("nested", &json!({"a": 1, "b": "two"}));
backend.emit("error", &json!("something went wrong"));
backend.emit("__end__", &Value::Null);
}
#[test]
fn test_otel_tracer_registration() {
setup_noop_provider();
Tracer::clear();
Tracer::add("otel", otel_tracer());
let span = Tracer::start("test.otel.span");
span.emit("greeting", &json!("hello"));
span.end();
Tracer::clear();
}
#[test]
fn test_flatten_attributes() {
setup_noop_provider();
let factory = OtelTracerFactory;
let backend = factory.create("flatten.test").unwrap();
backend.emit("string_val", &json!("hello"));
backend.emit("int_val", &json!(42));
backend.emit("float_val", &json!(3.14));
backend.emit("bool_val", &json!(true));
backend.emit("null_val", &Value::Null);
backend.emit("array_val", &json!([1, 2, 3]));
backend.emit("object_val", &json!({"key": "value"}));
backend.emit("__end__", &Value::Null);
}
}