strands_agents/telemetry/
config.rs1use opentelemetry::global;
8use opentelemetry::trace::TracerProvider;
9use opentelemetry_sdk::{
10 propagation::TraceContextPropagator,
11 trace::SdkTracerProvider,
12 Resource,
13};
14
15#[derive(Debug, Clone)]
17pub struct OtelResource {
18 pub service_name: String,
19 pub service_version: String,
20 pub sdk_name: String,
21 pub sdk_language: String,
22}
23
24impl Default for OtelResource {
25 fn default() -> Self {
26 Self {
27 service_name: "strands-agents".to_string(),
28 service_version: env!("CARGO_PKG_VERSION").to_string(),
29 sdk_name: "opentelemetry".to_string(),
30 sdk_language: "rust".to_string(),
31 }
32 }
33}
34
35impl OtelResource {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn attributes(&self) -> std::collections::HashMap<String, String> {
42 let mut attrs = std::collections::HashMap::new();
43 attrs.insert("service.name".to_string(), self.service_name.clone());
44 attrs.insert("service.version".to_string(), self.service_version.clone());
45 attrs.insert("telemetry.sdk.name".to_string(), self.sdk_name.clone());
46 attrs.insert("telemetry.sdk.language".to_string(), self.sdk_language.clone());
47 attrs
48 }
49
50 fn to_otel_resource(&self) -> Resource {
52 Resource::builder()
53 .with_service_name(self.service_name.clone())
54 .with_attributes([
55 opentelemetry::KeyValue::new("service.version", self.service_version.clone()),
56 opentelemetry::KeyValue::new("telemetry.sdk.name", self.sdk_name.clone()),
57 opentelemetry::KeyValue::new("telemetry.sdk.language", self.sdk_language.clone()),
58 ])
59 .build()
60 }
61}
62
63pub struct StrandsTelemetry {
93 pub resource: OtelResource,
94 tracer_provider: Option<SdkTracerProvider>,
95 console_enabled: bool,
96 otlp_enabled: bool,
97 meter_enabled: bool,
98}
99
100impl Default for StrandsTelemetry {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl StrandsTelemetry {
107 pub fn new() -> Self {
111 let mut instance = Self {
112 resource: OtelResource::default(),
113 tracer_provider: None,
114 console_enabled: false,
115 otlp_enabled: false,
116 meter_enabled: false,
117 };
118 instance.initialize_tracer();
119 instance
120 }
121
122 pub fn with_tracer_provider(tracer_provider: SdkTracerProvider) -> Self {
124 Self {
125 resource: OtelResource::default(),
126 tracer_provider: Some(tracer_provider),
127 console_enabled: false,
128 otlp_enabled: false,
129 meter_enabled: false,
130 }
131 }
132
133 fn initialize_tracer(&mut self) {
135 tracing::info!("Initializing tracer");
136
137 let resource = self.resource.to_otel_resource();
138 let tracer_provider = SdkTracerProvider::builder()
139 .with_resource(resource)
140 .build();
141
142 global::set_tracer_provider(tracer_provider.clone());
143
144 let propagator = TraceContextPropagator::new();
145 global::set_text_map_propagator(propagator);
146
147 self.tracer_provider = Some(tracer_provider);
148 }
149
150 #[cfg(feature = "otel-stdout")]
155 pub fn setup_console_exporter(mut self) -> Self {
156 use opentelemetry_stdout::SpanExporter as StdoutSpanExporter;
157
158 tracing::info!("Enabling console export");
159 self.console_enabled = true;
160
161 let exporter = StdoutSpanExporter::default();
162 let new_provider = SdkTracerProvider::builder()
163 .with_simple_exporter(exporter)
164 .with_resource(self.resource.to_otel_resource())
165 .build();
166
167 global::set_tracer_provider(new_provider.clone());
168 self.tracer_provider = Some(new_provider);
169
170 self
171 }
172
173 #[cfg(not(feature = "otel-stdout"))]
175 pub fn setup_console_exporter(mut self) -> Self {
176 tracing::warn!("otel-stdout feature not enabled, console exporter not available");
177 self.console_enabled = false;
178 self
179 }
180
181 #[cfg(feature = "otel-otlp")]
186 pub fn setup_otlp_exporter(mut self) -> Self {
187 use opentelemetry_otlp::SpanExporter;
188
189 tracing::info!("Enabling OTLP export");
190 self.otlp_enabled = true;
191
192 match SpanExporter::builder().with_tonic().build() {
193 Ok(exporter) => {
194 let new_provider = SdkTracerProvider::builder()
195 .with_batch_exporter(exporter)
196 .with_resource(self.resource.to_otel_resource())
197 .build();
198
199 global::set_tracer_provider(new_provider.clone());
200 self.tracer_provider = Some(new_provider);
201 tracing::info!("OTLP exporter configured");
202 }
203 Err(e) => {
204 tracing::error!("error=<{}> | Failed to configure OTLP exporter", e);
205 }
206 }
207
208 self
209 }
210
211 #[cfg(not(feature = "otel-otlp"))]
213 pub fn setup_otlp_exporter(mut self) -> Self {
214 tracing::warn!("otel-otlp feature not enabled, OTLP exporter not available");
215 self.otlp_enabled = false;
216 self
217 }
218
219 #[cfg(all(feature = "otel-stdout", feature = "otel-otlp"))]
221 pub fn setup_meter(mut self, enable_console: bool, enable_otlp: bool) -> Self {
222 use opentelemetry_sdk::metrics::SdkMeterProvider;
223
224 tracing::info!("Initializing meter");
225 self.meter_enabled = true;
226
227 let resource = self.resource.to_otel_resource();
228 let mut builder = SdkMeterProvider::builder().with_resource(resource);
229
230 if enable_console {
231 tracing::info!("Enabling console metrics exporter");
232 let exporter = opentelemetry_stdout::MetricExporter::default();
233 builder = builder.with_periodic_exporter(exporter);
234 }
235
236 if enable_otlp {
237 tracing::info!("Enabling OTLP metrics exporter");
238 match opentelemetry_otlp::MetricExporter::builder()
239 .with_tonic()
240 .build()
241 {
242 Ok(exporter) => {
243 builder = builder.with_periodic_exporter(exporter);
244 }
245 Err(e) => {
246 tracing::error!("error=<{}> | Failed to configure OTLP metrics exporter", e);
247 }
248 }
249 }
250
251 let meter_provider = builder.build();
252 opentelemetry::global::set_meter_provider(meter_provider);
253 tracing::info!("Strands Meter configured");
254
255 self
256 }
257
258 #[cfg(all(feature = "otel-stdout", not(feature = "otel-otlp")))]
260 pub fn setup_meter(mut self, enable_console: bool, _enable_otlp: bool) -> Self {
261 use opentelemetry_sdk::metrics::SdkMeterProvider;
262
263 tracing::info!("Initializing meter");
264 self.meter_enabled = true;
265
266 let resource = self.resource.to_otel_resource();
267 let mut builder = SdkMeterProvider::builder().with_resource(resource);
268
269 if enable_console {
270 tracing::info!("Enabling console metrics exporter");
271 let exporter = opentelemetry_stdout::MetricExporter::default();
272 builder = builder.with_periodic_exporter(exporter);
273 }
274
275 let meter_provider = builder.build();
276 opentelemetry::global::set_meter_provider(meter_provider);
277 tracing::info!("Strands Meter configured");
278
279 self
280 }
281
282 #[cfg(all(not(feature = "otel-stdout"), feature = "otel-otlp"))]
284 pub fn setup_meter(mut self, _enable_console: bool, enable_otlp: bool) -> Self {
285 use opentelemetry_sdk::metrics::SdkMeterProvider;
286
287 tracing::info!("Initializing meter");
288 self.meter_enabled = true;
289
290 let resource = self.resource.to_otel_resource();
291 let mut builder = SdkMeterProvider::builder().with_resource(resource);
292
293 if enable_otlp {
294 tracing::info!("Enabling OTLP metrics exporter");
295 match opentelemetry_otlp::MetricExporter::builder()
296 .with_tonic()
297 .build()
298 {
299 Ok(exporter) => {
300 builder = builder.with_periodic_exporter(exporter);
301 }
302 Err(e) => {
303 tracing::error!("error=<{}> | Failed to configure OTLP metrics exporter", e);
304 }
305 }
306 }
307
308 let meter_provider = builder.build();
309 opentelemetry::global::set_meter_provider(meter_provider);
310 tracing::info!("Strands Meter configured");
311
312 self
313 }
314
315 #[cfg(not(any(feature = "otel-stdout", feature = "otel-otlp")))]
317 pub fn setup_meter(mut self, _enable_console: bool, _enable_otlp: bool) -> Self {
318 tracing::warn!("Neither otel-stdout nor otel-otlp features enabled, meter not available");
319 self.meter_enabled = false;
320 self
321 }
322
323 pub fn is_console_enabled(&self) -> bool {
325 self.console_enabled
326 }
327
328 pub fn is_otlp_enabled(&self) -> bool {
330 self.otlp_enabled
331 }
332
333 pub fn is_meter_enabled(&self) -> bool {
335 self.meter_enabled
336 }
337
338 pub fn resource(&self) -> &OtelResource {
340 &self.resource
341 }
342
343 pub fn tracer_provider(&self) -> Option<&SdkTracerProvider> {
345 self.tracer_provider.as_ref()
346 }
347
348 pub fn tracer(&self, name: &'static str) -> Option<opentelemetry_sdk::trace::Tracer> {
350 self.tracer_provider.as_ref().map(|p| p.tracer(name))
351 }
352}
353
354pub struct StrandsTelemetryBuilder {
356 resource: OtelResource,
357 enable_console: bool,
358 enable_otlp: bool,
359 enable_console_metrics: bool,
360 enable_otlp_metrics: bool,
361}
362
363impl Default for StrandsTelemetryBuilder {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369impl StrandsTelemetryBuilder {
370 pub fn new() -> Self {
371 Self {
372 resource: OtelResource::default(),
373 enable_console: false,
374 enable_otlp: false,
375 enable_console_metrics: false,
376 enable_otlp_metrics: false,
377 }
378 }
379
380 pub fn with_resource(mut self, resource: OtelResource) -> Self {
381 self.resource = resource;
382 self
383 }
384
385 pub fn with_console_exporter(mut self) -> Self {
386 self.enable_console = true;
387 self
388 }
389
390 pub fn with_otlp_exporter(mut self) -> Self {
391 self.enable_otlp = true;
392 self
393 }
394
395 pub fn with_console_metrics(mut self) -> Self {
396 self.enable_console_metrics = true;
397 self
398 }
399
400 pub fn with_otlp_metrics(mut self) -> Self {
401 self.enable_otlp_metrics = true;
402 self
403 }
404
405 pub fn build(self) -> StrandsTelemetry {
406 let mut telemetry = StrandsTelemetry {
407 resource: self.resource,
408 tracer_provider: None,
409 console_enabled: false,
410 otlp_enabled: false,
411 meter_enabled: false,
412 };
413
414 telemetry.initialize_tracer();
415
416 if self.enable_console {
417 telemetry = telemetry.setup_console_exporter();
418 }
419 if self.enable_otlp {
420 telemetry = telemetry.setup_otlp_exporter();
421 }
422 if self.enable_console_metrics || self.enable_otlp_metrics {
423 telemetry = telemetry.setup_meter(self.enable_console_metrics, self.enable_otlp_metrics);
424 }
425
426 telemetry
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn test_otel_resource_default() {
436 let resource = OtelResource::default();
437 assert_eq!(resource.service_name, "strands-agents");
438 assert_eq!(resource.sdk_language, "rust");
439 }
440
441 #[test]
442 fn test_otel_resource_attributes() {
443 let resource = OtelResource::default();
444 let attrs = resource.attributes();
445 assert!(attrs.contains_key("service.name"));
446 assert!(attrs.contains_key("telemetry.sdk.language"));
447 }
448
449 #[test]
450 fn test_strands_telemetry_builder() {
451 let telemetry = StrandsTelemetryBuilder::new()
452 .with_console_exporter()
453 .build();
454
455 assert!(telemetry.tracer_provider().is_some());
456 }
457
458 #[test]
459 fn test_strands_telemetry_new() {
460 let telemetry = StrandsTelemetry::new();
461 assert!(telemetry.tracer_provider().is_some());
462 assert!(!telemetry.is_console_enabled());
463 assert!(!telemetry.is_otlp_enabled());
464 }
465
466 #[test]
467 fn test_strands_telemetry_chaining() {
468 let telemetry = StrandsTelemetry::new()
469 .setup_console_exporter();
470
471 assert!(telemetry.tracer_provider().is_some());
472 }
473
474 #[tokio::test]
475 #[cfg(feature = "otel-otlp")]
476 async fn test_strands_telemetry_otlp() {
477 let telemetry = StrandsTelemetry::new()
478 .setup_otlp_exporter();
479
480 assert!(telemetry.tracer_provider().is_some());
481 }
482}