1#[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#[derive(Debug, Default)]
28#[non_exhaustive]
29pub struct ObservabilityDetectionInterceptor;
30
31impl ObservabilityDetectionInterceptor {
32 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 if let Ok(provider) = aws_smithy_observability::global::get_telemetry_provider() {
53 let meter_provider = provider.meter_provider();
54
55 let is_otel = meter_provider
57 .as_any()
58 .downcast_ref::<OtelMeterProvider>()
59 .is_some();
60
61 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 _cfg.interceptor_state()
70 .store_append(SmithySdkFeature::ObservabilityMetrics);
71
72 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 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 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 #[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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 let interceptor = ObservabilityDetectionInterceptor::new();
491 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
492
493 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 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 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 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 let interceptor_new = ObservabilityDetectionInterceptor::new();
558 let interceptor_default = ObservabilityDetectionInterceptor::default();
559
560 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
561
562 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 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 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}