reqwest_streams/observability.rs
1//! Observability for streaming responses.
2//!
3//! The accounting itself lives in [`http_streams_core`] and is shared with `axum-streams`,
4//! which had grown an identical implementation independently. What stays here is
5//! [`ReqwestStreamOptions`] — it carries decode-side fields that mean nothing on the encode
6//! side, and its inherent builder methods could not be added to a type defined elsewhere
7//! (E0116) — plus the one thing core cannot see: a [`reqwest::Response`].
8
9use http_streams_core::{Direction, Progress, ProgressOptions, Side, StreamContext};
10use std::sync::Arc;
11use std::time::Duration;
12
13pub use http_streams_core::{
14 StreamErrorHandler as ReqwestStreamErrorHandler, StreamOutcome as ReqwestStreamOutcome,
15 StreamProgress as ReqwestStreamProgress, StreamProgressHandler as ReqwestStreamProgressHandler,
16};
17
18use crate::StreamBodyError;
19
20/// The default read-buffer size, 8 KiB.
21pub(crate) const INITIAL_CAPACITY: usize = http_streams_core::DEFAULT_BUF_CAPACITY;
22
23const DEFAULT_PROGRESS_INTERVAL: Duration = http_streams_core::DEFAULT_PROGRESS_INTERVAL;
24
25
26/// Options shared by every streaming format.
27///
28/// Build these with [`ReqwestStreamOptions::new`] and the setters below rather than with a
29/// struct literal, so that later options can be added without breaking you.
30///
31/// # Note on `max_obj_len`
32///
33/// Unlike the positional-argument methods, which make you choose a limit, a freshly built
34/// `ReqwestStreamOptions` does **not** limit object size — [`max_obj_len`] defaults to
35/// [`usize::MAX`]. Set it explicitly when reading from a source you do not control.
36///
37/// [`max_obj_len`]: ReqwestStreamOptions::max_obj_len
38#[non_exhaustive]
39pub struct ReqwestStreamOptions {
40 pub max_obj_len: usize,
41 pub buf_capacity: usize,
42 pub on_error: Option<ReqwestStreamErrorHandler>,
43 pub on_progress: Option<ReqwestStreamProgressHandler>,
44 pub progress_interval: Option<Duration>,
45 pub progress_items: Option<u64>,
46}
47
48impl Default for ReqwestStreamOptions {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl ReqwestStreamOptions {
55 pub fn new() -> Self {
56 Self {
57 max_obj_len: usize::MAX,
58 buf_capacity: INITIAL_CAPACITY,
59 on_error: None,
60 on_progress: None,
61 progress_interval: Some(DEFAULT_PROGRESS_INTERVAL),
62 progress_items: None,
63 }
64 }
65
66 /// The maximum size in bytes of a single decoded object.
67 ///
68 /// [`usize::MAX`], the default, means no limit.
69 pub fn max_obj_len(mut self, max_obj_len: usize) -> Self {
70 self.max_obj_len = max_obj_len;
71 self
72 }
73
74 /// The initial capacity of the stream's decoding buffer.
75 pub fn buf_capacity(mut self, buf_capacity: usize) -> Self {
76 self.buf_capacity = buf_capacity;
77 self
78 }
79
80 /// Registers a callback invoked for every error produced while reading the response,
81 /// covering both transport errors and decoding errors produced by the format itself.
82 ///
83 /// The error is still yielded by the stream; this is purely an observation hook. It does
84 /// not replace the `tracing` feature: when that feature is enabled both the log event and
85 /// this callback fire.
86 pub fn on_error<F>(mut self, handler: F) -> Self
87 where
88 F: Fn(&StreamBodyError) + Send + Sync + 'static,
89 {
90 self.on_error = Some(Arc::new(handler));
91 self
92 }
93
94 /// Registers a callback receiving progress snapshots while the response is read: one per
95 /// reporting interval or item step, plus a final one carrying the totals and how the
96 /// stream ended (completed, failed, or aborted because the consumer stopped reading).
97 ///
98 /// This is the same accounting the `tracing` feature reports, exposed for metrics: wire it
99 /// to a counter and you get streamed items and bytes without depending on tracing at all.
100 /// When the feature is enabled both happen.
101 ///
102 /// The counters are only maintained when someone is listening, so a stream with no
103 /// callback and no `tracing` subscriber interested in `reqwest_streams` pays nothing.
104 pub fn on_progress<F>(mut self, handler: F) -> Self
105 where
106 F: Fn(&ReqwestStreamProgress) + Send + Sync + 'static,
107 {
108 self.on_progress = Some(Arc::new(handler));
109 self
110 }
111
112 /// Reports progress at most once per `interval` (one second by default).
113 ///
114 /// Set the field to `None` directly to report on item steps only.
115 pub fn progress_interval(mut self, interval: Duration) -> Self {
116 self.progress_interval = Some(interval);
117 self
118 }
119
120 /// Additionally reports progress every `items` items.
121 ///
122 /// Off by default, and deliberately so: it is a linear step, so a large stream reports a
123 /// number of times proportional to its size. Prefer [`Self::progress_interval`] unless you
124 /// specifically want item-granular checkpoints.
125 pub fn progress_items(mut self, items: u64) -> Self {
126 self.progress_items = Some(items);
127 self
128 }
129}
130
131impl ReqwestStreamOptions {
132 /// The direction-neutral subset of these options, for the shared accounting.
133 pub(crate) fn progress_options(&self) -> ProgressOptions {
134 let mut opts = ProgressOptions::new();
135 opts.on_error = self.on_error.clone();
136 opts.on_progress = self.on_progress.clone();
137 opts.progress_interval = self.progress_interval;
138 opts.progress_items = self.progress_items;
139 opts
140 }
141}
142
143/// Builds the accounting handle for one response.
144///
145/// Must be called before `bytes_stream()` consumes the response: `status` and
146/// `content_length` go on the span, and the latter is what lets an operator turn `bytes` into
147/// a completion percentage. This is a client-side opportunity the server side does not have,
148/// and the reason this function lives here rather than in core — core cannot name
149/// [`reqwest::Response`].
150///
151/// The URL is deliberately *not* recorded: it carries query strings and userinfo, which
152/// routinely means presigned-URL signatures and `?api_key=`.
153pub(crate) fn response_progress(
154 format: &'static str,
155 response: &reqwest::Response,
156 options: &ReqwestStreamOptions,
157) -> Progress {
158 let mut context = StreamContext::new(format, Direction::Response, Side::Client)
159 .status(response.status().as_u16())
160 .content_length(response.content_length())
161 .buf_capacity(options.buf_capacity);
162
163 // `usize::MAX` means "no limit", which is noise rather than information.
164 if options.max_obj_len != usize::MAX {
165 context = context.max_obj_len(options.max_obj_len);
166 }
167
168 Progress::new(&context, &options.progress_options())
169}
170
171/// The shared shape of every decode pipeline in this crate.
172///
173/// Bytes are counted on the response body, items on the decoded stream, and the outermost
174/// wrapper owns error reporting, the outcome, and the `Drop` that notices a consumer which
175/// stopped reading early.
176pub(crate) fn decode_response<'b, T, FMT>(
177 response: reqwest::Response,
178 format: FMT,
179 format_name: &'static str,
180 options: ReqwestStreamOptions,
181) -> impl futures::Stream<Item = crate::StreamBodyResult<T>> + Send + 'b
182where
183 FMT: http_streams_core::format::StreamFormatDecode<T>,
184 FMT::Framer: 'b,
185 FMT::Parser: 'b,
186 FMT::Frame: 'b,
187 // Deliberately no `T: 'b`. Formats whose framing is independent of the item type — CSV —
188 // keep `T` out of every stored type, so callers are not forced to add an outlives bound to
189 // their own public signatures.
190{
191 // Taken before `bytes_stream()` consumes the response.
192 let progress = response_progress(format_name, &response, &options);
193
194 let decode_options = http_streams_core::DecodeOptions::new()
195 .max_obj_len(options.max_obj_len)
196 .buf_capacity(options.buf_capacity);
197
198 let bytes = http_streams_core::count_bytes(
199 futures::TryStreamExt::map_err(response.bytes_stream(), std::io::Error::other),
200 &progress,
201 );
202
203 let items = http_streams_core::decode_stream(
204 bytes,
205 format.framer(&decode_options),
206 format.parser(),
207 &decode_options,
208 );
209
210 http_streams_core::instrument(
211 Box::pin(items),
212 progress,
213 http_streams_core::Counting::Items,
214 )
215}