Skip to main content

telemetry_rust/middleware/aws/instrumentation/fluent_builder/
mod.rs

1use crate::{Context, future::InstrumentedFutureContext, middleware::aws::*};
2use aws_smithy_types::error::metadata::ProvideErrorMetadata;
3
4mod utils;
5
6#[cfg(feature = "aws-appconfigdata")]
7mod appconfigdata;
8#[cfg(feature = "aws-dynamodb")]
9mod dynamodb;
10#[cfg(feature = "aws-firehose")]
11mod firehose;
12#[cfg(feature = "aws-s3")]
13mod s3;
14#[cfg(feature = "aws-sagemaker-runtime")]
15mod sagemaker_runtime;
16#[cfg(feature = "aws-secretsmanager")]
17mod secretsmanager;
18#[cfg(feature = "aws-sns")]
19mod sns;
20#[cfg(feature = "aws-sqs")]
21mod sqs;
22#[cfg(feature = "aws-ssm")]
23mod ssm;
24
25/// A trait for AWS service clients that can be instrumented with OpenTelemetry tracing.
26///
27/// This trait provides methods to build spans for AWS operations and instrument the
28/// fluent builders returned by AWS SDK operations. The instrumentation automatically
29/// extracts both input attributes (from the fluent builder configuration) and output
30/// attributes (from the operation response) following OpenTelemetry semantic conventions.
31///
32/// # Example
33///
34/// ```rust
35/// use aws_sdk_dynamodb::{Client as DynamoClient, types::AttributeValue};
36/// use telemetry_rust::middleware::aws::AwsBuilderInstrument;
37///
38/// async fn query_table() -> Result<i32, Box<dyn std::error::Error>> {
39///     let config = aws_config::load_from_env().await;
40///     let dynamo_client = DynamoClient::new(&config);
41///
42///     let resp = dynamo_client
43///         .query()
44///         .table_name("table_name")
45///         .index_name("my_index")
46///         .key_condition_expression("PK = :pk")
47///         .expression_attribute_values(":pk", AttributeValue::S("Test".to_string()))
48///         .consistent_read(true)
49///         .projection_expression("id,name")
50///         .instrument()
51///         .send()
52///         .await?;
53///
54///     // Automatically extracts span attributes from the builder:
55///     // - aws.dynamodb.table_name: "table_name"
56///     // - aws.dynamodb.index_name: "my_index"
57///     // - aws.dynamodb.consistent_read: true
58///     // - aws.dynamodb.projection: "id,name"
59///     //
60///     // And from the AWS output:
61///     // - aws.dynamodb.count: number of items returned
62///     // - aws.dynamodb.scanned_count: number of items scanned
63///
64///     println!("DynamoDB items: {:#?}", resp.items());
65///     Ok(resp.count())
66/// }
67/// ```
68///
69/// # Comparison with Manual Instrumentation
70///
71/// This trait provides automatic instrumentation as an alternative to manual instrumentation
72/// using [`AwsInstrument`]. The automatic approach extracts attributes based on OpenTelemetry
73/// semantic conventions without requiring explicit attribute specification:
74///
75/// ```rust
76/// # use aws_sdk_dynamodb::Client as DynamoClient;
77/// # use telemetry_rust::middleware::aws::{AwsBuilderInstrument, AwsInstrument, DynamodbSpanBuilder};
78/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
79/// # let config = aws_config::load_from_env().await;
80/// # let dynamo_client = DynamoClient::new(&config);
81/// // Automatic instrumentation (recommended)
82/// let _ = dynamo_client
83///     .get_item()
84///     .table_name("table")
85///     .instrument() // All attributes extracted automatically
86///     .send()
87///     .await?;
88///
89/// // Manual instrumentation (more control, more verbose)
90/// let _ = dynamo_client
91///     .get_item()
92///     .table_name("table")
93///     .send()
94///     .instrument(DynamodbSpanBuilder::get_item("table"))
95///     .await?;
96/// # Ok(())
97/// # }
98/// ```
99pub trait AwsBuilderInstrument<'a>
100where
101    Self: Sized,
102{
103    /// Builds an AWS span for the specific operation represented by this builder.
104    ///
105    /// Returns an [`AwsSpanBuilder`] that contains the necessary span attributes
106    /// and metadata for the AWS operation.
107    fn build_aws_span(&self) -> AwsSpanBuilder<'a>;
108
109    /// Instruments this fluent builder with OpenTelemetry tracing.
110    ///
111    /// Returns an [`InstrumentedFluentBuilder`] that will automatically create
112    /// and manage spans when the operation is executed.
113    fn instrument(self) -> InstrumentedFluentBuilder<'a, Self> {
114        let span = self.build_aws_span();
115        InstrumentedFluentBuilder::new(self, span)
116    }
117}
118
119/// A wrapper that instruments AWS fluent builders with OpenTelemetry tracing.
120///
121/// This struct wraps AWS SDK fluent builders and automatically creates spans
122/// when operations are executed, providing distributed tracing capabilities
123/// for AWS service calls.
124pub struct InstrumentedFluentBuilder<'a, T: AwsBuilderInstrument<'a>> {
125    inner: T,
126    span: AwsSpanBuilder<'a>,
127}
128
129impl<'a, T: AwsBuilderInstrument<'a>> InstrumentedFluentBuilder<'a, T> {
130    /// Creates a new instrumented fluent builder.
131    ///
132    /// # Arguments
133    /// * `inner` - The AWS SDK fluent builder to wrap
134    /// * `span` - The span builder with AWS operation metadata
135    pub fn new(inner: T, span: AwsSpanBuilder<'a>) -> Self {
136        Self { inner, span }
137    }
138
139    /// Sets the OpenTelemetry context for this instrumented builder.
140    ///
141    /// # Arguments
142    /// * `context` - The OpenTelemetry context to use for span creation
143    pub fn context(mut self, context: &'a Context) -> Self {
144        self.span = self.span.context(context);
145        self
146    }
147
148    /// Sets the OpenTelemetry context for this instrumented builder.
149    ///
150    /// # Arguments
151    /// * `context` - Optional OpenTelemetry context to use for span creation
152    pub fn set_context(mut self, context: Option<&'a Context>) -> Self {
153        self.span = self.span.set_context(context);
154        self
155    }
156}
157
158pub(super) struct FluentBuilderSpan(AwsSpan);
159
160/// A trait for extracting OpenTelemetry attributes from AWS operation output objects.
161///
162/// This trait enables AWS SDK operation outputs to contribute additional telemetry data
163/// to spans after operations complete. It's automatically used by [`AwsBuilderInstrument`]
164/// to extract meaningful span attributes like item counts, error rates,
165/// and other response metadata that enhances observability.
166///
167/// # Usage
168///
169/// Can be used together with [`AwsBuilderInstrument::build_aws_span`] when access to the
170/// underlying [`AwsSpan`] is required.
171///
172/// # Example
173///
174/// ```rust
175/// use aws_sdk_dynamodb::{Client as DynamoClient, types::ReturnConsumedCapacity};
176/// use telemetry_rust::{
177///     KeyValue,
178///     middleware::aws::{AwsBuilderInstrument, InstrumentedFluentBuilderOutput},
179///     semconv,
180/// };
181///
182/// async fn query_table() -> Result<usize, Box<dyn std::error::Error>> {
183///     let config = aws_config::load_from_env().await;
184///     let dynamo_client = DynamoClient::new(&config);
185///
186///     let statement = "SELECT * FROM test";
187///     let query = dynamo_client
188///         .execute_statement()
189///         .statement(statement)
190///         .return_consumed_capacity(ReturnConsumedCapacity::Total);
191///
192///     let mut span = query
193///         // Extract span from fluent builder
194///         .build_aws_span()
195///         // Set additional attributes
196///         .attribute(KeyValue::new(semconv::DB_QUERY_TEXT, statement))
197///         // Start the span
198///         .start();
199///
200///     let result = query.send().await;
201///     if let Ok(output) = result.as_ref() {
202///         // Extract span attributes from ExecuteStatement output
203///         span.set_attributes(output.extract_attributes());
204///         // Set additional attributes
205///         span.set_attribute(KeyValue::new(
206///             semconv::AWS_DYNAMODB_CONSUMED_CAPACITY,
207///             format!("{:?}", output.consumed_capacity().unwrap()),
208///         ));
209///     }
210///     // End the span
211///     span.end(&result);
212///
213///     let items = result?.items.unwrap_or_default();
214///
215///     println!("DynamoDB items: {items:#?}");
216///     Ok(items.len())
217/// }
218/// ```
219///
220/// # Implementation Notes
221///
222/// Implementations should extract relevant attributes following OpenTelemetry semantic
223/// conventions for the specific AWS service. The extracted attributes will be added
224/// to the span after the operation completes successfully.
225pub trait InstrumentedFluentBuilderOutput {
226    /// Extracts OpenTelemetry attributes from the AWS operation output.
227    ///
228    /// Return an iterator of [`KeyValue`] pairs representing span attributes.
229    ///
230    /// The default implementation returns no attributes, which is appropriate for
231    /// operations that don't have meaningful response metrics to extract.
232    fn extract_attributes(&self) -> impl IntoIterator<Item = KeyValue> {
233        None
234    }
235}
236
237impl<T, E> InstrumentedFutureContext<Result<T, E>> for FluentBuilderSpan
238where
239    T: RequestId + InstrumentedFluentBuilderOutput,
240    E: RequestId + ProvideErrorMetadata + Error,
241{
242    fn on_result(mut self, result: &Result<T, E>) {
243        if let Ok(output) = result {
244            self.0.set_attributes(output.extract_attributes());
245        }
246        self.0.on_result(result)
247    }
248}
249
250/// Generates [`super::InstrumentedFluentBuilder`] implementation for AWS SDK operations.
251macro_rules! instrument_aws_operation {
252    ($sdk:ident::operation::$op:ident, $builder:ident, $output:ident, $error:ident) => {
253        use $sdk::operation::$op::builders::$builder;
254        use $sdk::operation::$op::$output;
255        impl
256            super::InstrumentedFluentBuilder<'_, $sdk::operation::$op::builders::$builder>
257        {
258            /// Executes the AWS operation with instrumentation.
259            ///
260            /// This method creates a span for the operation and executes it within
261            /// that span context, providing automatic distributed tracing.
262            pub async fn send(
263                self,
264            ) -> Result<
265                $sdk::operation::$op::$output,
266                $sdk::error::SdkError<$sdk::operation::$op::$error>,
267            > {
268                let span = self.span.start();
269                $crate::future::InstrumentedFuture::new(
270                    self.inner.send(),
271                    super::FluentBuilderSpan(span),
272                )
273                .await
274            }
275        }
276    };
277    ($sdk:ident::operation::$op:ident) => {
278        paste::paste! {
279            instrument_aws_operation!(
280                $sdk::operation::$op,
281                [<$op:camel FluentBuilder>],
282                [<$op:camel Output>],
283                [<$op:camel Error>]
284            );
285        }
286    };
287}
288
289pub(super) use instrument_aws_operation;