Skip to main content

agentforge_observability/
otlp.rs

1use agentforge_core::Trace;
2use async_trait::async_trait;
3use opentelemetry::{
4    global,
5    trace::{Span, SpanKind, Tracer},
6    Context, KeyValue,
7};
8
9use crate::{ExporterError, TraceExporter};
10
11/// Exports AgentForge traces as OpenTelemetry spans to any OTLP-compatible backend
12/// (Jaeger, Grafana Tempo, Honeycomb, etc.).
13///
14/// Env vars:
15/// - `AGENTFORGE_OTEL_ENDPOINT` — OTLP HTTP endpoint (default: `http://localhost:4318`)
16/// - `AGENTFORGE_OTEL_SERVICE_NAME` — service name (default: `agentforge`)
17pub struct OtlpExporter {
18    endpoint: String,
19    service_name: String,
20}
21
22impl OtlpExporter {
23    pub fn new(endpoint: String, service_name: String) -> Self {
24        Self {
25            endpoint,
26            service_name,
27        }
28    }
29
30    pub fn from_env() -> Self {
31        Self::new(
32            std::env::var("AGENTFORGE_OTEL_ENDPOINT")
33                .unwrap_or_else(|_| "http://localhost:4318".to_string()),
34            std::env::var("AGENTFORGE_OTEL_SERVICE_NAME")
35                .unwrap_or_else(|_| "agentforge".to_string()),
36        )
37    }
38}
39
40#[async_trait]
41impl TraceExporter for OtlpExporter {
42    async fn export(&self, trace: &Trace) -> Result<(), ExporterError> {
43        let tracer = global::tracer(self.service_name.clone());
44        let cx = Context::new();
45
46        let mut span = tracer
47            .span_builder(format!("agentforge.trace.{}", trace.id))
48            .with_kind(SpanKind::Internal)
49            .start_with_context(&tracer, &cx);
50
51        span.set_attribute(KeyValue::new("trace.id", trace.id.to_string()));
52        span.set_attribute(KeyValue::new("run.id", trace.run_id.to_string()));
53        span.set_attribute(KeyValue::new("scenario.id", trace.scenario_id.to_string()));
54        span.set_attribute(KeyValue::new("status", trace.status.to_string()));
55        span.set_attribute(KeyValue::new("latency_ms", trace.latency_ms as i64));
56        span.set_attribute(KeyValue::new("llm_calls", trace.llm_calls as i64));
57        span.set_attribute(KeyValue::new("input_tokens", trace.input_tokens as i64));
58        span.set_attribute(KeyValue::new("output_tokens", trace.output_tokens as i64));
59
60        if let Some(score) = trace.aggregate_score {
61            span.set_attribute(KeyValue::new("aggregate_score", score.to_string()));
62        }
63
64        // Add step count attribute
65        let step_count = trace.steps.len();
66        span.set_attribute(KeyValue::new("step_count", step_count as i64));
67
68        span.end();
69
70        tracing::debug!(
71            trace_id = %trace.id,
72            endpoint = %self.endpoint,
73            "Exported trace to OTLP"
74        );
75
76        Ok(())
77    }
78}