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