telemetry_rust/middleware/aws/mod.rs
1//! Instrumentation utilities for AWS SDK operations.
2//!
3//! This module provides comprehensive instrumentation for AWS services,
4//! including automatic instrumentation and a low-level API for manual span creation.
5//! It supports both individual AWS SDK operations and streaming/pagination.
6//!
7//! # Features
8//!
9//! - **Span Creation**: Manual span creation with [`AwsSpan`] and [`AwsSpanBuilder`]
10//! - **Instrumentation**: Automatic instrumentation for AWS SDK operations with [`AwsInstrument`] trait
11//! - **Stream Instrumentation**: Automatic instrumentation for AWS [`PaginationStream`](`aws_smithy_async::future::pagination_stream::PaginationStream`) with [`AwsStreamInstrument`] trait
12//!
13//! # Feature Flags
14//!
15//! - `aws-instrumentation`: Enables [`Future`] instrumentation via [`AwsInstrument`] trait
16//! - `aws-stream-instrumentation`: Enables [`Stream`][`futures_util::Stream`] instrumentation via [`AwsStreamInstrument`] trait
17
18use aws_types::request_id::RequestId;
19use opentelemetry::{
20 global::{self, BoxedSpan, BoxedTracer},
21 trace::{Span as _, SpanBuilder, SpanKind, Status, Tracer},
22};
23use std::error::Error;
24use tracing::Span;
25
26use crate::{Context, KeyValue, OpenTelemetrySpanExt, StringValue, semconv};
27
28mod instrumentation;
29mod operations;
30
31pub use instrumentation::*;
32pub use operations::*;
33
34/// A wrapper around an OpenTelemetry span specifically designed for AWS operations.
35///
36/// This struct represents an active span for an AWS SDK operation.
37/// It provides convenient methods for setting span attributes and recording
38/// AWS operation status upon its completion, including AWS request ID and optional error.
39///
40/// # Usage
41///
42/// `AwsSpan` can be used for manual instrumentation when you need fine-grained
43/// control over span lifecycle. But for most use cases, consider using the higher-level
44/// traits like [`AwsInstrument`] or [`AwsBuilderInstrument`] which provide automatic
45/// instrumentation.
46///
47/// Should be constructed using [`AwsSpanBuilder`] by calling [`AwsSpanBuilder::start`].
48///
49/// # Example
50///
51/// ```rust
52/// use aws_sdk_dynamodb::{Client as DynamoClient, types::AttributeValue};
53/// use telemetry_rust::{KeyValue, middleware::aws::DynamodbSpanBuilder, semconv};
54///
55/// async fn query_table() -> Result<i32, Box<dyn std::error::Error>> {
56/// let config = aws_config::load_from_env().await;
57/// let dynamo_client = DynamoClient::new(&config);
58///
59/// // Create and start a span manually
60/// let mut span = DynamodbSpanBuilder::query("table_name")
61/// .attribute(KeyValue::new(semconv::AWS_DYNAMODB_INDEX_NAME, "my_index"))
62/// .start();
63///
64/// let response = dynamo_client
65/// .query()
66/// .table_name("table_name")
67/// .index_name("my_index")
68/// .key_condition_expression("PK = :pk")
69/// .expression_attribute_values(":pk", AttributeValue::S("Test".to_string()))
70/// .send()
71/// .await;
72///
73/// // Add attributes from response
74/// if let Some(output) = response.as_ref().ok() {
75/// let count = output.count() as i64;
76/// let scanned_count = output.scanned_count() as i64;
77/// span.set_attributes([
78/// KeyValue::new(semconv::AWS_DYNAMODB_COUNT, count),
79/// KeyValue::new(semconv::AWS_DYNAMODB_SCANNED_COUNT, scanned_count),
80/// ]);
81/// }
82///
83/// // The span automatically handles success/error and request ID extraction
84/// span.end(&response);
85///
86/// let response = response?;
87/// println!("DynamoDB items: {:#?}", response.items());
88/// Ok(response.count())
89/// }
90/// ```
91pub struct AwsSpan {
92 span: BoxedSpan,
93}
94
95impl AwsSpan {
96 /// Ends the span with AWS response information.
97 ///
98 /// This method finalizes the span by recording the outcome of an AWS operation.
99 /// It automatically extracts request IDs and handles error reporting.
100 ///
101 /// # Arguments
102 ///
103 /// * `aws_response` - The result of the AWS operation, which must implement
104 /// `RequestId` for both success and error cases
105 ///
106 /// # Behavior
107 ///
108 /// - On success: Sets span status to OK and records the request ID
109 /// - On error: Records the error, sets error status, and records the request ID if available
110 pub fn end<T, E>(self, aws_response: &Result<T, E>)
111 where
112 T: RequestId,
113 E: RequestId + Error,
114 {
115 let mut span = self.span;
116 let (status, request_id) = match aws_response {
117 Ok(resp) => (Status::Ok, resp.request_id()),
118 Err(error) => {
119 span.record_error(&error);
120 (Status::error(error.to_string()), error.request_id())
121 }
122 };
123 if let Some(value) = request_id {
124 span.set_attribute(KeyValue::new(semconv::AWS_REQUEST_ID, value.to_owned()));
125 }
126 span.set_status(status);
127 }
128
129 /// Sets a single attribute on the span.
130 ///
131 /// This method allows you to add custom attributes to the span after it has been created.
132 /// This is useful for adding dynamic attributes that become available during operation execution.
133 ///
134 /// For more information see [`BoxedSpan::set_attribute`]
135 ///
136 /// # Arguments
137 ///
138 /// * `attribute` - The [`KeyValue`] attribute to add to the span
139 ///
140 /// # Example
141 ///
142 /// ```rust
143 /// use telemetry_rust::{KeyValue, middleware::aws::AwsSpanBuilder};
144 ///
145 /// let mut span = AwsSpanBuilder::client("DynamoDB", "GetItem", []).start();
146 /// span.set_attribute(KeyValue::new("custom.attribute", "value"));
147 /// ```
148 pub fn set_attribute(&mut self, attribute: KeyValue) {
149 self.span.set_attribute(attribute);
150 }
151
152 /// Sets multiple attributes on the span.
153 ///
154 /// This method allows you to add multiple custom attributes to the span at once.
155 /// This is more efficient than calling `set_attribute` multiple times.
156 ///
157 /// For more information see [`BoxedSpan::set_attributes`]
158 ///
159 /// # Arguments
160 ///
161 /// * `attributes` - An iterator of [`KeyValue`] attributes to add to the span
162 ///
163 /// # Example
164 ///
165 /// ```rust
166 /// use telemetry_rust::{KeyValue, middleware::aws::AwsSpanBuilder, semconv};
167 ///
168 /// let mut span = AwsSpanBuilder::client("DynamoDB", "GetItem", []).start();
169 /// span.set_attributes([
170 /// KeyValue::new(semconv::DB_NAMESPACE, "my_table"),
171 /// KeyValue::new("custom.attribute", "value"),
172 /// ]);
173 /// ```
174 pub fn set_attributes(&mut self, attributes: impl IntoIterator<Item = KeyValue>) {
175 self.span.set_attributes(attributes);
176 }
177}
178
179impl From<BoxedSpan> for AwsSpan {
180 #[inline]
181 fn from(span: BoxedSpan) -> Self {
182 Self { span }
183 }
184}
185
186/// Builder for creating AWS-specific OpenTelemetry spans.
187///
188/// This builder provides a fluent interface for constructing [`AwsSpan`] with
189/// required attributes and proper span kinds for different types of AWS operations.
190/// It automatically sets standard RPC attributes following OpenTelemetry semantic
191/// conventions for AWS services.
192///
193/// # Usage
194///
195/// This builder can be used with [`AwsInstrument`] trait to instrument any AWS operation,
196/// or to manually create [`AwsSpan`] if you need control over span lifecycle.
197/// For automatic instrumentation, use [`AwsBuilderInstrument`] trait.
198pub struct AwsSpanBuilder<'a> {
199 inner: SpanBuilder,
200 tracer: BoxedTracer,
201 context: Option<&'a Context>,
202}
203
204impl<'a> AwsSpanBuilder<'a> {
205 fn new(
206 span_kind: SpanKind,
207 service: impl Into<StringValue>,
208 method: impl Into<StringValue>,
209 custom_attributes: impl IntoIterator<Item = KeyValue>,
210 ) -> Self {
211 let service: StringValue = service.into();
212 let method: StringValue = method.into();
213 let tracer = global::tracer("aws_sdk");
214 let span_name = format!("{service}.{method}");
215 let mut attributes = vec![
216 KeyValue::new(semconv::RPC_METHOD, method),
217 KeyValue::new(semconv::RPC_SYSTEM, "aws-api"),
218 KeyValue::new(semconv::RPC_SERVICE, service),
219 ];
220 attributes.extend(custom_attributes);
221 let inner = tracer
222 .span_builder(span_name)
223 .with_attributes(attributes)
224 .with_kind(span_kind);
225
226 Self {
227 inner,
228 tracer,
229 context: None,
230 }
231 }
232
233 /// Creates a client span builder for AWS operations.
234 ///
235 /// Client spans represent outbound calls to AWS services from your application.
236 ///
237 /// # Arguments
238 ///
239 /// * `service` - The AWS service name (e.g., "S3", "DynamoDB")
240 /// * `method` - The operation name (e.g., "GetObject", "PutItem")
241 /// * `attributes` - Additional custom attributes for the span
242 pub fn client(
243 service: impl Into<StringValue>,
244 method: impl Into<StringValue>,
245 attributes: impl IntoIterator<Item = KeyValue>,
246 ) -> Self {
247 Self::new(SpanKind::Client, service, method, attributes)
248 }
249
250 /// Creates a producer span builder for AWS operations.
251 ///
252 /// Producer spans represent operations that send messages or data to AWS services.
253 ///
254 /// # Arguments
255 ///
256 /// * `service` - The AWS service name (e.g., "SQS", "SNS")
257 /// * `method` - The operation name (e.g., "SendMessage", "Publish")
258 /// * `attributes` - Additional custom attributes for the span
259 pub fn producer(
260 service: impl Into<StringValue>,
261 method: impl Into<StringValue>,
262 attributes: impl IntoIterator<Item = KeyValue>,
263 ) -> Self {
264 Self::new(SpanKind::Producer, service, method, attributes)
265 }
266
267 /// Creates a consumer span builder for AWS operations.
268 ///
269 /// Consumer spans represent operations that receive messages or data from AWS services.
270 ///
271 /// # Arguments
272 ///
273 /// * `service` - The AWS service name (e.g., "SQS", "Kinesis")
274 /// * `method` - The operation name (e.g., "ReceiveMessage", "GetRecords")
275 /// * `attributes` - Additional custom attributes for the span
276 pub fn consumer(
277 service: impl Into<StringValue>,
278 method: impl Into<StringValue>,
279 attributes: impl IntoIterator<Item = KeyValue>,
280 ) -> Self {
281 Self::new(SpanKind::Consumer, service, method, attributes)
282 }
283
284 /// Adds multiple attributes to the span being built.
285 ///
286 /// # Arguments
287 ///
288 /// * `iter` - An iterator of [`KeyValue`] attributes to add to the span
289 pub fn attributes(mut self, iter: impl IntoIterator<Item = KeyValue>) -> Self {
290 if let Some(attributes) = &mut self.inner.attributes {
291 attributes.extend(iter);
292 }
293 self
294 }
295
296 /// Adds a single attribute to the span being built.
297 ///
298 /// This is a convenience method for adding one attribute at a time.
299 ///
300 /// # Arguments
301 ///
302 /// * `attribute` - The [`KeyValue`] attribute to add to the span
303 #[inline]
304 pub fn attribute(self, attribute: KeyValue) -> Self {
305 self.attributes(std::iter::once(attribute))
306 }
307
308 /// Sets the parent [`Context`] for the span.
309 ///
310 /// # Arguments
311 ///
312 /// * `context` - The OpenTelemetry [`Context`] to use as the parent
313 #[inline]
314 pub fn context(mut self, context: &'a Context) -> Self {
315 self.context = Some(context);
316 self
317 }
318
319 /// Optionally sets the parent [`Context`] for the span.
320 ///
321 /// # Arguments
322 ///
323 /// * `context` - An optional OpenTelemetry [`Context`] to use as the parent
324 #[inline]
325 pub fn set_context(mut self, context: Option<&'a Context>) -> Self {
326 self.context = context;
327 self
328 }
329
330 #[inline(always)]
331 fn start_with_context(self, parent_cx: &Context) -> AwsSpan {
332 self.inner
333 .start_with_context(&self.tracer, parent_cx)
334 .into()
335 }
336
337 /// Starts the span and returns an [`AwsSpan`].
338 ///
339 /// This method creates and starts the span using either the explicitly set context
340 /// or the current tracing span's context as the parent.
341 #[inline]
342 pub fn start(self) -> AwsSpan {
343 match self.context {
344 Some(context) => self.start_with_context(context),
345 None => self.start_with_context(&Span::current().context()),
346 }
347 }
348}