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

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