xray_lite_aws_sdk/
lib.rs

1#![warn(missing_docs)]
2
3//! Extension of `xray-lite` for [AWS SDK for Rust](https://aws.amazon.com/sdk-for-rust/).
4//!
5//! With this crate, you can easily add the X-Ray tracing capability to your
6//! AWS service requests through
7//! [AWS SDK for Rust](https://aws.amazon.com/sdk-for-rust/).
8//! It utilizes the [interceptor](https://docs.rs/aws-smithy-runtime-api/latest/aws_smithy_runtime_api/client/interceptors/trait.Intercept.html)
9//! which can be attached to `CustomizableOperation` available via the
10//! `customize` method of any request builder; e.g.,
11//! [`aws_sdk_s3::operation::get_object::builders::GetObjectFluentBuilder::customize`](https://docs.rs/aws-sdk-s3/latest/aws_sdk_s3/operation/get_object/builders/struct.GetObjectFluentBuilder.html#method.customize)
12//!
13//! The following example shows how to report a subsegment for each attempt of
14//! the S3 GetObject operation:
15//! ```no_run
16//! use aws_config::BehaviorVersion;
17//! use xray_lite::{DaemonClient, SubsegmentContext};
18//! use xray_lite_aws_sdk::ContextExt as _;
19//!
20//! async fn get_object_from_s3() {
21//!     let xray_client = DaemonClient::from_lambda_env().unwrap();
22//!     let xray_context = SubsegmentContext::from_lambda_env(xray_client).unwrap();
23//!
24//!     let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
25//!     let s3_client = aws_sdk_s3::Client::new(&config);
26//!     s3_client
27//!         .get_object()
28//!         .bucket("the-bucket-name")
29//!         .key("the-object-key")
30//!         .customize()
31//!         .interceptor(xray_context.intercept_operation("S3", "GetObject"))
32//!         .send()
33//!         .await
34//!         .unwrap();
35//! }
36//! ```
37
38use std::sync::{Arc, Mutex};
39
40use aws_smithy_runtime_api::box_error::BoxError;
41use aws_smithy_runtime_api::client::interceptors::context::{
42    BeforeTransmitInterceptorContextMut, BeforeTransmitInterceptorContextRef,
43    FinalizerInterceptorContextRef,
44};
45use aws_smithy_runtime_api::client::interceptors::Intercept;
46use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
47use aws_smithy_types::config_bag::ConfigBag;
48use aws_types::request_id::RequestId;
49
50use xray_lite::{AwsNamespace, Context, Header, SubsegmentSession};
51
52/// Extension of [`Context`].
53///
54/// This trait is automatically implemented for any [`Context`] that satisfies
55/// the bounds.
56pub trait ContextExt: Context + Clone + std::fmt::Debug + Send + Sync + 'static {
57    /// Creates an [`Intercept`](https://docs.rs/aws-smithy-runtime-api/1.1.7/aws_smithy_runtime_api/client/interceptors/trait.Intercept.html)
58    /// for the AWS service request.
59    ///
60    /// A returned `Intercept` implements the following hooks:
61    /// 1. [`read_before_attempt`](https://docs.rs/aws-smithy-runtime-api/1.1.7/aws_smithy_runtime_api/client/interceptors/trait.Intercept.html#method.read_before_attempt):
62    ///    Starts a subsegment of the AWS service request
63    /// 2. [`modify_before_transmit`](https://docs.rs/aws-smithy-runtime-api/1.1.7/aws_smithy_runtime_api/client/interceptors/trait.Intercept.html#method.modify_before_transmit):
64    ///    Injects the `X-Amzn-Trace-Id` header into the request
65    /// 3. [`read_after_attempt`](https://docs.rs/aws-smithy-runtime-api/1.1.7/aws_smithy_runtime_api/client/interceptors/trait.Intercept.html#method.read_after_attempt):
66    ///    Updates the subsegment with the request ID and the response status,
67    ///    and reports the subsegment to the X-Ray daemon
68    fn intercept_operation(
69        &self,
70        service: impl Into<String>,
71        operation: impl Into<String>,
72    ) -> impl Intercept + 'static {
73        XrayIntercept::new_with_operation(self.clone(), service, operation)
74    }
75}
76
77impl<T> ContextExt for T where T: Context + Clone + std::fmt::Debug + Send + Sync + 'static {}
78
79#[derive(Debug)]
80struct XrayIntercept<T>
81where
82    T: Context + Clone + std::fmt::Debug + Send + Sync + 'static,
83{
84    context: T,
85    service: String,
86    operation: String,
87    // session is unnecessarily wrapped in Mutex because `Intercept` is
88    // immutable during its method calls.
89    #[allow(clippy::type_complexity)]
90    session: Arc<Mutex<Option<SubsegmentSession<T::Client, AwsNamespace>>>>,
91}
92
93impl<T> XrayIntercept<T>
94where
95    T: Context + Clone + std::fmt::Debug + Send + Sync + 'static,
96{
97    fn new_with_operation(
98        context: T,
99        service: impl Into<String>,
100        operation: impl Into<String>,
101    ) -> Self {
102        Self {
103            context,
104            service: service.into(),
105            operation: operation.into(),
106            session: Arc::new(Mutex::new(None)),
107        }
108    }
109}
110
111impl<T> Intercept for XrayIntercept<T>
112where
113    T: Context + Clone + std::fmt::Debug + Send + Sync + 'static,
114{
115    fn name(&self) -> &'static str {
116        "XrayIntercept"
117    }
118
119    fn read_before_attempt(
120        &self,
121        _context: &BeforeTransmitInterceptorContextRef<'_>,
122        _runtime_components: &RuntimeComponents,
123        _cfg: &mut ConfigBag,
124    ) -> Result<(), BoxError> {
125        let session = self.context.enter_subsegment(AwsNamespace::new(
126            self.service.clone(),
127            self.operation.clone(),
128        ));
129        *self.session.lock().unwrap() = Some(session);
130        Ok(())
131    }
132
133    fn modify_before_transmit(
134        &self,
135        context: &mut BeforeTransmitInterceptorContextMut<'_>,
136        _runtime_components: &RuntimeComponents,
137        _cfg: &mut ConfigBag,
138    ) -> Result<(), BoxError> {
139        let trace_id = self
140            .session
141            .lock()
142            .unwrap()
143            .as_ref()
144            .and_then(|s| s.x_amzn_trace_id());
145        if let Some(trace_id) = trace_id {
146            context
147                .request_mut()
148                .headers_mut()
149                .insert(Header::NAME, trace_id);
150        }
151        Ok(())
152    }
153
154    fn read_after_attempt(
155        &self,
156        context: &FinalizerInterceptorContextRef<'_>,
157        _runtime_components: &RuntimeComponents,
158        _cfg: &mut ConfigBag,
159    ) -> Result<(), BoxError> {
160        let mut session = self.session.lock().unwrap();
161        if let Some(mut session) = session.take() {
162            if let Some(namespace) = session.namespace_mut() {
163                if let Some(response) = context.response() {
164                    namespace.response_status(response.status().as_u16());
165                    if let Some(request_id) = response.request_id() {
166                        namespace.request_id(request_id);
167                    }
168                }
169            }
170        }
171        Ok(())
172    }
173}