agentforge_observability/
lib.rs1pub mod datadog;
2pub mod langsmith;
3pub mod otlp;
4
5use agentforge_core::Trace;
6use async_trait::async_trait;
7use thiserror::Error;
8
9#[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#[async_trait]
24pub trait TraceExporter: Send + Sync {
25 async fn export(&self, trace: &Trace) -> Result<(), ExporterError>;
27
28 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#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum ObservabilityBackend {
41 Otlp,
42 LangSmith,
43 Datadog,
44 Disabled,
46}
47
48impl ObservabilityBackend {
49 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
64pub struct NoopExporter;
66
67#[async_trait]
68impl TraceExporter for NoopExporter {
69 async fn export(&self, _trace: &Trace) -> Result<(), ExporterError> {
70 Ok(())
71 }
72}
73
74pub 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}