Skip to main content

agentforge_observability/
lib.rs

1pub mod datadog;
2pub mod langsmith;
3pub mod otlp;
4
5use agentforge_core::Trace;
6use async_trait::async_trait;
7use thiserror::Error;
8
9/// Error type for trace exporters.
10#[derive(Debug, Error)]
11pub enum ExporterError {
12    #[error("HTTP error exporting trace: {0}")]
13    Http(#[from] reqwest::Error),
14    #[error("Serialization error: {0}")]
15    Serialization(#[from] serde_json::Error),
16    #[error("Configuration error: {0}")]
17    Config(String),
18    #[error("Export error: {0}")]
19    Export(String),
20}
21
22/// Trait implemented by all observability backends.
23#[async_trait]
24pub trait TraceExporter: Send + Sync {
25    /// Export a single trace to the backend.
26    async fn export(&self, trace: &Trace) -> Result<(), ExporterError>;
27
28    /// Export a batch of traces (default: loop over `export`).
29    async fn export_batch(&self, traces: &[Trace]) -> Vec<Result<(), ExporterError>> {
30        let mut results = Vec::with_capacity(traces.len());
31        for trace in traces {
32            results.push(self.export(trace).await);
33        }
34        results
35    }
36}
37
38/// The active observability backend, selected at runtime by env vars.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum ObservabilityBackend {
41    Otlp,
42    LangSmith,
43    Datadog,
44    /// No-op exporter (observability disabled).
45    Disabled,
46}
47
48impl ObservabilityBackend {
49    /// Read from `AGENTFORGE_OBSERVABILITY_BACKEND` env var.
50    pub fn from_env() -> Self {
51        match std::env::var("AGENTFORGE_OBSERVABILITY_BACKEND")
52            .unwrap_or_default()
53            .to_lowercase()
54            .as_str()
55        {
56            "otlp" => ObservabilityBackend::Otlp,
57            "langsmith" => ObservabilityBackend::LangSmith,
58            "datadog" => ObservabilityBackend::Datadog,
59            _ => ObservabilityBackend::Disabled,
60        }
61    }
62}
63
64/// A no-op exporter used when observability is disabled.
65pub struct NoopExporter;
66
67#[async_trait]
68impl TraceExporter for NoopExporter {
69    async fn export(&self, _trace: &Trace) -> Result<(), ExporterError> {
70        Ok(())
71    }
72}
73
74/// Build a `TraceExporter` based on environment configuration.
75pub fn build_exporter() -> Box<dyn TraceExporter> {
76    match ObservabilityBackend::from_env() {
77        ObservabilityBackend::Otlp => {
78            tracing::info!("Observability: OTLP exporter enabled");
79            Box::new(otlp::OtlpExporter::from_env())
80        }
81        ObservabilityBackend::LangSmith => {
82            tracing::info!("Observability: LangSmith exporter enabled");
83            Box::new(langsmith::LangSmithExporter::from_env())
84        }
85        ObservabilityBackend::Datadog => {
86            tracing::info!("Observability: Datadog exporter enabled");
87            Box::new(datadog::DatadogExporter::from_env())
88        }
89        ObservabilityBackend::Disabled => {
90            tracing::debug!(
91                "Observability: disabled (set AGENTFORGE_OBSERVABILITY_BACKEND to enable)"
92            );
93            Box::new(NoopExporter)
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[tokio::test]
103    async fn noop_exporter_succeeds() {
104        use agentforge_core::{FailureCluster, TraceStatus};
105        use chrono::Utc;
106        use uuid::Uuid;
107
108        let trace = Trace {
109            id: Uuid::new_v4(),
110            run_id: Uuid::new_v4(),
111            scenario_id: Uuid::new_v4(),
112            status: TraceStatus::Pass,
113            steps: vec![],
114            final_output: None,
115            scores: None,
116            aggregate_score: Some(1.0),
117            failure_cluster: FailureCluster::NoFailure,
118            failure_reason: None,
119            review_needed: false,
120            llm_calls: 1,
121            tool_invocations: 0,
122            input_tokens: 100,
123            output_tokens: 50,
124            latency_ms: 200,
125            retry_count: 0,
126            seed: 42,
127            created_at: Utc::now(),
128        };
129
130        let exporter = NoopExporter;
131        assert!(exporter.export(&trace).await.is_ok());
132    }
133}