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;