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
13use aws_smithy_observability_otel::meter::OtelMeterProvider;
14use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
15use aws_smithy_runtime_api::box_error::BoxError;
16use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextRef;
17use aws_smithy_runtime_api::client::interceptors::Intercept;
18use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
19use aws_smithy_types::config_bag::ConfigBag;
20
21/// Interceptor that detects when observability features are being used
22/// and tracks them for business metrics.
23#[derive(Debug, Default)]
24#[non_exhaustive]
25pub struct ObservabilityDetectionInterceptor;
26
27impl ObservabilityDetectionInterceptor {
28    /// Creates a new `ObservabilityDetectionInterceptor`
29    pub fn new() -> Self {
30        Self
31    }
32}
33
34impl Intercept for ObservabilityDetectionInterceptor {
35    fn name(&self) -> &'static str {
36        "ObservabilityDetectionInterceptor"
37    }
38
39    fn read_before_signing(
40        &self,
41        _context: &BeforeTransmitInterceptorContextRef<'_>,
42        _runtime_components: &RuntimeComponents,
43        cfg: &mut ConfigBag,
44    ) -> Result<(), BoxError> {
45        // Try to get the global telemetry provider
46        if let Ok(provider) = aws_smithy_observability::global::get_telemetry_provider() {
47            // Use type-safe downcasting to detect OpenTelemetry meter provider
48            // This works with any ProvideMeter implementation and doesn't require
49            // implementation-specific boolean flags
50            if provider
51                .meter_provider()
52                .as_any()
53                .downcast_ref::<OtelMeterProvider>()
54                .is_some()
55            {
56                // Track that observability metrics are enabled
57                cfg.interceptor_state()
58                    .store_append(SmithySdkFeature::ObservabilityMetrics);
59            }
60        }
61
62        Ok(())
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::sdk_feature::AwsSdkFeature;
70    use aws_smithy_observability::TelemetryProvider;
71    use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
72    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
73    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
74    use aws_smithy_types::config_bag::ConfigBag;
75
76    #[test]
77    fn test_detects_noop_provider() {
78        let mut context = InterceptorContext::new(Input::doesnt_matter());
79        context.enter_serialization_phase();
80        context.set_request(HttpRequest::empty());
81        let _ = context.take_input();
82        context.enter_before_transmit_phase();
83
84        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
85        let mut cfg = ConfigBag::base();
86
87        // Set a noop provider (ignore error if already set by another test)
88        let _ = aws_smithy_observability::global::set_telemetry_provider(TelemetryProvider::noop());
89
90        let interceptor = ObservabilityDetectionInterceptor::new();
91        let ctx = Into::into(&context);
92        interceptor
93            .read_before_signing(&ctx, &rc, &mut cfg)
94            .unwrap();
95
96        // Should not track any features for noop provider since it doesn't downcast to OtelMeterProvider
97        let smithy_features: Vec<_> = cfg.load::<SmithySdkFeature>().collect();
98        assert_eq!(smithy_features.len(), 0);
99
100        let aws_features: Vec<_> = cfg.load::<AwsSdkFeature>().collect();
101        assert_eq!(aws_features.len(), 0);
102    }
103
104    #[test]
105    fn test_custom_provider_not_detected_as_otel() {
106        use aws_smithy_observability::meter::{Meter, ProvideMeter};
107        use aws_smithy_observability::noop::NoopMeterProvider;
108        use aws_smithy_observability::Attributes;
109        use std::sync::Arc;
110
111        // Create a custom (non-OTel, non-noop) meter provider
112        // This simulates a user implementing their own metrics provider
113        #[derive(Debug)]
114        struct CustomMeterProvider {
115            inner: NoopMeterProvider,
116        }
117
118        impl ProvideMeter for CustomMeterProvider {
119            fn get_meter(&self, scope: &'static str, attributes: Option<&Attributes>) -> Meter {
120                // Delegate to noop for simplicity, but this is a distinct type
121                self.inner.get_meter(scope, attributes)
122            }
123
124            fn as_any(&self) -> &dyn std::any::Any {
125                self
126            }
127        }
128
129        let mut context = InterceptorContext::new(Input::doesnt_matter());
130        context.enter_serialization_phase();
131        context.set_request(HttpRequest::empty());
132        let _ = context.take_input();
133        context.enter_before_transmit_phase();
134
135        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
136        let mut cfg = ConfigBag::base();
137
138        // Set the custom provider
139        let custom_provider = Arc::new(CustomMeterProvider {
140            inner: NoopMeterProvider,
141        });
142        let telemetry_provider = TelemetryProvider::builder()
143            .meter_provider(custom_provider)
144            .build();
145        let _ = aws_smithy_observability::global::set_telemetry_provider(telemetry_provider);
146
147        let interceptor = ObservabilityDetectionInterceptor::new();
148        let ctx = Into::into(&context);
149        interceptor
150            .read_before_signing(&ctx, &rc, &mut cfg)
151            .unwrap();
152
153        // Should NOT track any features for custom provider since it doesn't downcast to OtelMeterProvider
154        // The new implementation only emits metrics when OTel is detected
155        let smithy_features: Vec<_> = cfg
156            .interceptor_state()
157            .load::<SmithySdkFeature>()
158            .cloned()
159            .collect();
160        assert!(
161            !smithy_features.iter().any(|f| *f == SmithySdkFeature::ObservabilityMetrics),
162            "Should not detect custom provider as having observability metrics (only OTel is tracked)"
163        );
164
165        // Verify no AWS-specific features are tracked for custom provider
166        let aws_features: Vec<_> = cfg
167            .interceptor_state()
168            .load::<AwsSdkFeature>()
169            .cloned()
170            .collect();
171        assert_eq!(
172            aws_features.len(),
173            0,
174            "Should not track any AWS-specific features for custom provider"
175        );
176    }
177}