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}