gemini_genai_rs/telemetry/
mod.rs1pub mod logging;
11pub mod metrics;
12pub mod spans;
13
14#[derive(Debug, Clone)]
16pub struct TelemetryConfig {
17 pub logging_enabled: bool,
20 pub log_filter: String,
22 pub json_logs: bool,
24 pub metrics_enabled: bool,
26 pub metrics_addr: Option<String>,
28 pub otel_traces: bool,
30 pub otel_metrics: bool,
32 pub otel_service_name: String,
34 pub otel_endpoint: Option<String>,
39 pub otel_gcp_project: Option<String>,
42}
43
44impl Default for TelemetryConfig {
45 fn default() -> Self {
46 Self {
47 logging_enabled: true,
48 log_filter: "info".to_string(),
49 json_logs: false,
50 metrics_enabled: false,
51 metrics_addr: None,
52 otel_traces: false,
53 otel_metrics: false,
54 otel_service_name: "gemini-genai-rs".to_string(),
55 otel_endpoint: None,
56 otel_gcp_project: None,
57 }
58 }
59}
60
61#[derive(Default)]
64pub struct TelemetryGuard {
65 #[cfg(feature = "otel-base")]
66 _tracer_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
67 #[cfg(feature = "otel-base")]
68 _meter_provider: Option<opentelemetry_sdk::metrics::SdkMeterProvider>,
69 #[cfg(not(feature = "otel-base"))]
70 _private: (),
71}
72
73impl TelemetryConfig {
74 pub fn init(&self) -> Result<TelemetryGuard, Box<dyn std::error::Error>> {
87 #[allow(unused_mut)]
88 let mut guard = TelemetryGuard::default();
89
90 #[cfg(feature = "otel-otlp")]
92 let otel_tracer = if self.otel_traces {
93 use opentelemetry_otlp::WithExportConfig as _;
94 let mut builder = opentelemetry_otlp::SpanExporter::builder().with_tonic();
95 if let Some(endpoint) = &self.otel_endpoint {
96 builder = builder.with_endpoint(endpoint.clone());
97 }
98 let exporter = builder.build()?;
99 let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
100 .with_batch_exporter(exporter)
101 .with_resource(self.otel_resource())
102 .build();
103 let tracer = opentelemetry::trace::TracerProvider::tracer(
104 &provider,
105 self.otel_service_name.clone(),
106 );
107 guard._tracer_provider = Some(provider);
108 Some(tracer)
109 } else {
110 None
111 };
112
113 #[cfg(feature = "otel-otlp")]
114 if self.otel_metrics {
115 use opentelemetry_otlp::WithExportConfig as _;
116 let mut builder = opentelemetry_otlp::MetricExporter::builder().with_tonic();
117 if let Some(endpoint) = &self.otel_endpoint {
118 builder = builder.with_endpoint(endpoint.clone());
119 }
120 let exporter = builder.build()?;
121 let provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
122 .with_periodic_exporter(exporter)
123 .with_resource(self.otel_resource())
124 .build();
125 opentelemetry::global::set_meter_provider(provider.clone());
126 guard._meter_provider = Some(provider);
127 }
128
129 #[cfg(feature = "tracing-subscriber")]
131 if self.logging_enabled {
132 #[cfg(feature = "otel-otlp")]
133 {
134 self.init_tracing_subscriber_with_tracer(otel_tracer)
135 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
136 }
137 #[cfg(not(feature = "otel-otlp"))]
138 {
139 self.init_tracing_subscriber()
140 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
141 }
142 }
143
144 Ok(guard)
145 }
146
147 #[cfg(feature = "otel-gcp")]
158 pub async fn init_gcp(
159 &self,
160 ) -> Result<TelemetryGuard, Box<dyn std::error::Error + Send + Sync>> {
161 use opentelemetry_gcloud_trace::GcpCloudTraceExporterBuilder;
162
163 let mut guard = TelemetryGuard::default();
164
165 let otel_tracer = if self.otel_traces {
167 let gcp_trace_builder = if let Some(ref project_id) = self.otel_gcp_project {
168 GcpCloudTraceExporterBuilder::new(project_id.clone())
169 .with_resource(self.otel_resource())
170 } else {
171 GcpCloudTraceExporterBuilder::for_default_project_id()
172 .await?
173 .with_resource(self.otel_resource())
174 };
175
176 let tracer_provider = gcp_trace_builder.create_provider().await?;
177 let tracer = gcp_trace_builder.install(&tracer_provider).await?;
178 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
179 guard._tracer_provider = Some(tracer_provider);
180 Some(tracer)
181 } else {
182 None
183 };
184
185 if self.otel_metrics {
187 use opentelemetry_gcloud_monitoring_exporter::{
188 GCPMetricsExporter, GCPMetricsExporterConfig,
189 };
190
191 let mut metrics_cfg = GCPMetricsExporterConfig {
192 prefix: format!("custom.googleapis.com/{}", self.otel_service_name),
193 ..Default::default()
194 };
195 if let Some(ref project_id) = self.otel_gcp_project {
196 metrics_cfg.project_id = Some(project_id.clone());
197 }
198 let metrics_exporter = GCPMetricsExporter::init(metrics_cfg).await?;
199
200 use opentelemetry_sdk::metrics::periodic_reader_with_async_runtime::PeriodicReader;
201 let reader =
202 PeriodicReader::builder(metrics_exporter, opentelemetry_sdk::runtime::Tokio)
203 .build();
204
205 let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
206 .with_resource(self.otel_resource())
207 .with_reader(reader)
208 .build();
209 opentelemetry::global::set_meter_provider(meter_provider.clone());
210 guard._meter_provider = Some(meter_provider);
211 }
212
213 #[cfg(feature = "tracing-subscriber")]
215 if self.logging_enabled {
216 self.init_tracing_subscriber_with_tracer(otel_tracer)?;
217 }
218
219 Ok(guard)
220 }
221
222 #[cfg(feature = "tracing-subscriber")]
227 #[allow(dead_code)]
228 fn init_tracing_subscriber(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
229 self.init_tracing_subscriber_with_tracer(None)
230 }
231
232 #[cfg(feature = "tracing-subscriber")]
236 fn init_tracing_subscriber_with_tracer(
237 &self,
238 #[cfg(feature = "otel-base")] otel_tracer: Option<opentelemetry_sdk::trace::Tracer>,
239 #[cfg(not(feature = "otel-base"))] _otel_tracer: Option<()>,
240 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
241 use tracing_subscriber::EnvFilter;
242 use tracing_subscriber::prelude::*;
243
244 let filter =
245 EnvFilter::try_new(&self.log_filter).unwrap_or_else(|_| EnvFilter::new("info"));
246
247 let fmt_layer = if self.json_logs {
248 tracing_subscriber::fmt::layer().json().boxed()
249 } else {
250 tracing_subscriber::fmt::layer().boxed()
251 };
252
253 let registry = tracing_subscriber::registry().with(filter).with(fmt_layer);
254
255 #[cfg(feature = "otel-base")]
256 {
257 if let Some(tracer) = otel_tracer {
258 let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
259 let subscriber = registry.with(otel_layer);
260 tracing::subscriber::set_global_default(subscriber)
261 .map_err(|e| format!("Failed to set tracing subscriber: {e}"))?;
262 } else {
263 tracing::subscriber::set_global_default(registry)
264 .map_err(|e| format!("Failed to set tracing subscriber: {e}"))?;
265 }
266 }
267
268 #[cfg(not(feature = "otel-base"))]
269 {
270 tracing::subscriber::set_global_default(registry)
271 .map_err(|e| format!("Failed to set tracing subscriber: {e}"))?;
272 }
273
274 Ok(())
275 }
276
277 #[cfg(any(feature = "otel-otlp", feature = "otel-gcp"))]
283 pub(crate) fn otel_resource(&self) -> opentelemetry_sdk::Resource {
284 use opentelemetry::KeyValue;
285 opentelemetry_sdk::Resource::builder_empty()
286 .with_attributes([KeyValue::new(
287 "service.name",
288 self.otel_service_name.clone(),
289 )])
290 .build()
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn default_config_values() {
300 let config = TelemetryConfig::default();
301 assert!(config.logging_enabled);
302 assert_eq!(config.log_filter, "info");
303 assert!(!config.json_logs);
304 assert!(!config.metrics_enabled);
305 assert!(config.metrics_addr.is_none());
306 assert!(!config.otel_traces);
307 assert!(!config.otel_metrics);
308 assert_eq!(config.otel_service_name, "gemini-genai-rs");
309 assert!(config.otel_gcp_project.is_none());
310 }
311
312 #[test]
313 fn config_builder_pattern() {
314 let config = TelemetryConfig {
315 logging_enabled: false,
316 log_filter: "debug".to_string(),
317 json_logs: true,
318 metrics_enabled: true,
319 metrics_addr: Some("0.0.0.0:9090".to_string()),
320 otel_traces: true,
321 otel_metrics: true,
322 otel_service_name: "my-service".to_string(),
323 otel_endpoint: None,
324 otel_gcp_project: Some("my-project".to_string()),
325 };
326 assert!(!config.logging_enabled);
327 assert_eq!(config.log_filter, "debug");
328 assert!(config.json_logs);
329 assert!(config.metrics_enabled);
330 assert_eq!(config.metrics_addr.as_deref(), Some("0.0.0.0:9090"));
331 assert!(config.otel_traces);
332 assert!(config.otel_metrics);
333 assert_eq!(config.otel_service_name, "my-service");
334 assert_eq!(config.otel_gcp_project.as_deref(), Some("my-project"));
335 }
336
337 #[test]
338 fn telemetry_guard_default() {
339 let _guard = TelemetryGuard::default();
340 }
342}