strands_agents/telemetry/
config.rs

1//! OpenTelemetry configuration and setup utilities for Strands agents.
2//!
3//! This module provides centralized configuration and initialization functionality
4//! for OpenTelemetry components and other telemetry infrastructure shared across
5//! Strands applications.
6
7use opentelemetry::global;
8use opentelemetry::trace::TracerProvider;
9use opentelemetry_sdk::{
10    propagation::TraceContextPropagator,
11    trace::SdkTracerProvider,
12    Resource,
13};
14
15/// Resource information for telemetry.
16#[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    /// Get resource attributes as a map.
41    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    /// Convert to OpenTelemetry SDK Resource.
51    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
63/// OpenTelemetry configuration and setup for Strands applications.
64///
65/// Automatically initializes a tracer provider with text map propagators.
66/// Trace exporters (console, OTLP) can be set up individually using dedicated methods
67/// that support method chaining for convenient configuration.
68///
69/// # Examples
70///
71/// Quick setup with method chaining:
72/// ```ignore
73/// StrandsTelemetry::new().setup_console_exporter().setup_otlp_exporter();
74/// ```
75///
76/// Using a custom tracer provider:
77/// ```ignore
78/// StrandsTelemetry::with_tracer_provider(my_provider).setup_console_exporter();
79/// ```
80///
81/// Step-by-step configuration:
82/// ```ignore
83/// let telemetry = StrandsTelemetry::new();
84/// telemetry.setup_console_exporter();
85/// telemetry.setup_otlp_exporter();
86/// ```
87///
88/// To setup global meter provider:
89/// ```ignore
90/// telemetry.setup_meter(true, true); // enable_console, enable_otlp
91/// ```
92pub 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    /// Create a new StrandsTelemetry instance.
108    ///
109    /// This initializes a new tracer provider and sets it as the global provider.
110    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    /// Create a new StrandsTelemetry instance with a pre-configured tracer provider.
123    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    /// Initialize the OpenTelemetry tracer.
134    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    /// Set up console exporter for traces.
151    ///
152    /// This method configures a SimpleSpanProcessor with a ConsoleSpanExporter,
153    /// allowing trace data to be output to the console.
154    #[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    /// Set up console exporter for traces (no-op when feature not enabled).
174    #[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    /// Set up OTLP exporter for traces.
182    ///
183    /// This method configures a BatchSpanProcessor with an OTLPSpanExporter,
184    /// allowing trace data to be exported to an OTLP endpoint.
185    #[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    /// Set up OTLP exporter for traces (no-op when feature not enabled).
212    #[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    /// Set up the meter provider for metrics.
220    #[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    /// Set up the meter provider for metrics (console only).
259    #[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    /// Set up the meter provider for metrics (OTLP only).
283    #[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    /// Set up the meter provider for metrics (no-op when features not enabled).
316    #[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    /// Check if console exporter is enabled.
324    pub fn is_console_enabled(&self) -> bool {
325        self.console_enabled
326    }
327
328    /// Check if OTLP exporter is enabled.
329    pub fn is_otlp_enabled(&self) -> bool {
330        self.otlp_enabled
331    }
332
333    /// Check if meter is enabled.
334    pub fn is_meter_enabled(&self) -> bool {
335        self.meter_enabled
336    }
337
338    /// Get the resource.
339    pub fn resource(&self) -> &OtelResource {
340        &self.resource
341    }
342
343    /// Get the tracer provider.
344    pub fn tracer_provider(&self) -> Option<&SdkTracerProvider> {
345        self.tracer_provider.as_ref()
346    }
347
348    /// Get a tracer from the provider.
349    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
354/// Builder for StrandsTelemetry.
355pub 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}