gemini_adk_rs/telemetry/setup.rs
1//! OTLP exporter setup helpers for agent-level telemetry.
2//!
3//! Provides a convenience [`TelemetrySetup`] builder that configures tracing-subscriber
4//! with optional OpenTelemetry exporters. This is a higher-level wrapper around the
5//! L0 [`gemini_genai_rs::telemetry::TelemetryConfig`] tailored for agent applications.
6//!
7//! # Feature flags
8//!
9//! - `tracing-support`: Enables tracing-subscriber with env-filter and fmt layer.
10//! - `otel-otlp`: Adds OTLP trace export via `opentelemetry-otlp`.
11//! - `otel-gcp`: Adds Google Cloud Trace export via `opentelemetry-gcloud-trace`.
12//!
13//! Without any of these features, [`TelemetrySetup::init`] is a no-op that returns `Ok(())`.
14
15/// Configuration for telemetry export.
16///
17/// Use the builder methods to configure the desired exporters, then call [`init`](TelemetrySetup::init)
18/// to set up the global tracing subscriber.
19///
20/// # Examples
21///
22/// ```rust,no_run
23/// use gemini_adk_rs::telemetry::setup::TelemetrySetup;
24///
25/// // Basic setup with console logging only (requires `tracing-support` feature)
26/// TelemetrySetup::new("my-agent-service").init().unwrap();
27///
28/// // With OTLP export (requires `otel-otlp` feature)
29/// TelemetrySetup::new("my-agent-service")
30/// .with_otlp("http://localhost:4317")
31/// .with_content_capture(true)
32/// .init()
33/// .unwrap();
34///
35/// // With Google Cloud Trace (requires `otel-gcp` feature)
36/// TelemetrySetup::new("my-agent-service")
37/// .with_cloud_trace()
38/// .init()
39/// .unwrap();
40/// ```
41#[derive(Debug, Clone)]
42pub struct TelemetrySetup {
43 /// Service name for OTel resource identification.
44 pub service_name: String,
45 /// OTLP gRPC endpoint (e.g., `http://localhost:4317`).
46 /// When set, enables OTLP trace export (requires `otel-otlp` feature).
47 pub otlp_endpoint: Option<String>,
48 /// Enable Google Cloud Trace export (requires `otel-gcp` feature).
49 pub cloud_trace: bool,
50 /// Whether to capture prompt/completion content in spans.
51 /// Defaults to `false` to avoid logging sensitive data.
52 pub capture_content: bool,
53}
54
55impl TelemetrySetup {
56 /// Create a new telemetry setup with the given service name.
57 ///
58 /// Defaults to console logging only, no OTLP or Cloud Trace export,
59 /// and content capture disabled.
60 pub fn new(service_name: impl Into<String>) -> Self {
61 Self {
62 service_name: service_name.into(),
63 otlp_endpoint: None,
64 cloud_trace: false,
65 capture_content: false,
66 }
67 }
68
69 /// Set the OTLP gRPC endpoint for trace export.
70 ///
71 /// Requires the `otel-otlp` feature. If the feature is not enabled,
72 /// this value is ignored during [`init`](TelemetrySetup::init).
73 pub fn with_otlp(mut self, endpoint: impl Into<String>) -> Self {
74 self.otlp_endpoint = Some(endpoint.into());
75 self
76 }
77
78 /// Enable Google Cloud Trace export.
79 ///
80 /// Requires the `otel-gcp` feature. If the feature is not enabled,
81 /// this value is ignored during [`init`](TelemetrySetup::init).
82 pub fn with_cloud_trace(mut self) -> Self {
83 self.cloud_trace = true;
84 self
85 }
86
87 /// Set whether to capture prompt/completion content in trace spans.
88 ///
89 /// Defaults to `false`. When enabled, LLM request and response content
90 /// may be recorded in span attributes, which is useful for debugging
91 /// but should be disabled in production to avoid logging sensitive data.
92 pub fn with_content_capture(mut self, capture: bool) -> Self {
93 self.capture_content = capture;
94 self
95 }
96
97 /// Initialize the tracing subscriber with the configured exporters.
98 ///
99 /// This is a convenience function that sets up:
100 /// - `tracing-subscriber` with `EnvFilter` (reads `RUST_LOG` env var, defaults to `info`)
101 /// - OpenTelemetry tracer (if `otlp_endpoint` is set and `otel-otlp` feature is enabled)
102 /// - Google Cloud Trace (if `cloud_trace` is set and `otel-gcp` feature is enabled)
103 /// - Pretty log format for development
104 ///
105 /// # Feature behavior
106 ///
107 /// | Features enabled | Behavior |
108 /// |-----------------|----------|
109 /// | (none) | No-op, returns `Ok(())` |
110 /// | `tracing-support` | Console logging with env-filter |
111 /// | `tracing-support` + `otel-otlp` | Console + OTLP trace export |
112 /// | `tracing-support` + `otel-gcp` | Console + Cloud Trace export |
113 ///
114 /// # Errors
115 ///
116 /// Returns an error if the tracing subscriber cannot be set (e.g., if one
117 /// is already registered globally), or if OTel exporter initialization fails.
118 pub fn init(self) -> Result<(), Box<dyn std::error::Error>> {
119 #[cfg(feature = "tracing-support")]
120 {
121 let config = gemini_genai_rs::telemetry::TelemetryConfig {
122 logging_enabled: true,
123 log_filter: std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
124 json_logs: false,
125 metrics_enabled: false,
126 metrics_addr: None,
127 otel_traces: self.otlp_endpoint.is_some() || self.cloud_trace,
128 otel_metrics: false,
129 otel_service_name: self.service_name,
130 otel_gcp_project: None,
131 };
132
133 // Set OTLP endpoint env var if provided (the OTLP exporter reads it).
134 #[cfg(feature = "otel-otlp")]
135 if let Some(ref endpoint) = self.otlp_endpoint {
136 std::env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint);
137 }
138
139 let _guard = config.init()?;
140 // Note: The guard is intentionally leaked here so the providers stay alive
141 // for the process lifetime. For finer control, use TelemetryConfig::init()
142 // directly and hold the guard.
143 std::mem::forget(_guard);
144 }
145
146 #[cfg(not(feature = "tracing-support"))]
147 {
148 let _ = self; // suppress unused warning
149 }
150
151 Ok(())
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn telemetry_setup_defaults() {
161 let setup = TelemetrySetup::new("test-service");
162 assert_eq!(setup.service_name, "test-service");
163 assert!(setup.otlp_endpoint.is_none());
164 assert!(!setup.cloud_trace);
165 assert!(!setup.capture_content);
166 }
167
168 #[test]
169 fn telemetry_setup_builder_chain() {
170 let setup = TelemetrySetup::new("my-service")
171 .with_otlp("http://localhost:4317")
172 .with_cloud_trace()
173 .with_content_capture(true);
174
175 assert_eq!(setup.service_name, "my-service");
176 assert_eq!(
177 setup.otlp_endpoint.as_deref(),
178 Some("http://localhost:4317")
179 );
180 assert!(setup.cloud_trace);
181 assert!(setup.capture_content);
182 }
183
184 #[test]
185 fn telemetry_setup_clone() {
186 let setup = TelemetrySetup::new("svc").with_otlp("http://otel:4317");
187 let cloned = setup.clone();
188 assert_eq!(cloned.service_name, "svc");
189 assert_eq!(cloned.otlp_endpoint.as_deref(), Some("http://otel:4317"));
190 }
191}