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 + | use aws_smithy_observability_otel::meter::OtelMeterProvider;
|
14 + | use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
|
15 + | use aws_smithy_runtime_api::box_error::BoxError;
|
16 + | use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextRef;
|
17 + | use aws_smithy_runtime_api::client::interceptors::Intercept;
|
18 + | use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
|
19 + | use 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]
|
25 + | pub struct ObservabilityDetectionInterceptor;
|
26 + |
|
27 + | impl ObservabilityDetectionInterceptor {
|
28 + | /// Creates a new `ObservabilityDetectionInterceptor`
|
29 + | pub fn new() -> Self {
|
30 + | Self
|
31 + | }
|
32 + | }
|
33 + |
|
34 + | impl 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)]
|
67 + | mod 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 + | }
|