aws_runtime/
observability_detection.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Observability feature detection for business metrics tracking
7//!
8//! This module provides an interceptor for detecting observability features in the AWS SDK:
9//!
10//! - [`ObservabilityDetectionInterceptor`]: Detects observability features during
11//!   request processing and tracks them for business metrics in the User-Agent header.
12
13#[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
14use crate::sdk_feature::AwsSdkFeature;
15#[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
16use aws_smithy_observability_otel::meter::OtelMeterProvider;
17#[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
18use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
19use aws_smithy_runtime_api::box_error::BoxError;
20use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextRef;
21use aws_smithy_runtime_api::client::interceptors::Intercept;
22use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
23use aws_smithy_types::config_bag::ConfigBag;
24
25/// Interceptor that detects when observability features are being used
26/// and tracks them for business metrics.
27#[derive(Debug, Default)]
28#[non_exhaustive]
29pub struct ObservabilityDetectionInterceptor;
30
31impl ObservabilityDetectionInterceptor {
32    /// Creates a new `ObservabilityDetectionInterceptor`
33    pub fn new() -> Self {
34        Self
35    }
36}
37
38impl Intercept for ObservabilityDetectionInterceptor {
39    fn name(&self) -> &'static str {
40        "ObservabilityDetectionInterceptor"
41    }
42
43    fn read_before_signing(
44        &self,
45        _context: &BeforeTransmitInterceptorContextRef<'_>,
46        _runtime_components: &RuntimeComponents,
47        _cfg: &mut ConfigBag,
48    ) -> Result<(), BoxError> {
49        #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
50        {
51            // Try to get the global telemetry provider
52            if let Ok(provider) = aws_smithy_observability::global::get_telemetry_provider() {
53                let meter_provider = provider.meter_provider();
54
55                // Check if this is an OpenTelemetry meter provider
56                let is_otel = meter_provider
57                    .as_any()
58                    .downcast_ref::<OtelMeterProvider>()
59                    .is_some();
60
61                // Check if this is a noop provider (we don't want to track noop)
62                let is_noop = meter_provider
63                    .as_any()
64                    .downcast_ref::<aws_smithy_observability::noop::NoopMeterProvider>()
65                    .is_some();
66
67                if !is_noop {
68                    // Track generic observability metrics (for any non-noop provider)
69                    _cfg.interceptor_state()
70                        .store_append(SmithySdkFeature::ObservabilityMetrics);
71
72                    // If it's specifically OpenTelemetry, track that too
73                    if is_otel {
74                        _cfg.interceptor_state()
75                            .store_append(AwsSdkFeature::ObservabilityOtelMetrics);
76                    }
77                }
78            }
79        }
80
81        Ok(())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::sdk_feature::AwsSdkFeature;
89    use aws_smithy_observability::TelemetryProvider;
90    use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
91    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
92    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
93    use aws_smithy_types::config_bag::ConfigBag;
94
95    #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
96    #[test]
97    #[serial_test::serial]
98    fn test_detects_noop_provider() {
99        let mut context = InterceptorContext::new(Input::doesnt_matter());
100        context.enter_serialization_phase();
101        context.set_request(HttpRequest::empty());
102        let _ = context.take_input();
103        context.enter_before_transmit_phase();
104
105        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
106        let mut cfg = ConfigBag::base();
107
108        // Set a noop provider (ignore error if already set by another test)
109        let _ = aws_smithy_observability::global::set_telemetry_provider(TelemetryProvider::noop());
110
111        let interceptor = ObservabilityDetectionInterceptor::new();
112        let ctx = Into::into(&context);
113        interceptor
114            .read_before_signing(&ctx, &rc, &mut cfg)
115            .unwrap();
116
117        // Should not track any features for noop provider
118        let smithy_features: Vec<_> = cfg
119            .interceptor_state()
120            .load::<SmithySdkFeature>()
121            .cloned()
122            .collect();
123        assert_eq!(
124            smithy_features.len(),
125            0,
126            "Should not track Smithy features for noop provider"
127        );
128
129        let aws_features: Vec<_> = cfg
130            .interceptor_state()
131            .load::<AwsSdkFeature>()
132            .cloned()
133            .collect();
134        assert_eq!(
135            aws_features.len(),
136            0,
137            "Should not track AWS features for noop provider"
138        );
139    }
140
141    #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
142    #[test]
143    #[serial_test::serial]
144    fn test_custom_provider_not_detected_as_otel() {
145        use aws_smithy_observability::meter::{Meter, ProvideMeter};
146        use aws_smithy_observability::noop::NoopMeterProvider;
147        use aws_smithy_observability::Attributes;
148        use std::sync::Arc;
149
150        // Create a custom (non-OTel, non-noop) meter provider
151        // This simulates a user implementing their own metrics provider
152        #[derive(Debug)]
153        struct CustomMeterProvider {
154            inner: NoopMeterProvider,
155        }
156
157        impl ProvideMeter for CustomMeterProvider {
158            fn get_meter(&self, scope: &'static str, attributes: Option<&Attributes>) -> Meter {
159                // Delegate to noop for simplicity, but this is a distinct type
160                self.inner.get_meter(scope, attributes)
161            }
162
163            fn as_any(&self) -> &dyn std::any::Any {
164                self
165            }
166        }
167
168        let mut context = InterceptorContext::new(Input::doesnt_matter());
169        context.enter_serialization_phase();
170        context.set_request(HttpRequest::empty());
171        let _ = context.take_input();
172        context.enter_before_transmit_phase();
173
174        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
175        let mut cfg = ConfigBag::base();
176
177        // Set the custom provider
178        let custom_provider = Arc::new(CustomMeterProvider {
179            inner: NoopMeterProvider,
180        });
181        let telemetry_provider = TelemetryProvider::builder()
182            .meter_provider(custom_provider)
183            .build();
184        let _ = aws_smithy_observability::global::set_telemetry_provider(telemetry_provider);
185
186        let interceptor = ObservabilityDetectionInterceptor::new();
187        let ctx = Into::into(&context);
188        interceptor
189            .read_before_signing(&ctx, &rc, &mut cfg)
190            .unwrap();
191
192        // Should track generic observability metrics for custom provider
193        let smithy_features: Vec<_> = cfg
194            .interceptor_state()
195            .load::<SmithySdkFeature>()
196            .cloned()
197            .collect();
198        assert!(
199            smithy_features.contains(&SmithySdkFeature::ObservabilityMetrics),
200            "Should detect custom provider as having observability metrics"
201        );
202
203        // Should NOT track AWS-specific observability metrics for custom provider
204        let aws_features: Vec<_> = cfg
205            .interceptor_state()
206            .load::<AwsSdkFeature>()
207            .cloned()
208            .collect();
209        assert!(
210            !aws_features.contains(&AwsSdkFeature::ObservabilityOtelMetrics),
211            "Should NOT track OTel-specific metrics for custom provider"
212        );
213        assert_eq!(
214            aws_features.len(),
215            0,
216            "Should not track any AWS-specific features for custom provider"
217        );
218    }
219
220    #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
221    #[test]
222    #[serial_test::serial]
223    fn test_detects_otel_provider() {
224        use aws_smithy_observability_otel::meter::OtelMeterProvider;
225        use opentelemetry_sdk::metrics::SdkMeterProvider;
226        use std::sync::Arc;
227
228        let mut context = InterceptorContext::new(Input::doesnt_matter());
229        context.enter_serialization_phase();
230        context.set_request(HttpRequest::empty());
231        let _ = context.take_input();
232        context.enter_before_transmit_phase();
233
234        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
235        let mut cfg = ConfigBag::base();
236
237        // Create an actual OpenTelemetry meter provider
238        let otel_sdk_provider = SdkMeterProvider::builder().build();
239        let otel_provider = Arc::new(OtelMeterProvider::new(otel_sdk_provider));
240        let telemetry_provider = TelemetryProvider::builder()
241            .meter_provider(otel_provider)
242            .build();
243        let _ = aws_smithy_observability::global::set_telemetry_provider(telemetry_provider);
244
245        let interceptor = ObservabilityDetectionInterceptor::new();
246        let ctx = Into::into(&context);
247        interceptor
248            .read_before_signing(&ctx, &rc, &mut cfg)
249            .unwrap();
250
251        // Should track generic observability metrics for OTel provider
252        let smithy_features: Vec<_> = cfg
253            .interceptor_state()
254            .load::<SmithySdkFeature>()
255            .cloned()
256            .collect();
257        assert!(
258            smithy_features.contains(&SmithySdkFeature::ObservabilityMetrics),
259            "Should detect OTel provider as having observability metrics"
260        );
261
262        // Should ALSO track AWS-specific OTel observability metrics
263        let aws_features: Vec<_> = cfg
264            .interceptor_state()
265            .load::<AwsSdkFeature>()
266            .cloned()
267            .collect();
268        assert!(
269            aws_features.contains(&AwsSdkFeature::ObservabilityOtelMetrics),
270            "Should track OTel-specific metrics for OTel provider"
271        );
272    }
273
274    // Edge case tests
275
276    #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
277    #[test]
278    #[serial_test::serial]
279    fn test_multiple_provider_changes() {
280        use aws_smithy_observability::meter::{Meter, ProvideMeter};
281        use aws_smithy_observability::noop::NoopMeterProvider;
282        use aws_smithy_observability::Attributes;
283        use aws_smithy_observability_otel::meter::OtelMeterProvider;
284        use opentelemetry_sdk::metrics::SdkMeterProvider;
285        use std::sync::Arc;
286
287        #[derive(Debug)]
288        struct CustomMeterProvider {
289            inner: NoopMeterProvider,
290        }
291
292        impl ProvideMeter for CustomMeterProvider {
293            fn get_meter(&self, scope: &'static str, attributes: Option<&Attributes>) -> Meter {
294                self.inner.get_meter(scope, attributes)
295            }
296
297            fn as_any(&self) -> &dyn std::any::Any {
298                self
299            }
300        }
301
302        let interceptor = ObservabilityDetectionInterceptor::new();
303        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
304
305        // Test 1: Start with noop provider
306        let _ = aws_smithy_observability::global::set_telemetry_provider(TelemetryProvider::noop());
307
308        let mut context1 = InterceptorContext::new(Input::doesnt_matter());
309        context1.enter_serialization_phase();
310        context1.set_request(HttpRequest::empty());
311        let _ = context1.take_input();
312        context1.enter_before_transmit_phase();
313        let mut cfg1 = ConfigBag::base();
314
315        interceptor
316            .read_before_signing(&Into::into(&context1), &rc, &mut cfg1)
317            .unwrap();
318
319        let smithy_features: Vec<_> = cfg1
320            .interceptor_state()
321            .load::<SmithySdkFeature>()
322            .cloned()
323            .collect();
324        assert_eq!(
325            smithy_features.len(),
326            0,
327            "Noop provider should not be tracked"
328        );
329
330        // Test 2: Switch to custom provider
331        let custom_provider = Arc::new(CustomMeterProvider {
332            inner: NoopMeterProvider,
333        });
334        let telemetry_provider = TelemetryProvider::builder()
335            .meter_provider(custom_provider)
336            .build();
337        let _ = aws_smithy_observability::global::set_telemetry_provider(telemetry_provider);
338
339        let mut context2 = InterceptorContext::new(Input::doesnt_matter());
340        context2.enter_serialization_phase();
341        context2.set_request(HttpRequest::empty());
342        let _ = context2.take_input();
343        context2.enter_before_transmit_phase();
344        let mut cfg2 = ConfigBag::base();
345
346        interceptor
347            .read_before_signing(&Into::into(&context2), &rc, &mut cfg2)
348            .unwrap();
349
350        let smithy_features: Vec<_> = cfg2
351            .interceptor_state()
352            .load::<SmithySdkFeature>()
353            .cloned()
354            .collect();
355        assert!(
356            smithy_features.contains(&SmithySdkFeature::ObservabilityMetrics),
357            "Custom provider should be tracked"
358        );
359        let aws_features: Vec<_> = cfg2
360            .interceptor_state()
361            .load::<AwsSdkFeature>()
362            .cloned()
363            .collect();
364        assert_eq!(
365            aws_features.len(),
366            0,
367            "Custom provider should not have OTel features"
368        );
369
370        // Test 3: Switch to OTel provider
371        let otel_sdk_provider = SdkMeterProvider::builder().build();
372        let otel_provider = Arc::new(OtelMeterProvider::new(otel_sdk_provider));
373        let telemetry_provider = TelemetryProvider::builder()
374            .meter_provider(otel_provider)
375            .build();
376        let _ = aws_smithy_observability::global::set_telemetry_provider(telemetry_provider);
377
378        let mut context3 = InterceptorContext::new(Input::doesnt_matter());
379        context3.enter_serialization_phase();
380        context3.set_request(HttpRequest::empty());
381        let _ = context3.take_input();
382        context3.enter_before_transmit_phase();
383        let mut cfg3 = ConfigBag::base();
384
385        interceptor
386            .read_before_signing(&Into::into(&context3), &rc, &mut cfg3)
387            .unwrap();
388
389        let smithy_features: Vec<_> = cfg3
390            .interceptor_state()
391            .load::<SmithySdkFeature>()
392            .cloned()
393            .collect();
394        assert!(
395            smithy_features.contains(&SmithySdkFeature::ObservabilityMetrics),
396            "OTel provider should be tracked"
397        );
398        let aws_features: Vec<_> = cfg3
399            .interceptor_state()
400            .load::<AwsSdkFeature>()
401            .cloned()
402            .collect();
403        assert!(
404            aws_features.contains(&AwsSdkFeature::ObservabilityOtelMetrics),
405            "OTel provider should have OTel features"
406        );
407    }
408
409    #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
410    #[test]
411    #[serial_test::serial]
412    fn test_multiple_invocations_same_provider() {
413        use aws_smithy_observability::meter::{Meter, ProvideMeter};
414        use aws_smithy_observability::noop::NoopMeterProvider;
415        use aws_smithy_observability::Attributes;
416        use std::sync::Arc;
417
418        #[derive(Debug)]
419        struct CustomMeterProvider {
420            inner: NoopMeterProvider,
421        }
422
423        impl ProvideMeter for CustomMeterProvider {
424            fn get_meter(&self, scope: &'static str, attributes: Option<&Attributes>) -> Meter {
425                self.inner.get_meter(scope, attributes)
426            }
427
428            fn as_any(&self) -> &dyn std::any::Any {
429                self
430            }
431        }
432
433        // Set up a custom provider
434        let custom_provider = Arc::new(CustomMeterProvider {
435            inner: NoopMeterProvider,
436        });
437        let telemetry_provider = TelemetryProvider::builder()
438            .meter_provider(custom_provider)
439            .build();
440        let _ = aws_smithy_observability::global::set_telemetry_provider(telemetry_provider);
441
442        let interceptor = ObservabilityDetectionInterceptor::new();
443        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
444
445        // Invoke interceptor multiple times with the same provider
446        for i in 0..3 {
447            let mut context = InterceptorContext::new(Input::doesnt_matter());
448            context.enter_serialization_phase();
449            context.set_request(HttpRequest::empty());
450            let _ = context.take_input();
451            context.enter_before_transmit_phase();
452            let mut cfg = ConfigBag::base();
453
454            interceptor
455                .read_before_signing(&Into::into(&context), &rc, &mut cfg)
456                .unwrap();
457
458            // Each invocation should detect the provider consistently
459            let smithy_features: Vec<_> = cfg
460                .interceptor_state()
461                .load::<SmithySdkFeature>()
462                .cloned()
463                .collect();
464            assert!(
465                smithy_features.contains(&SmithySdkFeature::ObservabilityMetrics),
466                "Invocation {} should detect custom provider",
467                i
468            );
469            let aws_features: Vec<_> = cfg
470                .interceptor_state()
471                .load::<AwsSdkFeature>()
472                .cloned()
473                .collect();
474            assert_eq!(
475                aws_features.len(),
476                0,
477                "Invocation {} should not have OTel features for custom provider",
478                i
479            );
480        }
481    }
482
483    #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
484    #[test]
485    #[serial_test::serial]
486    fn test_interceptor_handles_errors_gracefully() {
487        // This test verifies that the interceptor doesn't panic or fail
488        // when the global provider is in various states
489
490        let interceptor = ObservabilityDetectionInterceptor::new();
491        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
492
493        // Test with noop provider (should succeed without tracking)
494        let _ = aws_smithy_observability::global::set_telemetry_provider(TelemetryProvider::noop());
495
496        let mut context = InterceptorContext::new(Input::doesnt_matter());
497        context.enter_serialization_phase();
498        context.set_request(HttpRequest::empty());
499        let _ = context.take_input();
500        context.enter_before_transmit_phase();
501        let mut cfg = ConfigBag::base();
502
503        // Should not return an error
504        let result = interceptor.read_before_signing(&Into::into(&context), &rc, &mut cfg);
505        assert!(
506            result.is_ok(),
507            "Interceptor should handle noop provider gracefully"
508        );
509
510        // Verify no features were tracked
511        let smithy_features: Vec<_> = cfg
512            .interceptor_state()
513            .load::<SmithySdkFeature>()
514            .cloned()
515            .collect();
516        assert_eq!(
517            smithy_features.len(),
518            0,
519            "Should not track features for noop provider"
520        );
521    }
522
523    #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))]
524    #[test]
525    #[serial_test::serial]
526    fn test_interceptor_with_default_constructor() {
527        use aws_smithy_observability::meter::{Meter, ProvideMeter};
528        use aws_smithy_observability::noop::NoopMeterProvider;
529        use aws_smithy_observability::Attributes;
530        use std::sync::Arc;
531
532        #[derive(Debug)]
533        struct CustomMeterProvider {
534            inner: NoopMeterProvider,
535        }
536
537        impl ProvideMeter for CustomMeterProvider {
538            fn get_meter(&self, scope: &'static str, attributes: Option<&Attributes>) -> Meter {
539                self.inner.get_meter(scope, attributes)
540            }
541
542            fn as_any(&self) -> &dyn std::any::Any {
543                self
544            }
545        }
546
547        // Set up a custom provider
548        let custom_provider = Arc::new(CustomMeterProvider {
549            inner: NoopMeterProvider,
550        });
551        let telemetry_provider = TelemetryProvider::builder()
552            .meter_provider(custom_provider)
553            .build();
554        let _ = aws_smithy_observability::global::set_telemetry_provider(telemetry_provider);
555
556        // Test both constructors produce equivalent behavior
557        let interceptor_new = ObservabilityDetectionInterceptor::new();
558        let interceptor_default = ObservabilityDetectionInterceptor::default();
559
560        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
561
562        // Test with new()
563        let mut context1 = InterceptorContext::new(Input::doesnt_matter());
564        context1.enter_serialization_phase();
565        context1.set_request(HttpRequest::empty());
566        let _ = context1.take_input();
567        context1.enter_before_transmit_phase();
568        let mut cfg1 = ConfigBag::base();
569
570        interceptor_new
571            .read_before_signing(&Into::into(&context1), &rc, &mut cfg1)
572            .unwrap();
573
574        // Test with default()
575        let mut context2 = InterceptorContext::new(Input::doesnt_matter());
576        context2.enter_serialization_phase();
577        context2.set_request(HttpRequest::empty());
578        let _ = context2.take_input();
579        context2.enter_before_transmit_phase();
580        let mut cfg2 = ConfigBag::base();
581
582        interceptor_default
583            .read_before_signing(&Into::into(&context2), &rc, &mut cfg2)
584            .unwrap();
585
586        // Both should produce the same results
587        let smithy_features1: Vec<_> = cfg1
588            .interceptor_state()
589            .load::<SmithySdkFeature>()
590            .cloned()
591            .collect();
592        let smithy_features2: Vec<_> = cfg2
593            .interceptor_state()
594            .load::<SmithySdkFeature>()
595            .cloned()
596            .collect();
597        assert_eq!(
598            smithy_features1, smithy_features2,
599            "Both constructors should produce identical behavior"
600        );
601    }
602}