Skip to main content

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}