Skip to main content

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