gemini_genai_rs/telemetry/
mod.rs1pub mod logging;
11pub mod metrics;
12pub mod spans;
13
14#[cfg(feature = "metrics")]
16pub use metrics_exporter_prometheus as metrics_exporter;
17
18#[derive(Debug, Clone)]
20pub struct TelemetryConfig {
21 pub logging_enabled: bool,
24 pub log_filter: String,
26 pub json_logs: bool,
28 pub metrics_enabled: bool,
30 pub metrics_addr: Option<String>,
32 pub otel_traces: bool,
34 pub otel_metrics: bool,
36 pub otel_service_name: String,
38 pub otel_endpoint: Option<String>,
43 pub otel_gcp_project: Option<String>,
46}
47
48impl Default for TelemetryConfig {
49 fn default() -> Self {
50 Self {
51 logging_enabled: true,
52 log_filter: "info".to_string(),
53 json_logs: false,
54 metrics_enabled: false,
55 metrics_addr: None,
56 otel_traces: false,
57 otel_metrics: false,
58 otel_service_name: "gemini-genai-rs".to_string(),
59 otel_endpoint: None,
60 otel_gcp_project: None,
61 }
62 }
63}
64
65#[derive(Default)]
68pub struct TelemetryGuard {
69 #[cfg(feature = "otel-base")]
70 _tracer_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
71 #[cfg(feature = "otel-base")]
72 _meter_provider: Option<opentelemetry_sdk::metrics::SdkMeterProvider>,
73 #[cfg(not(feature = "otel-base"))]
74 _private: (),
75}
76
77#[derive(Default)]
81pub struct Exporters {
82 pub guard: TelemetryGuard,
84 #[cfg(feature = "otel-base")]
85 tracer: Option<opentelemetry_sdk::trace::Tracer>,
86}
87
88impl Exporters {
89 #[cfg(feature = "otel-base")]
91 pub fn layer<S>(
92 &self,
93 ) -> Option<tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::Tracer>>
94 where
95 S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
96 {
97 self.tracer
98 .clone()
99 .map(|tracer| tracing_opentelemetry::layer().with_tracer(tracer))
100 }
101}
102
103impl TelemetryConfig {
104 pub fn from_env() -> Self {
116 let var = |name: &str| std::env::var(name).ok().filter(|v| !v.trim().is_empty());
117 let endpoint = var("OTEL_EXPORTER_OTLP_ENDPOINT");
118 let gcp = var("ADK_TELEMETRY").is_some_and(|v| v.eq_ignore_ascii_case("gcp"));
119 let metrics_addr = var("ADK_METRICS_ADDR");
120 Self {
121 log_filter: var("RUST_LOG").unwrap_or_else(|| "info".to_string()),
122 metrics_enabled: metrics_addr.is_some(),
123 metrics_addr,
124 otel_traces: endpoint.is_some() || gcp,
125 otel_metrics: endpoint.is_some() || gcp,
126 otel_service_name: var("OTEL_SERVICE_NAME")
127 .unwrap_or_else(|| "gemini-live".to_string()),
128 otel_endpoint: endpoint,
129 otel_gcp_project: if gcp {
130 var("GOOGLE_CLOUD_PROJECT")
131 } else {
132 None
133 },
134 ..Self::default()
135 }
136 }
137
138 pub fn wants_gcp(&self) -> bool {
141 (self.otel_traces || self.otel_metrics) && self.otel_endpoint.is_none()
142 }
143
144 #[cfg(feature = "metrics")]
149 pub fn install_metrics(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
150 let Some(addr) = self
151 .metrics_addr
152 .as_deref()
153 .filter(|_| self.metrics_enabled)
154 else {
155 return Ok(());
156 };
157 let addr: std::net::SocketAddr = addr.parse()?;
158 metrics_exporter_prometheus::PrometheusBuilder::new()
159 .with_http_listener(addr)
160 .install()?;
161 Ok(())
162 }
163
164 #[cfg(feature = "metrics")]
168 pub fn prometheus_recorder() -> Result<
169 metrics_exporter_prometheus::PrometheusHandle,
170 Box<dyn std::error::Error + Send + Sync>,
171 > {
172 Ok(metrics_exporter_prometheus::PrometheusBuilder::new().install_recorder()?)
173 }
174
175 #[cfg(feature = "otel-otlp")]
179 pub fn build_otlp(&self) -> Result<Exporters, Box<dyn std::error::Error + Send + Sync>> {
180 use opentelemetry_otlp::WithExportConfig as _;
181 let mut guard = TelemetryGuard::default();
182 let tracer = if self.otel_traces {
183 let mut builder = opentelemetry_otlp::SpanExporter::builder().with_tonic();
184 if let Some(endpoint) = &self.otel_endpoint {
185 builder = builder.with_endpoint(endpoint.clone());
186 }
187 let exporter = builder.build()?;
188 let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
189 .with_batch_exporter(exporter)
190 .with_resource(self.otel_resource())
191 .build();
192 let tracer = opentelemetry::trace::TracerProvider::tracer(
193 &provider,
194 self.otel_service_name.clone(),
195 );
196 guard._tracer_provider = Some(provider);
197 Some(tracer)
198 } else {
199 None
200 };
201 if self.otel_metrics {
202 let mut builder = opentelemetry_otlp::MetricExporter::builder().with_tonic();
203 if let Some(endpoint) = &self.otel_endpoint {
204 builder = builder.with_endpoint(endpoint.clone());
205 }
206 let exporter = builder.build()?;
207 let provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
208 .with_periodic_exporter(exporter)
209 .with_resource(self.otel_resource())
210 .build();
211 opentelemetry::global::set_meter_provider(provider.clone());
212 guard._meter_provider = Some(provider);
213 }
214 Ok(Exporters { guard, tracer })
215 }
216
217 pub fn init(&self) -> Result<TelemetryGuard, Box<dyn std::error::Error>> {
229 #[cfg(feature = "metrics")]
230 self.install_metrics()
231 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
232
233 #[cfg(feature = "otel-otlp")]
234 let Exporters { guard, tracer } = self
235 .build_otlp()
236 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
237 #[cfg(not(feature = "otel-otlp"))]
238 let guard = TelemetryGuard::default();
239
240 #[cfg(feature = "tracing-subscriber")]
241 if self.logging_enabled {
242 #[cfg(feature = "otel-otlp")]
243 self.init_tracing_subscriber_with_tracer(tracer)
244 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
245 #[cfg(not(feature = "otel-otlp"))]
246 self.init_tracing_subscriber()
247 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
248 }
249
250 Ok(guard)
251 }
252
253 #[cfg(feature = "otel-gcp")]
264 pub async fn init_gcp(
265 &self,
266 ) -> Result<TelemetryGuard, Box<dyn std::error::Error + Send + Sync>> {
267 #[cfg(feature = "metrics")]
268 self.install_metrics()?;
269 let Exporters { guard, tracer } = self.build_gcp().await?;
270 #[cfg(feature = "tracing-subscriber")]
271 if self.logging_enabled {
272 self.init_tracing_subscriber_with_tracer(tracer)?;
273 }
274 Ok(guard)
275 }
276
277 #[cfg(feature = "otel-gcp")]
281 pub async fn build_gcp(&self) -> Result<Exporters, Box<dyn std::error::Error + Send + Sync>> {
282 use opentelemetry_gcloud_trace::GcpCloudTraceExporterBuilder;
283
284 let mut guard = TelemetryGuard::default();
285
286 let otel_tracer = if self.otel_traces {
288 let gcp_trace_builder = if let Some(ref project_id) = self.otel_gcp_project {
289 GcpCloudTraceExporterBuilder::new(project_id.clone())
290 .with_resource(self.otel_resource())
291 } else {
292 GcpCloudTraceExporterBuilder::for_default_project_id()
293 .await?
294 .with_resource(self.otel_resource())
295 };
296
297 let tracer_provider = gcp_trace_builder.create_provider().await?;
298 let tracer = gcp_trace_builder.install(&tracer_provider).await?;
299 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
300 guard._tracer_provider = Some(tracer_provider);
301 Some(tracer)
302 } else {
303 None
304 };
305
306 if self.otel_metrics {
308 use opentelemetry_gcloud_monitoring_exporter::{
309 GCPMetricsExporter, GCPMetricsExporterConfig,
310 };
311
312 let mut metrics_cfg = GCPMetricsExporterConfig {
313 prefix: format!("custom.googleapis.com/{}", self.otel_service_name),
314 ..Default::default()
315 };
316 if let Some(ref project_id) = self.otel_gcp_project {
317 metrics_cfg.project_id = Some(project_id.clone());
318 }
319 let metrics_exporter = GCPMetricsExporter::init(metrics_cfg).await?;
320
321 use opentelemetry_sdk::metrics::periodic_reader_with_async_runtime::PeriodicReader;
322 let reader =
323 PeriodicReader::builder(metrics_exporter, opentelemetry_sdk::runtime::Tokio)
324 .build();
325
326 let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
327 .with_resource(self.otel_resource())
328 .with_reader(reader)
329 .build();
330 opentelemetry::global::set_meter_provider(meter_provider.clone());
331 guard._meter_provider = Some(meter_provider);
332 }
333
334 Ok(Exporters {
335 guard,
336 tracer: otel_tracer,
337 })
338 }
339
340 #[cfg(feature = "tracing-subscriber")]
345 #[allow(dead_code)]
346 fn init_tracing_subscriber(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
347 self.init_tracing_subscriber_with_tracer(None)
348 }
349
350 #[cfg(feature = "tracing-subscriber")]
354 fn init_tracing_subscriber_with_tracer(
355 &self,
356 #[cfg(feature = "otel-base")] otel_tracer: Option<opentelemetry_sdk::trace::Tracer>,
357 #[cfg(not(feature = "otel-base"))] _otel_tracer: Option<()>,
358 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
359 use tracing_subscriber::EnvFilter;
360 use tracing_subscriber::prelude::*;
361
362 let filter =
363 EnvFilter::try_new(&self.log_filter).unwrap_or_else(|_| EnvFilter::new("info"));
364
365 let fmt_layer = if self.json_logs {
366 tracing_subscriber::fmt::layer().json().boxed()
367 } else {
368 tracing_subscriber::fmt::layer().boxed()
369 };
370
371 let registry = tracing_subscriber::registry().with(filter).with(fmt_layer);
372
373 #[cfg(feature = "otel-base")]
374 {
375 if let Some(tracer) = otel_tracer {
376 let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
377 let subscriber = registry.with(otel_layer);
378 tracing::subscriber::set_global_default(subscriber)
379 .map_err(|e| format!("Failed to set tracing subscriber: {e}"))?;
380 } else {
381 tracing::subscriber::set_global_default(registry)
382 .map_err(|e| format!("Failed to set tracing subscriber: {e}"))?;
383 }
384 }
385
386 #[cfg(not(feature = "otel-base"))]
387 {
388 tracing::subscriber::set_global_default(registry)
389 .map_err(|e| format!("Failed to set tracing subscriber: {e}"))?;
390 }
391
392 Ok(())
393 }
394
395 #[cfg(any(feature = "otel-otlp", feature = "otel-gcp"))]
401 pub(crate) fn otel_resource(&self) -> opentelemetry_sdk::Resource {
402 use opentelemetry::KeyValue;
403 opentelemetry_sdk::Resource::builder_empty()
404 .with_attributes([KeyValue::new(
405 "service.name",
406 self.otel_service_name.clone(),
407 )])
408 .build()
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn default_config_values() {
418 let config = TelemetryConfig::default();
419 assert!(config.logging_enabled);
420 assert_eq!(config.log_filter, "info");
421 assert!(!config.json_logs);
422 assert!(!config.metrics_enabled);
423 assert!(config.metrics_addr.is_none());
424 assert!(!config.otel_traces);
425 assert!(!config.otel_metrics);
426 assert_eq!(config.otel_service_name, "gemini-genai-rs");
427 assert!(config.otel_gcp_project.is_none());
428 }
429
430 #[test]
431 fn config_builder_pattern() {
432 let config = TelemetryConfig {
433 logging_enabled: false,
434 log_filter: "debug".to_string(),
435 json_logs: true,
436 metrics_enabled: true,
437 metrics_addr: Some("0.0.0.0:9090".to_string()),
438 otel_traces: true,
439 otel_metrics: true,
440 otel_service_name: "my-service".to_string(),
441 otel_endpoint: None,
442 otel_gcp_project: Some("my-project".to_string()),
443 };
444 assert!(!config.logging_enabled);
445 assert_eq!(config.log_filter, "debug");
446 assert!(config.json_logs);
447 assert!(config.metrics_enabled);
448 assert_eq!(config.metrics_addr.as_deref(), Some("0.0.0.0:9090"));
449 assert!(config.otel_traces);
450 assert!(config.otel_metrics);
451 assert_eq!(config.otel_service_name, "my-service");
452 assert_eq!(config.otel_gcp_project.as_deref(), Some("my-project"));
453 }
454
455 #[test]
456 fn telemetry_guard_default() {
457 let _guard = TelemetryGuard::default();
458 }
460}