Skip to main content

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

1//! Extended instrumentation for S3 `GetObject` operations.
2//!
3//! Provides additional methods to allow instrumenting the full operation, including response body transfer:
4//! - [`InstrumentedFluentBuilder::collect()`] — buffers the entire body into [`AggregatedBytes`]
5//! - [`InstrumentedFluentBuilder::stream()`] — yields body chunks via [`InstrumentedByteStream`]
6use bytes::Bytes;
7use futures_util::Stream;
8use pin_project_lite::pin_project;
9use std::{
10    pin::Pin,
11    task::{Context, Poll},
12};
13
14use crate::{
15    future::InstrumentedFutureContext,
16    middleware::aws::{
17        AwsSpan, InstrumentedFluentBuilder, InstrumentedFluentBuilderOutput,
18    },
19    semconv,
20};
21use aws_sdk_s3::{
22    error::SdkError,
23    operation::get_object::{GetObjectError, builders::GetObjectFluentBuilder},
24};
25use aws_smithy_types::byte_stream::{
26    AggregatedBytes, ByteStream, error::Error as ByteStreamError,
27};
28use aws_types::request_id::RequestId;
29use opentelemetry::{KeyValue, trace::Status};
30
31/// Error returned by [`InstrumentedFluentBuilder::collect`] on a [`GetObjectFluentBuilder`].
32#[derive(thiserror::Error, Debug)]
33pub enum GetObjectCollectError {
34    /// The S3 `GetObject` request failed.
35    #[error(transparent)]
36    SdkError(#[from] Box<SdkError<GetObjectError>>),
37
38    /// Reading the response body stream failed.
39    #[error(transparent)]
40    ByteStreamError(#[from] ByteStreamError),
41}
42
43pin_project! {
44    /// An instrumented S3 `GetObject` response body stream.
45    ///
46    /// Created by [`InstrumentedFluentBuilder::stream()`] on a [`GetObjectFluentBuilder`].
47    /// Implements [`Stream`]`<Item = Result<`[`Bytes`]`, `[`ByteStreamError`]`>>`, yielding the
48    /// response body chunk by chunk.
49    ///
50    /// The associated span ends when the stream is exhausted or an error is encountered.
51    /// If the stream is dropped before completion, the span ends via `Drop` with no explicit
52    /// status set (remains `Status::Unset`).
53    pub struct InstrumentedByteStream {
54        #[pin]
55        inner: ByteStream,
56        span: Option<AwsSpan>,
57    }
58}
59
60impl InstrumentedByteStream {
61    fn new(body: ByteStream, span: AwsSpan) -> Self {
62        Self {
63            inner: body,
64            span: Some(span),
65        }
66    }
67}
68
69impl Stream for InstrumentedByteStream {
70    type Item = Result<Bytes, ByteStreamError>;
71
72    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
73        let this = self.project();
74        match this.inner.poll_next(cx) {
75            Poll::Ready(None) => {
76                if let Some(mut span) = this.span.take() {
77                    span.set_status(Status::Ok);
78                }
79                Poll::Ready(None)
80            }
81            Poll::Ready(Some(Err(err))) => {
82                if let Some(mut span) = this.span.take() {
83                    span.record_error(&err);
84                    span.set_status(Status::error(err.to_string()));
85                }
86                Poll::Ready(Some(Err(err)))
87            }
88            other => other,
89        }
90    }
91}
92
93impl InstrumentedFluentBuilder<'_, GetObjectFluentBuilder> {
94    /// Sends the `GetObject` request and collects the full response body as [`AggregatedBytes`].
95    ///
96    /// This method instruments the **entire operation** — both the SDK call and the
97    /// subsequent body transfer — within a single span. It provides accurate timing
98    /// for the complete download, not just getting the initial response headers.
99    ///
100    /// Use this instead of calling [`InstrumentedFluentBuilder::send()`] followed by `body.collect()`
101    /// when you need full tracing coverage. If you need the entire [`GetObjectOutput`] object
102    /// (e.g. to inspect response headers or metadata), use `.send()` directly,
103    /// but be aware that the span will not cover body transfer in that case.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`GetObjectCollectError::SdkError`] if the HTTP request fails, or
108    /// [`GetObjectCollectError::ByteStreamError`] if reading the response body fails.
109    ///
110    /// # Example
111    ///
112    /// ```rust
113    /// use aws_sdk_s3::Client as S3Client;
114    /// use telemetry_rust::middleware::aws::AwsBuilderInstrument;
115    ///
116    /// async fn download_object(s3_client: &S3Client) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
117    ///     let body = s3_client
118    ///         .get_object()
119    ///         .bucket("my_bucket")
120    ///         .key("my_key")
121    ///         .instrument()
122    ///         .collect()
123    ///         .await?;
124    ///     Ok(body.to_vec())
125    /// }
126    /// ```
127    ///
128    /// [`GetObjectOutput`]: aws_sdk_s3::operation::get_object::GetObjectOutput
129    pub async fn collect(self) -> Result<AggregatedBytes, GetObjectCollectError> {
130        let mut span = self.span.start();
131        span.set_attribute(KeyValue::new("aws.s3.body.mode", "collect"));
132
133        let result = self.inner.send().await;
134        let Ok(output) = result else {
135            span.on_result(&result);
136            return Err(Box::new(result.unwrap_err()).into());
137        };
138
139        if let Some(value) = output.request_id() {
140            span.set_attribute(KeyValue::new(semconv::AWS_REQUEST_ID, value.to_owned()));
141        }
142
143        span.set_attributes(output.extract_attributes());
144        match output.body.collect().await {
145            Ok(body) => {
146                span.set_status(Status::Ok);
147                Ok(body)
148            }
149            Err(err) => {
150                span.record_error(&err);
151                span.set_status(Status::error(err.to_string()));
152                Err(err.into())
153            }
154        }
155    }
156
157    /// Sends the `GetObject` request and returns an instrumented stream of the response body.
158    ///
159    /// Unlike [`collect`][Self::collect], this method does not buffer the body in memory.
160    /// Instead it returns an [`InstrumentedByteStream`] that yields chunks as they arrive.
161    /// The span covers the **entire operation** — both the SDK call and the body transfer —
162    /// ending only when the stream is exhausted or an error occurs.
163    ///
164    /// If you need the full body in memory, prefer [`collect`][Self::collect].
165    /// If you need the raw [`GetObjectOutput`] or [`ByteStream`],
166    /// use `.send()` directly, but be aware the span will not cover body transfer.
167    ///
168    /// # Errors
169    ///
170    /// Returns `Err` if the `GetObject` request itself fails. Body transfer errors surface
171    /// as [`Err`] items yielded by the returned stream.
172    ///
173    /// # Example
174    ///
175    /// ```rust
176    /// use aws_sdk_s3::Client as S3Client;
177    /// use futures_util::TryStreamExt;
178    /// use telemetry_rust::middleware::aws::AwsBuilderInstrument;
179    ///
180    /// async fn stream_object(s3_client: &S3Client) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
181    ///     let mut stream = s3_client
182    ///         .get_object()
183    ///         .bucket("my_bucket")
184    ///         .key("my_key")
185    ///         .instrument()
186    ///         .stream()
187    ///         .await?;
188    ///
189    ///     let mut body = Vec::new();
190    ///     while let Some(chunk) = stream.try_next().await? {
191    ///         body.extend_from_slice(&chunk);
192    ///     }
193    ///     Ok(body)
194    /// }
195    /// ```
196    ///
197    /// [`GetObjectOutput`]: aws_sdk_s3::operation::get_object::GetObjectOutput
198    /// [`ByteStream`]: aws_smithy_types::byte_stream::ByteStream
199    pub async fn stream(
200        self,
201    ) -> Result<InstrumentedByteStream, SdkError<GetObjectError>> {
202        let mut span = self.span.start();
203        span.set_attribute(KeyValue::new("aws.s3.body.mode", "stream"));
204
205        let result = self.inner.send().await;
206        let Ok(output) = result else {
207            span.on_result(&result);
208            return Err(result.unwrap_err());
209        };
210
211        if let Some(value) = output.request_id() {
212            span.set_attribute(KeyValue::new(semconv::AWS_REQUEST_ID, value.to_owned()));
213        }
214
215        span.set_attributes(output.extract_attributes());
216        Ok(InstrumentedByteStream::new(output.body, span))
217    }
218}