Skip to main content

aws_smithy_runtime_api/client/http/
telemetry.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Provider-neutral observations from one HTTP request attempt.
7//!
8//! A runtime installs [`CaptureHttpAttemptTelemetry`] on a request before
9//! transmission. A compatible HTTP client records the facts it owns without
10//! depending on metric instruments, exporters, or tracing providers.
11
12use crate::client::connection::ConnectionMetadata;
13use aws_smithy_types::config_bag::{Storable, StoreReplace};
14use std::fmt;
15use std::sync::{Arc, Mutex, MutexGuard};
16use std::time::{Duration, SystemTime};
17
18/// Whether the selected connection had accepted an earlier request.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum ConnectionUsage {
22    /// The connection had not accepted an earlier request.
23    ///
24    /// For a multiplexed connection, only the first accepted request is fresh.
25    /// Concurrent requests accepted afterward observe reuse.
26    Fresh,
27    /// The connection had accepted at least one earlier request.
28    Reused,
29}
30
31/// Timing and reuse state for the connection selected by one request attempt.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33#[non_exhaustive]
34pub struct ConnectionAcquisitionTelemetry {
35    duration: Option<Duration>,
36    usage: ConnectionUsage,
37}
38
39impl ConnectionAcquisitionTelemetry {
40    /// Creates a completed connection-acquisition observation.
41    pub fn new(duration: Duration, usage: ConnectionUsage) -> Self {
42        Self {
43            duration: Some(duration),
44            usage,
45        }
46    }
47
48    /// Creates an observation from acquisition start and completion times.
49    ///
50    /// The duration is absent when `completed_at` precedes `started_at`. The
51    /// selected connection and its reuse state remain valid observations.
52    pub fn from_interval(
53        started_at: SystemTime,
54        completed_at: SystemTime,
55        usage: ConnectionUsage,
56    ) -> Self {
57        Self {
58            duration: completed_at.duration_since(started_at).ok(),
59            usage,
60        }
61    }
62
63    /// Returns elapsed time until the selected connection accepted the request.
64    ///
65    /// This is absent when the HTTP client's clock did not produce a valid
66    /// interval.
67    pub fn duration(&self) -> Option<Duration> {
68        self.duration
69    }
70
71    /// Returns whether the selected connection had accepted an earlier request.
72    pub fn usage(&self) -> ConnectionUsage {
73        self.usage
74    }
75}
76
77/// Facts recorded by a compatible HTTP client for one request attempt.
78#[derive(Clone, Debug, Default)]
79#[non_exhaustive]
80pub struct HttpAttemptTelemetry {
81    selection: Option<ConnectionSelection>,
82    connector_call_duration: Option<Duration>,
83}
84
85impl HttpAttemptTelemetry {
86    /// Returns completed connection-acquisition telemetry, when supplied.
87    pub fn acquisition(&self) -> Option<&ConnectionAcquisitionTelemetry> {
88        self.selection
89            .as_ref()
90            .map(|selection| &selection.acquisition)
91    }
92
93    /// Returns metadata for the connection that accepted the request, when supplied.
94    pub fn connection(&self) -> Option<&ConnectionMetadata> {
95        self.selection
96            .as_ref()
97            .map(|selection| &selection.connection)
98    }
99
100    /// Returns the complete HTTP connector call duration, when supplied.
101    ///
102    /// This ends when the connector returns a response head or terminal error.
103    /// It does not measure response-body transfer or time to first response byte.
104    pub fn connector_call_duration(&self) -> Option<Duration> {
105        self.connector_call_duration
106    }
107}
108
109#[derive(Clone, Debug)]
110struct ConnectionSelection {
111    acquisition: ConnectionAcquisitionTelemetry,
112    connection: ConnectionMetadata,
113}
114
115/// Shared request extension used to capture HTTP-attempt telemetry.
116///
117/// Each observation is recorded at most once. Acquisition timing and selected
118/// connection metadata are committed together.
119#[derive(Clone, Default)]
120pub struct CaptureHttpAttemptTelemetry {
121    state: Arc<Mutex<HttpAttemptTelemetry>>,
122}
123
124impl CaptureHttpAttemptTelemetry {
125    /// Creates an empty attempt capture.
126    pub fn new() -> Self {
127        Self::default()
128    }
129
130    /// Returns the observations recorded so far.
131    pub fn get(&self) -> HttpAttemptTelemetry {
132        self.lock().clone()
133    }
134
135    /// Records the complete HTTP connector call duration.
136    ///
137    /// Returns `true` when this call recorded the value.
138    pub fn record_connector_call_duration(&self, duration: Duration) -> bool {
139        let mut state = self.lock();
140        if state.connector_call_duration.is_some() {
141            return false;
142        }
143        state.connector_call_duration = Some(duration);
144        true
145    }
146
147    /// Records the connection selection that accepted the request.
148    ///
149    /// Acquisition and connection metadata are committed together. Returns
150    /// `true` when this call recorded the selection.
151    pub fn record_connection_selection(
152        &self,
153        acquisition: ConnectionAcquisitionTelemetry,
154        connection: ConnectionMetadata,
155    ) -> bool {
156        let mut state = self.lock();
157        if state.selection.is_some() {
158            return false;
159        }
160        state.selection = Some(ConnectionSelection {
161            acquisition,
162            connection,
163        });
164        true
165    }
166
167    fn lock(&self) -> MutexGuard<'_, HttpAttemptTelemetry> {
168        self.state
169            .lock()
170            .unwrap_or_else(std::sync::PoisonError::into_inner)
171    }
172}
173
174impl fmt::Debug for CaptureHttpAttemptTelemetry {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.debug_struct("CaptureHttpAttemptTelemetry")
177            .finish_non_exhaustive()
178    }
179}
180
181impl Storable for CaptureHttpAttemptTelemetry {
182    type Storer = StoreReplace<Self>;
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    fn connection() -> ConnectionMetadata {
190        ConnectionMetadata::builder()
191            .proxied(false)
192            .poison_fn(|| {})
193            .build()
194    }
195
196    #[test]
197    fn records_connector_call_and_selection_independently() {
198        let capture = CaptureHttpAttemptTelemetry::new();
199        let acquisition =
200            ConnectionAcquisitionTelemetry::new(Duration::from_millis(3), ConnectionUsage::Fresh);
201
202        assert!(capture.record_connection_selection(acquisition, connection()));
203        assert!(capture.record_connector_call_duration(Duration::from_millis(7)));
204
205        let telemetry = capture.get();
206        assert_eq!(telemetry.acquisition(), Some(&acquisition));
207        assert_eq!(
208            telemetry.connector_call_duration(),
209            Some(Duration::from_millis(7))
210        );
211        assert!(telemetry.connection().is_some());
212    }
213
214    #[test]
215    fn first_recorded_value_wins() {
216        let capture = CaptureHttpAttemptTelemetry::new();
217        let first =
218            ConnectionAcquisitionTelemetry::new(Duration::from_millis(3), ConnectionUsage::Fresh);
219        let second =
220            ConnectionAcquisitionTelemetry::new(Duration::from_millis(9), ConnectionUsage::Reused);
221        assert!(capture.record_connection_selection(first, connection()));
222        assert!(!capture.record_connection_selection(second, connection()));
223        assert!(capture.record_connector_call_duration(Duration::from_millis(4)));
224        assert!(!capture.record_connector_call_duration(Duration::from_millis(8)));
225
226        let telemetry = capture.get();
227        assert_eq!(telemetry.acquisition(), Some(&first));
228        assert_eq!(
229            telemetry.connector_call_duration(),
230            Some(Duration::from_millis(4))
231        );
232    }
233
234    #[test]
235    fn backwards_acquisition_interval_retains_selection_facts() {
236        let started_at = SystemTime::UNIX_EPOCH + Duration::from_secs(2);
237        let completed_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
238
239        let acquisition = ConnectionAcquisitionTelemetry::from_interval(
240            started_at,
241            completed_at,
242            ConnectionUsage::Fresh,
243        );
244        let capture = CaptureHttpAttemptTelemetry::new();
245        assert!(capture.record_connection_selection(acquisition, connection()));
246
247        let telemetry = capture.get();
248        assert_eq!(telemetry.acquisition().expect("selection").duration(), None);
249        assert_eq!(
250            telemetry.acquisition().expect("selection").usage(),
251            ConnectionUsage::Fresh
252        );
253        assert!(telemetry.connection().is_some());
254    }
255}