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