AWS SDK

AWS SDK

rev. 39e8f0a4a4cd7fc94cae1e2f796e9c21897d9cb9 (ignoring whitespace)

Files changed:

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-runtime-api/src/client/connection.rs

@@ -1,1 +369,431 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Types related to connection monitoring and management.
    7      7   
           8  +
mod establishment;
           9  +
          10  +
pub use establishment::{ConnectionEstablishmentMetadata, ConnectionEstablishmentMetadataBuilder};
          11  +
    8     12   
use aws_smithy_types::config_bag::{Storable, StoreReplace};
    9     13   
use std::fmt;
   10     14   
use std::net::SocketAddr;
   11     15   
use std::sync::{Arc, Mutex};
   12     16   
   13     17   
/// Opaque identifier for a physical HTTP connection.
   14     18   
///
   15     19   
/// The assigning HTTP client defines the scope of an ID. IDs from different
   16     20   
/// clients or pools are not comparable.
   17     21   
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
   18     22   
pub struct ConnectionId(u64);
   19     23   
   20     24   
impl ConnectionId {
   21     25   
    /// Creates an ID from a value assigned by an HTTP client.
   22     26   
    pub const fn new(value: u64) -> Self {
   23     27   
        Self(value)
   24     28   
    }
   25     29   
}
   26     30   
   27     31   
impl fmt::Display for ConnectionId {
   28     32   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
   29     33   
        self.0.fmt(f)
   30     34   
    }
   31     35   
}
   32     36   
   33     37   
/// Metadata that tracks the state of an active connection.
   34     38   
#[derive(Clone)]
   35     39   
pub struct ConnectionMetadata {
   36     40   
    is_proxied: bool,
   37     41   
    remote_addr: Option<SocketAddr>,
   38     42   
    local_addr: Option<SocketAddr>,
   39     43   
    poison_fn: Arc<dyn Fn() + Send + Sync>,
   40     44   
    connection_id: Option<ConnectionId>,
          45  +
    establishment: Option<ConnectionEstablishmentMetadata>,
   41     46   
}
   42     47   
   43     48   
impl ConnectionMetadata {
   44     49   
    /// Poison this connection, ensuring that it won't be reused.
   45     50   
    pub fn poison(&self) {
   46     51   
        tracing::debug!(
   47     52   
            see_for_more_info = "https://smithy-lang.github.io/smithy-rs/design/client/detailed_error_explanations.html",
   48     53   
            "Connection encountered an issue and should not be re-used. Marking it for closure"
   49     54   
        );
   50     55   
        (self.poison_fn)()
   51     56   
    }
   52     57   
   53     58   
    /// Create a new [`ConnectionMetadata`].
   54     59   
    #[deprecated(
   55     60   
        since = "1.1.0",
   56     61   
        note = "`ConnectionMetadata::new` is deprecated in favour of `ConnectionMetadata::builder`."
   57     62   
    )]
   58     63   
    pub fn new(
   59     64   
        is_proxied: bool,
   60     65   
        remote_addr: Option<SocketAddr>,
   61     66   
        poison: impl Fn() + Send + Sync + 'static,
   62     67   
    ) -> Self {
   63     68   
        Self {
   64     69   
            is_proxied,
   65     70   
            remote_addr,
   66     71   
            // need to use builder to set this field
   67     72   
            local_addr: None,
   68     73   
            poison_fn: Arc::new(poison),
   69     74   
            connection_id: None,
          75  +
            establishment: None,
   70     76   
        }
   71     77   
    }
   72     78   
   73     79   
    /// Builder for this connection metadata
   74     80   
    pub fn builder() -> ConnectionMetadataBuilder {
   75     81   
        ConnectionMetadataBuilder::new()
   76     82   
    }
   77     83   
   78     84   
    /// Get the remote address for this connection, if one is set.
   79     85   
    pub fn remote_addr(&self) -> Option<SocketAddr> {
   80     86   
        self.remote_addr
   81     87   
    }
   82     88   
   83     89   
    /// Get the local address for this connection, if one is set.
   84     90   
    pub fn local_addr(&self) -> Option<SocketAddr> {
   85     91   
        self.local_addr
   86     92   
    }
   87     93   
   88     94   
    /// Get the ID assigned to this connection, if the HTTP client provides one.
   89     95   
    ///
   90     96   
    /// Clients that do not track physical connection identity leave this unset.
   91     97   
    pub fn connection_id(&self) -> Option<ConnectionId> {
   92     98   
        self.connection_id
   93     99   
    }
         100  +
         101  +
    /// Get measurements from the establishment that installed this connection.
         102  +
    ///
         103  +
    /// Clients that do not expose successful establishment measurements leave
         104  +
    /// this unset.
         105  +
    pub fn establishment(&self) -> Option<&ConnectionEstablishmentMetadata> {
         106  +
        self.establishment.as_ref()
         107  +
    }
   94    108   
}
   95    109   
   96    110   
impl fmt::Debug for ConnectionMetadata {
   97    111   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
   98    112   
        f.debug_struct("SmithyConnection")
   99    113   
            .field("is_proxied", &self.is_proxied)
  100    114   
            .field("remote_addr", &self.remote_addr)
  101    115   
            .field("local_addr", &self.local_addr)
  102    116   
            .field("connection_id", &self.connection_id)
         117  +
            .field("establishment", &self.establishment)
  103    118   
            .finish()
  104    119   
    }
  105    120   
}
  106    121   
  107    122   
/// Builder type that is used to construct a [`ConnectionMetadata`] value.
  108    123   
#[derive(Default)]
  109    124   
pub struct ConnectionMetadataBuilder {
  110    125   
    is_proxied: Option<bool>,
  111    126   
    remote_addr: Option<SocketAddr>,
  112    127   
    local_addr: Option<SocketAddr>,
  113    128   
    poison_fn: Option<Arc<dyn Fn() + Send + Sync>>,
  114    129   
    connection_id: Option<ConnectionId>,
         130  +
    establishment: Option<ConnectionEstablishmentMetadata>,
  115    131   
}
  116    132   
  117    133   
impl fmt::Debug for ConnectionMetadataBuilder {
  118    134   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  119    135   
        f.debug_struct("ConnectionMetadataBuilder")
  120    136   
            .field("is_proxied", &self.is_proxied)
  121    137   
            .field("remote_addr", &self.remote_addr)
  122    138   
            .field("local_addr", &self.local_addr)
  123    139   
            .field("connection_id", &self.connection_id)
         140  +
            .field("establishment", &self.establishment)
  124    141   
            .finish()
  125    142   
    }
  126    143   
}
  127    144   
  128    145   
impl ConnectionMetadataBuilder {
  129    146   
    /// Creates a new builder.
  130    147   
    pub fn new() -> Self {
  131    148   
        Self::default()
  132    149   
    }
  133    150   
  134    151   
    /// Set whether or not the associated connection is to an HTTP proxy.
  135    152   
    pub fn proxied(mut self, proxied: bool) -> Self {
  136    153   
        self.set_proxied(Some(proxied));
  137    154   
        self
  138    155   
    }
  139    156   
  140    157   
    /// Set whether or not the associated connection is to an HTTP proxy.
  141    158   
    pub fn set_proxied(&mut self, proxied: Option<bool>) -> &mut Self {
  142    159   
        self.is_proxied = proxied;
  143    160   
        self
  144    161   
    }
  145    162   
  146    163   
    /// Set the remote address of the connection used.
  147    164   
    pub fn remote_addr(mut self, remote_addr: SocketAddr) -> Self {
  148    165   
        self.set_remote_addr(Some(remote_addr));
  149    166   
        self
  150    167   
    }
  151    168   
  152    169   
    /// Set the remote address of the connection used.
  153    170   
    pub fn set_remote_addr(&mut self, remote_addr: Option<SocketAddr>) -> &mut Self {
  154    171   
        self.remote_addr = remote_addr;
  155    172   
        self
  156    173   
    }
  157    174   
  158    175   
    /// Set the local address of the connection used.
  159    176   
    pub fn local_addr(mut self, local_addr: SocketAddr) -> Self {
  160    177   
        self.set_local_addr(Some(local_addr));
  161    178   
        self
  162    179   
    }
  163    180   
  164    181   
    /// Set the local address of the connection used.
  165    182   
    pub fn set_local_addr(&mut self, local_addr: Option<SocketAddr>) -> &mut Self {
  166    183   
        self.local_addr = local_addr;
  167    184   
        self
  168    185   
    }
  169    186   
  170    187   
    /// Set the [`ConnectionId`] assigned by the HTTP client.
  171    188   
    pub fn connection_id(mut self, connection_id: ConnectionId) -> Self {
  172    189   
        self.set_connection_id(Some(connection_id));
  173    190   
        self
  174    191   
    }
  175    192   
  176    193   
    /// Set the [`ConnectionId`] assigned by the HTTP client.
  177    194   
    pub fn set_connection_id(&mut self, connection_id: Option<ConnectionId>) -> &mut Self {
  178    195   
        self.connection_id = connection_id;
  179    196   
        self
  180    197   
    }
  181    198   
         199  +
    /// Sets measurements from the establishment that installed this connection.
         200  +
    pub fn establishment(mut self, establishment: ConnectionEstablishmentMetadata) -> Self {
         201  +
        self.set_establishment(Some(establishment));
         202  +
        self
         203  +
    }
         204  +
         205  +
    /// Sets measurements from the establishment that installed this connection.
         206  +
    pub fn set_establishment(
         207  +
        &mut self,
         208  +
        establishment: Option<ConnectionEstablishmentMetadata>,
         209  +
    ) -> &mut Self {
         210  +
        self.establishment = establishment;
         211  +
        self
         212  +
    }
         213  +
  182    214   
    /// Set a closure which will poison the associated connection.
  183    215   
    ///
  184    216   
    /// A poisoned connection will not be reused for subsequent requests by the pool
  185    217   
    pub fn poison_fn(mut self, poison_fn: impl Fn() + Send + Sync + 'static) -> Self {
  186    218   
        self.set_poison_fn(Some(poison_fn));
  187    219   
        self
  188    220   
    }
  189    221   
  190    222   
    /// Set a closure which will poison the associated connection.
  191    223   
    ///
  192    224   
    /// A poisoned connection will not be reused for subsequent requests by the pool
  193    225   
    pub fn set_poison_fn(
  194    226   
        &mut self,
  195    227   
        poison_fn: Option<impl Fn() + Send + Sync + 'static>,
  196    228   
    ) -> &mut Self {
  197    229   
        self.poison_fn =
  198    230   
            poison_fn.map(|poison_fn| Arc::new(poison_fn) as Arc<dyn Fn() + Send + Sync>);
  199    231   
        self
  200    232   
    }
  201    233   
  202    234   
    /// Build a [`ConnectionMetadata`] value.
  203    235   
    ///
  204    236   
    /// # Panics
  205    237   
    ///
  206    238   
    /// If either the `is_proxied` or `poison_fn` has not been set, then this method will panic
  207    239   
    pub fn build(self) -> ConnectionMetadata {
  208    240   
        ConnectionMetadata {
  209    241   
            is_proxied: self
  210    242   
                .is_proxied
  211    243   
                .expect("is_proxied should be set for ConnectionMetadata"),
  212    244   
            remote_addr: self.remote_addr,
  213    245   
            local_addr: self.local_addr,
  214    246   
            poison_fn: self
  215    247   
                .poison_fn
  216    248   
                .expect("poison_fn should be set for ConnectionMetadata"),
  217    249   
            connection_id: self.connection_id,
         250  +
            establishment: self.establishment,
  218    251   
        }
  219    252   
    }
  220    253   
}
  221    254   
  222    255   
type LoaderFn = dyn Fn() -> Option<ConnectionMetadata> + Send + Sync;
  223    256   
  224    257   
/// State for a middleware that will monitor and manage connections.
  225    258   
#[derive(Clone, Default)]
  226    259   
pub struct CaptureSmithyConnection {
  227    260   
    loader: Arc<Mutex<Option<Box<LoaderFn>>>>,
  228    261   
}
  229    262   
  230    263   
impl CaptureSmithyConnection {
  231    264   
    /// Create a new connection monitor.
  232    265   
    pub fn new() -> Self {
  233    266   
        Self {
  234    267   
            loader: Default::default(),
  235    268   
        }
  236    269   
    }
  237    270   
  238    271   
    /// Set the retriever that will capture the `hyper` connection.
  239    272   
    pub fn set_connection_retriever<F>(&self, f: F)
  240    273   
    where
  241    274   
        F: Fn() -> Option<ConnectionMetadata> + Send + Sync + 'static,
  242    275   
    {
  243    276   
        *self.loader.lock().unwrap() = Some(Box::new(f));
  244    277   
    }
  245    278   
  246    279   
    /// Get the associated connection metadata.
  247    280   
    pub fn get(&self) -> Option<ConnectionMetadata> {
  248    281   
        match self.loader.lock().unwrap().as_ref() {
  249    282   
            Some(loader) => loader(),
  250    283   
            None => {
  251    284   
                tracing::debug!("no loader was set on the CaptureSmithyConnection");
  252    285   
                None
  253    286   
            }
  254    287   
        }
  255    288   
    }
  256    289   
}
  257    290   
  258    291   
impl fmt::Debug for CaptureSmithyConnection {
  259    292   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  260    293   
        write!(f, "CaptureSmithyConnection")
  261    294   
    }
  262    295   
}
  263    296   
  264    297   
impl Storable for CaptureSmithyConnection {
  265    298   
    type Storer = StoreReplace<Self>;
  266    299   
}
  267    300   
  268    301   
#[cfg(test)]
  269    302   
mod tests {
  270    303   
    use std::{
  271    304   
        net::{IpAddr, Ipv6Addr},
  272    305   
        sync::Mutex,
         306  +
        time::Duration,
  273    307   
    };
  274    308   
  275    309   
    use super::*;
  276    310   
  277    311   
    const TEST_SOCKET_ADDR: SocketAddr = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 100);
  278    312   
  279    313   
    #[test]
  280    314   
    #[should_panic]
  281    315   
    fn builder_panic_missing_proxied() {
  282    316   
        ConnectionMetadataBuilder::new()
  283    317   
            .poison_fn(|| {})
  284    318   
            .local_addr(TEST_SOCKET_ADDR)
  285    319   
            .remote_addr(TEST_SOCKET_ADDR)
  286    320   
            .build();
  287    321   
    }
  288    322   
  289    323   
    #[test]
  290    324   
    #[should_panic]
  291    325   
    fn builder_panic_missing_poison_fn() {
  292    326   
        ConnectionMetadataBuilder::new()
  293    327   
            .proxied(true)
  294    328   
            .local_addr(TEST_SOCKET_ADDR)
  295    329   
            .remote_addr(TEST_SOCKET_ADDR)
  296    330   
            .build();
  297    331   
    }
  298    332   
  299    333   
    #[test]
  300    334   
    fn builder_all_fields_successful() {
  301    335   
        let mutable_flag = Arc::new(Mutex::new(false));
         336  +
        let establishment = ConnectionEstablishmentMetadata::builder()
         337  +
            .total_duration(Duration::from_millis(13))
         338  +
            .transport_duration(Duration::from_millis(8))
         339  +
            .protocol_handshake_duration(Duration::from_millis(5))
         340  +
            .dns_duration(Duration::from_millis(1))
         341  +
            .socket_connect_duration(Duration::from_millis(2))
         342  +
            .proxy_duration(Duration::from_millis(3))
         343  +
            .tls_duration(Duration::from_millis(4))
         344  +
            .build();
  302    345   
  303    346   
        let connection_metadata = ConnectionMetadataBuilder::new()
  304    347   
            .proxied(true)
  305    348   
            .local_addr(TEST_SOCKET_ADDR)
  306    349   
            .remote_addr(TEST_SOCKET_ADDR)
  307    350   
            .connection_id(ConnectionId::new(17))
         351  +
            .establishment(establishment.clone())
  308    352   
            .poison_fn({
  309    353   
                let mutable_flag = Arc::clone(&mutable_flag);
  310    354   
                move || {
  311    355   
                    let mut guard = mutable_flag.lock().unwrap();
  312    356   
                    *guard = !*guard;
  313    357   
                }
  314    358   
            })
  315    359   
            .build();
  316    360   
  317    361   
        assert!(connection_metadata.is_proxied);
  318    362   
        assert_eq!(connection_metadata.remote_addr(), Some(TEST_SOCKET_ADDR));
  319    363   
        assert_eq!(connection_metadata.local_addr(), Some(TEST_SOCKET_ADDR));
  320    364   
        assert_eq!(
  321    365   
            connection_metadata.connection_id(),
  322    366   
            Some(ConnectionId::new(17))
  323    367   
        );
         368  +
        assert_eq!(connection_metadata.establishment(), Some(&establishment));
         369  +
        assert_eq!(establishment.total_duration(), Duration::from_millis(13));
         370  +
        assert_eq!(establishment.transport_duration(), Duration::from_millis(8));
         371  +
        assert_eq!(
         372  +
            establishment.protocol_handshake_duration(),
         373  +
            Some(Duration::from_millis(5))
         374  +
        );
         375  +
        assert_eq!(establishment.dns_duration(), Some(Duration::from_millis(1)));
         376  +
        assert_eq!(
         377  +
            establishment.socket_connect_duration(),
         378  +
            Some(Duration::from_millis(2))
         379  +
        );
         380  +
        assert_eq!(
         381  +
            establishment.proxy_duration(),
         382  +
            Some(Duration::from_millis(3))
         383  +
        );
         384  +
        assert_eq!(establishment.tls_duration(), Some(Duration::from_millis(4)));
  324    385   
        assert_eq!("17", ConnectionId::new(17).to_string());
  325    386   
        assert!(!(*mutable_flag.lock().unwrap()));
  326    387   
        connection_metadata.poison();
  327    388   
        assert!(*mutable_flag.lock().unwrap());
  328    389   
    }
  329    390   
  330    391   
    #[test]
  331    392   
    fn builder_optional_fields_translate() {
  332    393   
        let metadata1 = ConnectionMetadataBuilder::new()
  333    394   
            .proxied(true)
  334    395   
            .poison_fn(|| {})
  335    396   
            .build();
  336    397   
  337    398   
        assert_eq!(metadata1.local_addr(), None);
  338    399   
        assert_eq!(metadata1.remote_addr(), None);
  339    400   
        assert_eq!(metadata1.connection_id(), None);
         401  +
        assert_eq!(metadata1.establishment(), None);
  340    402   
  341    403   
        let metadata2 = ConnectionMetadataBuilder::new()
  342    404   
            .proxied(true)
  343    405   
            .poison_fn(|| {})
  344    406   
            .local_addr(TEST_SOCKET_ADDR)
  345    407   
            .build();
  346    408   
  347    409   
        assert_eq!(metadata2.local_addr(), Some(TEST_SOCKET_ADDR));
  348    410   
        assert_eq!(metadata2.remote_addr(), None);
  349    411   

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-runtime-api/src/client/connection/establishment.rs

@@ -0,1 +0,197 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
use std::sync::Arc;
           7  +
use std::time::Duration;
           8  +
           9  +
/// Measurements from one successful connection establishment.
          10  +
///
          11  +
/// The total and transport durations are available when an HTTP client exposes
          12  +
/// this value. Transport substages remain optional because a custom connector
          13  +
/// may expose only its aggregate connection work.
          14  +
#[derive(Clone, Debug, Eq, PartialEq)]
          15  +
#[non_exhaustive]
          16  +
pub struct ConnectionEstablishmentMetadata {
          17  +
    inner: Arc<ConnectionEstablishmentMetadataInner>,
          18  +
}
          19  +
          20  +
#[derive(Debug, Eq, PartialEq)]
          21  +
struct ConnectionEstablishmentMetadataInner {
          22  +
    total_duration: Duration,
          23  +
    transport_duration: Duration,
          24  +
    protocol_handshake_duration: Option<Duration>,
          25  +
    dns_duration: Option<Duration>,
          26  +
    socket_connect_duration: Option<Duration>,
          27  +
    proxy_duration: Option<Duration>,
          28  +
    tls_duration: Option<Duration>,
          29  +
}
          30  +
          31  +
impl ConnectionEstablishmentMetadata {
          32  +
    /// Creates a builder for successful establishment measurements.
          33  +
    pub fn builder() -> ConnectionEstablishmentMetadataBuilder {
          34  +
        ConnectionEstablishmentMetadataBuilder::new()
          35  +
    }
          36  +
          37  +
    /// Returns elapsed time from establishment start until pool installation.
          38  +
    pub fn total_duration(&self) -> Duration {
          39  +
        self.inner.total_duration
          40  +
    }
          41  +
          42  +
    /// Returns elapsed time spent in the configured transport connector.
          43  +
    pub fn transport_duration(&self) -> Duration {
          44  +
        self.inner.transport_duration
          45  +
    }
          46  +
          47  +
    /// Returns elapsed time spent in the HTTP protocol handshake, when known.
          48  +
    pub fn protocol_handshake_duration(&self) -> Option<Duration> {
          49  +
        self.inner.protocol_handshake_duration
          50  +
    }
          51  +
          52  +
    /// Returns elapsed time spent resolving DNS, when reported by the connector.
          53  +
    pub fn dns_duration(&self) -> Option<Duration> {
          54  +
        self.inner.dns_duration
          55  +
    }
          56  +
          57  +
    /// Returns elapsed time spent establishing the selected socket, when reported.
          58  +
    pub fn socket_connect_duration(&self) -> Option<Duration> {
          59  +
        self.inner.socket_connect_duration
          60  +
    }
          61  +
          62  +
    /// Returns elapsed time spent negotiating a proxy path, when reported.
          63  +
    pub fn proxy_duration(&self) -> Option<Duration> {
          64  +
        self.inner.proxy_duration
          65  +
    }
          66  +
          67  +
    /// Returns elapsed time spent in TLS establishment, when reported.
          68  +
    pub fn tls_duration(&self) -> Option<Duration> {
          69  +
        self.inner.tls_duration
          70  +
    }
          71  +
}
          72  +
          73  +
/// Builder for [`ConnectionEstablishmentMetadata`].
          74  +
#[derive(Clone, Debug, Default)]
          75  +
pub struct ConnectionEstablishmentMetadataBuilder {
          76  +
    total_duration: Option<Duration>,
          77  +
    transport_duration: Option<Duration>,
          78  +
    protocol_handshake_duration: Option<Duration>,
          79  +
    dns_duration: Option<Duration>,
          80  +
    socket_connect_duration: Option<Duration>,
          81  +
    proxy_duration: Option<Duration>,
          82  +
    tls_duration: Option<Duration>,
          83  +
}
          84  +
          85  +
impl ConnectionEstablishmentMetadataBuilder {
          86  +
    /// Creates an empty builder.
          87  +
    pub fn new() -> Self {
          88  +
        Self::default()
          89  +
    }
          90  +
          91  +
    /// Sets total elapsed establishment time.
          92  +
    pub fn total_duration(mut self, duration: Duration) -> Self {
          93  +
        self.set_total_duration(Some(duration));
          94  +
        self
          95  +
    }
          96  +
          97  +
    /// Sets total elapsed establishment time.
          98  +
    pub fn set_total_duration(&mut self, duration: Option<Duration>) -> &mut Self {
          99  +
        self.total_duration = duration;
         100  +
        self
         101  +
    }
         102  +
         103  +
    /// Sets aggregate transport-connector time.
         104  +
    pub fn transport_duration(mut self, duration: Duration) -> Self {
         105  +
        self.set_transport_duration(Some(duration));
         106  +
        self
         107  +
    }
         108  +
         109  +
    /// Sets aggregate transport-connector time.
         110  +
    pub fn set_transport_duration(&mut self, duration: Option<Duration>) -> &mut Self {
         111  +
        self.transport_duration = duration;
         112  +
        self
         113  +
    }
         114  +
         115  +
    /// Sets HTTP protocol-handshake time.
         116  +
    pub fn protocol_handshake_duration(mut self, duration: Duration) -> Self {
         117  +
        self.set_protocol_handshake_duration(Some(duration));
         118  +
        self
         119  +
    }
         120  +
         121  +
    /// Sets HTTP protocol-handshake time.
         122  +
    pub fn set_protocol_handshake_duration(&mut self, duration: Option<Duration>) -> &mut Self {
         123  +
        self.protocol_handshake_duration = duration;
         124  +
        self
         125  +
    }
         126  +
         127  +
    /// Sets DNS resolution time.
         128  +
    pub fn dns_duration(mut self, duration: Duration) -> Self {
         129  +
        self.set_dns_duration(Some(duration));
         130  +
        self
         131  +
    }
         132  +
         133  +
    /// Sets DNS resolution time.
         134  +
    pub fn set_dns_duration(&mut self, duration: Option<Duration>) -> &mut Self {
         135  +
        self.dns_duration = duration;
         136  +
        self
         137  +
    }
         138  +
         139  +
    /// Sets socket establishment time.
         140  +
    pub fn socket_connect_duration(mut self, duration: Duration) -> Self {
         141  +
        self.set_socket_connect_duration(Some(duration));
         142  +
        self
         143  +
    }
         144  +
         145  +
    /// Sets socket establishment time.
         146  +
    pub fn set_socket_connect_duration(&mut self, duration: Option<Duration>) -> &mut Self {
         147  +
        self.socket_connect_duration = duration;
         148  +
        self
         149  +
    }
         150  +
         151  +
    /// Sets proxy negotiation time.
         152  +
    pub fn proxy_duration(mut self, duration: Duration) -> Self {
         153  +
        self.set_proxy_duration(Some(duration));
         154  +
        self
         155  +
    }
         156  +
         157  +
    /// Sets proxy negotiation time.
         158  +
    pub fn set_proxy_duration(&mut self, duration: Option<Duration>) -> &mut Self {
         159  +
        self.proxy_duration = duration;
         160  +
        self
         161  +
    }
         162  +
         163  +
    /// Sets TLS establishment time.
         164  +
    pub fn tls_duration(mut self, duration: Duration) -> Self {
         165  +
        self.set_tls_duration(Some(duration));
         166  +
        self
         167  +
    }
         168  +
         169  +
    /// Sets TLS establishment time.
         170  +
    pub fn set_tls_duration(&mut self, duration: Option<Duration>) -> &mut Self {
         171  +
        self.tls_duration = duration;
         172  +
        self
         173  +
    }
         174  +
         175  +
    /// Builds successful connection-establishment metadata.
         176  +
    ///
         177  +
    /// # Panics
         178  +
    ///
         179  +
    /// Panics when total or transport duration is unset.
         180  +
    pub fn build(self) -> ConnectionEstablishmentMetadata {
         181  +
        ConnectionEstablishmentMetadata {
         182  +
            inner: Arc::new(ConnectionEstablishmentMetadataInner {
         183  +
                total_duration: self
         184  +
                    .total_duration
         185  +
                    .expect("total_duration is required for connection establishment metadata"),
         186  +
                transport_duration: self
         187  +
                    .transport_duration
         188  +
                    .expect("transport_duration is required for connection establishment metadata"),
         189  +
                protocol_handshake_duration: self.protocol_handshake_duration,
         190  +
                dns_duration: self.dns_duration,
         191  +
                socket_connect_duration: self.socket_connect_duration,
         192  +
                proxy_duration: self.proxy_duration,
         193  +
                tls_duration: self.tls_duration,
         194  +
            }),
         195  +
        }
         196  +
    }
         197  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-runtime-api/src/client/http.rs

@@ -23,23 +82,84 @@
   43     43   
//! - TLS settings
   44     44   
//! - Timeouts
   45     45   
//!
   46     46   
//! Some of these aren't implemented yet, but they will appear in the [`HttpConnectorSettings`] struct
   47     47   
//! once they are.
   48     48   
//!
   49     49   
//! [`hyper`]: https://crates.io/crates/hyper
   50     50   
//! [`tower`]: https://crates.io/crates/tower
   51     51   
//! [`aws-smithy-runtime`]: https://crates.io/crates/aws-smithy-runtime
   52     52   
          53  +
pub mod telemetry;
          54  +
   53     55   
use crate::box_error::BoxError;
   54     56   
use crate::client::connector_metadata::ConnectorMetadata;
   55     57   
use crate::client::orchestrator::{HttpRequest, HttpResponse};
   56     58   
use crate::client::result::ConnectorError;
   57     59   
use crate::client::runtime_components::sealed::ValidateConfig;
   58     60   
use crate::client::runtime_components::{RuntimeComponents, RuntimeComponentsBuilder};
   59     61   
use crate::impl_shared_conversions;
   60     62   
use aws_smithy_types::config_bag::ConfigBag;
   61     63   
use std::fmt;
   62     64   
use std::sync::Arc;

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-runtime-api/src/client/http/telemetry.rs

@@ -0,1 +0,255 @@
           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  +
          12  +
use crate::client::connection::ConnectionMetadata;
          13  +
use aws_smithy_types::config_bag::{Storable, StoreReplace};
          14  +
use std::fmt;
          15  +
use std::sync::{Arc, Mutex, MutexGuard};
          16  +
use 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]
          21  +
pub 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]
          34  +
pub struct ConnectionAcquisitionTelemetry {
          35  +
    duration: Option<Duration>,
          36  +
    usage: ConnectionUsage,
          37  +
}
          38  +
          39  +
impl 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]
          80  +
pub struct HttpAttemptTelemetry {
          81  +
    selection: Option<ConnectionSelection>,
          82  +
    connector_call_duration: Option<Duration>,
          83  +
}
          84  +
          85  +
impl 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)]
         110  +
struct 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)]
         120  +
pub struct CaptureHttpAttemptTelemetry {
         121  +
    state: Arc<Mutex<HttpAttemptTelemetry>>,
         122  +
}
         123  +
         124  +
impl 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  +
         174  +
impl 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  +
         181  +
impl Storable for CaptureHttpAttemptTelemetry {
         182  +
    type Storer = StoreReplace<Self>;
         183  +
}
         184  +
         185  +
#[cfg(test)]
         186  +
mod 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  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-runtime-api/src/client/result.rs

@@ -546,546 +640,640 @@
  566    566   
pub(crate) enum ConnectionStatus {
  567    567   
    /// This request was never connected to the remote
  568    568   
    ///
  569    569   
    /// This indicates the failure was during connection establishment
  570    570   
    NeverConnected,
  571    571   
  572    572   
    /// It is unknown whether a connection was established
  573    573   
    Unknown,
  574    574   
  575    575   
    /// The request connected to the remote prior to failure
  576         -
    Connected(ConnectionMetadata),
         576  +
    Connected(Box<ConnectionMetadata>),
  577    577   
}
  578    578   
  579    579   
impl Display for ConnectorError {
  580    580   
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
  581    581   
        match self.kind {
  582    582   
            ConnectorErrorKind::Timeout => write!(f, "timeout"),
  583    583   
            ConnectorErrorKind::User => write!(f, "user error"),
  584    584   
            ConnectorErrorKind::Io => write!(f, "io error"),
  585    585   
            ConnectorErrorKind::Other(_) => write!(f, "other"),
  586    586   
        }
  587    587   
    }
  588    588   
}
  589    589   
  590    590   
impl Error for ConnectorError {
  591    591   
    fn source(&self) -> Option<&(dyn Error + 'static)> {
  592    592   
        Some(self.source.as_ref())
  593    593   
    }
  594    594   
}
  595    595   
  596    596   
impl ConnectorError {
  597    597   
    /// Construct a [`ConnectorError`] from an error caused by a timeout
  598    598   
    ///
  599    599   
    /// Timeout errors are typically retried on a new connection.
  600    600   
    pub fn timeout(source: BoxError) -> Self {
  601    601   
        Self {
  602    602   
            kind: ConnectorErrorKind::Timeout,
  603    603   
            source,
  604    604   
            connection: ConnectionStatus::Unknown,
  605    605   
        }
  606    606   
    }
  607    607   
  608    608   
    /// Include connection information along with this error
  609    609   
    pub fn with_connection(mut self, info: ConnectionMetadata) -> Self {
  610         -
        self.connection = ConnectionStatus::Connected(info);
         610  +
        self.connection = ConnectionStatus::Connected(Box::new(info));
  611    611   
        self
  612    612   
    }
  613    613   
  614    614   
    /// Set the connection status on this error to report that a connection was never established
  615    615   
    pub fn never_connected(mut self) -> Self {
  616    616   
        self.connection = ConnectionStatus::NeverConnected;
  617    617   
        self
  618    618   
    }
  619    619   
  620    620   
    /// Construct a [`ConnectorError`] from an error caused by the user (e.g. invalid HTTP request)

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-runtime-api/src/http/extensions.rs

@@ -1,1 +59,62 @@
   17     17   
        Self::default()
   18     18   
    }
   19     19   
   20     20   
    /// Adds an extension to the request extensions
   21     21   
    pub(crate) fn insert<T: Send + Sync + Clone + 'static>(&mut self, extension: T) {
   22     22   
        #[cfg(feature = "http-02x")]
   23     23   
        self.extensions_02x.insert(extension.clone());
   24     24   
        self.extensions_1x.insert(extension);
   25     25   
    }
   26     26   
   27         -
    // Reads a previously-inserted extension of type `T`, if present.
          27  +
    /// Reads a previously inserted extension of type `T`, if present.
   28     28   
    pub(crate) fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
   29         -
        self.extensions_1x.get::<T>()
          29  +
        let extension = self.extensions_1x.get::<T>();
          30  +
        #[cfg(feature = "http-02x")]
          31  +
        let extension = extension.or_else(|| self.extensions_02x.get::<T>());
          32  +
        extension
   30     33   
    }
   31     34   
}
   32     35   
   33     36   
#[cfg(feature = "http-02x")]
   34     37   
impl From<http_02x::Extensions> for Extensions {
   35     38   
    fn from(value: http_02x::Extensions) -> Self {
   36     39   
        Self {
   37     40   
            extensions_02x: value,
   38     41   
            extensions_1x: Default::default(),
   39     42   
        }

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-runtime-api/src/http/request.rs

@@ -319,319 +378,383 @@
  339    339   
    {
  340    340   
        let uri = uri.try_into()?;
  341    341   
        self.uri = uri;
  342    342   
        Ok(())
  343    343   
    }
  344    344   
  345    345   
    /// Adds an extension to the request extensions
  346    346   
    pub fn add_extension<T: Send + Sync + Clone + 'static>(&mut self, extension: T) {
  347    347   
        self.extensions.insert(extension.clone());
  348    348   
    }
         349  +
         350  +
    /// Returns an extension previously added to this request.
         351  +
    pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
         352  +
        self.extensions.get()
         353  +
    }
  349    354   
}
  350    355   
  351    356   
impl Request<SdkBody> {
  352    357   
    /// Attempts to clone this request
  353    358   
    ///
  354    359   
    /// On clone, any extensions will be cleared.
  355    360   
    ///
  356    361   
    /// If the body is cloneable, this will clone the request. Otherwise `None` will be returned
  357    362   
    pub fn try_clone(&self) -> Option<Self> {
  358    363   
        let body = self.body().try_clone()?;

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-runtime/src/client/metrics.rs

@@ -1,1 +137,160 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
use aws_smithy_async::time::{SharedTimeSource, TimeSource};
    7      7   
use aws_smithy_observability::{
    8      8   
    global::get_telemetry_provider, instruments::Histogram, AttributeValue, Attributes,
    9      9   
    ObservabilityError,
   10     10   
};
          11  +
use aws_smithy_runtime_api::client::http::telemetry::{
          12  +
    CaptureHttpAttemptTelemetry, ConnectionAcquisitionTelemetry, HttpAttemptTelemetry,
          13  +
};
   11     14   
use aws_smithy_runtime_api::client::{
   12         -
    interceptors::{dyn_dispatch_hint, Intercept, SharedInterceptor},
          15  +
    interceptors::{
          16  +
        context::BeforeTransmitInterceptorContextMut, dyn_dispatch_hint, Intercept,
          17  +
        SharedInterceptor,
          18  +
    },
   13     19   
    orchestrator::Metadata,
   14     20   
    runtime_components::RuntimeComponentsBuilder,
   15     21   
    runtime_plugin::RuntimePlugin,
   16     22   
};
   17     23   
use aws_smithy_types::config_bag::{FrozenLayer, Layer, Storable, StoreReplace};
   18     24   
use aws_smithy_types::telemetry::{CapturedTelemetryAttributes, RequestedTelemetryAttributes};
   19     25   
use std::{borrow::Cow, sync::Arc, time::SystemTime};
   20     26   
   21     27   
/// Sets the outcome attributes (`error.type` and `http.status_code`) on `attrs` from a
   22     28   
/// finalizer-phase context.
   23     29   
fn add_outcome_attrs(
   24     30   
    attrs: &mut Attributes,
   25     31   
    context: &aws_smithy_runtime_api::client::interceptors::context::FinalizerInterceptorContextRef<
   26     32   
        '_,
   27     33   
    >,
   28     34   
) {
   29     35   
    // Coarse category only; the error is type-erased here, so the modeled name isn't reachable.
   30     36   
    // Absent on success, per OTel convention.
   31     37   
    if let Some(Err(err)) = context.output_or_error() {
   32     38   
        let category = if err.is_timeout_error() {
   33     39   
            "timeout"
   34     40   
        } else if err.is_connector_error() {
   35     41   
            "connector"
   36     42   
        } else if err.is_response_error() {
   37     43   
            "response"
   38     44   
        } else if err.is_operation_error() {
   39     45   
            "operation"
   40     46   
        } else {
   41     47   
            "other"
   42     48   
        };
   43     49   
        attrs.set("error.type", AttributeValue::String(category.into()));
   44     50   
    }
   45     51   
   46     52   
    // Raw HTTP status code, whenever a response reached us.
   47     53   
    if let Some(response) = context.response() {
   48     54   
        attrs.set(
   49     55   
            "http.status_code",
   50     56   
            AttributeValue::I64(i64::from(response.status().as_u16())),
   51     57   
        );
   52     58   
    }
   53     59   
}
   54     60   
   55     61   
/// Struct to hold metric data in the ConfigBag
   56     62   
#[derive(Debug, Clone)]
   57     63   
pub(crate) struct MeasurementsContainer {
   58     64   
    call_start: SystemTime,
   59     65   
    attempts: u32,
   60     66   
    attempt_start: SystemTime,
          67  +
    attempt_capture: Option<CaptureHttpAttemptTelemetry>,
   61     68   
}
   62     69   
   63     70   
impl Storable for MeasurementsContainer {
   64     71   
    type Storer = StoreReplace<Self>;
   65     72   
}
   66     73   
   67     74   
/// Instruments for recording a single operation
   68     75   
#[derive(Debug, Clone)]
   69     76   
pub(crate) struct OperationTelemetry {
   70     77   
    pub(crate) operation_duration: Arc<dyn Histogram>,
   71     78   
    pub(crate) attempt_duration: Arc<dyn Histogram>,
          79  +
    pub(crate) connection_acquisition_duration: Arc<dyn Histogram>,
   72     80   
    // Body sizes are their own instruments rather than attributes on the duration histogram: body
   73     81   
    // size is near-unique per call, so attaching it as a label would fragment the duration metric
   74     82   
    // into one time series per byte count.
   75     83   
    pub(crate) request_body_size: Arc<dyn Histogram>,
   76     84   
    pub(crate) response_body_size: Arc<dyn Histogram>,
   77     85   
}
   78     86   
   79     87   
impl OperationTelemetry {
   80     88   
    pub(crate) fn new(scope: &'static str) -> Result<Self, ObservabilityError> {
   81     89   
        let meter = get_telemetry_provider()?
   82     90   
            .meter_provider()
   83     91   
            .get_meter(scope, None);
   84     92   
   85     93   
        Ok(Self{
   86     94   
            operation_duration: meter
   87     95   
                .create_histogram("smithy.client.call.duration")
   88     96   
                .set_units("s")
   89     97   
                .set_description("Overall call duration (including retries and time to send or receive request and response body)")
   90     98   
                .build(),
   91     99   
            attempt_duration: meter
   92    100   
                .create_histogram("smithy.client.call.attempt.duration")
   93    101   
                .set_units("s")
   94    102   
                .set_description("The time it takes to connect to the service, send the request, and get back HTTP status code and headers (including time queued waiting to be sent)")
   95    103   
                .build(),
         104  +
            connection_acquisition_duration: meter
         105  +
                .create_histogram("smithy.client.http.connections.acquire_duration")
         106  +
                .set_units("s")
         107  +
                .set_description("Time spent acquiring the connection that accepted the request")
         108  +
                .build(),
   96    109   
            request_body_size: meter
   97    110   
                .create_histogram("smithy.client.call.request.size")
   98    111   
                .set_units("By")
   99    112   
                .set_description("Size of the transferred request body, in bytes")
  100    113   
                .build(),
  101    114   
            response_body_size: meter
  102    115   
                .create_histogram("smithy.client.call.response.size")
  103    116   
                .set_units("By")
  104    117   
                .set_description("Size of the transferred response body, in bytes")
  105    118   
                .build(),
  106    119   
        })
  107    120   
    }
         121  +
         122  +
    fn record_connection_acquisition(&self, telemetry: &HttpAttemptTelemetry) {
         123  +
        if let Some(duration) = telemetry
         124  +
            .acquisition()
         125  +
            .and_then(ConnectionAcquisitionTelemetry::duration)
         126  +
        {
         127  +
            self.connection_acquisition_duration
         128  +
                .record(duration.as_secs_f64(), None, None);
         129  +
        }
         130  +
    }
  108    131   
}
  109    132   
  110    133   
impl Storable for OperationTelemetry {
  111    134   
    type Storer = StoreReplace<Self>;
  112    135   
}
  113    136   
  114    137   
#[derive(Debug)]
  115    138   
pub(crate) struct MetricsInterceptor {
  116    139   
    // Holding a TimeSource here isn't ideal, but RuntimeComponents aren't available in
  117    140   
    // the read_before_execution hook and that is when we need to start the timer for
@@ -159,182 +283,361 @@
  179    202   
  180    203   
    fn read_before_execution(
  181    204   
        &self,
  182    205   
        _context: &aws_smithy_runtime_api::client::interceptors::context::BeforeSerializationInterceptorContextRef<'_>,
  183    206   
        cfg: &mut aws_smithy_types::config_bag::ConfigBag,
  184    207   
    ) -> Result<(), aws_smithy_runtime_api::box_error::BoxError> {
  185    208   
        cfg.interceptor_state().store_put(MeasurementsContainer {
  186    209   
            call_start: self.time_source.now(),
  187    210   
            attempts: 0,
  188    211   
            attempt_start: SystemTime::UNIX_EPOCH,
         212  +
            attempt_capture: None,
  189    213   
        });
  190    214   
  191    215   
        Ok(())
  192    216   
    }
  193    217   
  194    218   
    fn read_after_execution(
  195    219   
        &self,
  196    220   
        context: &aws_smithy_runtime_api::client::interceptors::context::FinalizerInterceptorContextRef<'_>,
  197    221   
        _runtime_components: &aws_smithy_runtime_api::client::runtime_components::RuntimeComponents,
  198    222   
        cfg: &mut aws_smithy_types::config_bag::ConfigBag,
  199    223   
    ) -> Result<(), aws_smithy_runtime_api::box_error::BoxError> {
  200    224   
        let (measurements, instruments) = self.get_measurements_and_instruments(cfg);
  201    225   
  202    226   
        let attributes = self.get_attrs_from_cfg(cfg);
  203    227   
  204    228   
        if let Some(mut attrs) = attributes {
  205    229   
            // The outcome is only known at the finalizer, so it is set here rather than in
  206    230   
            // `get_attrs_from_cfg` (which also serves the per-attempt path).
  207    231   
            add_outcome_attrs(&mut attrs, context);
  208    232   
  209    233   
            // Transferred byte sizes are recorded on their own instruments by the byte
  210    234   
            // interceptor (see `telemetry_bytes`), not as attributes on the duration histogram.
  211    235   
  212    236   
            let call_end = self.time_source.now();
  213    237   
            let call_duration = call_end.duration_since(measurements.call_start);
  214    238   
            if let Ok(elapsed) = call_duration {
  215    239   
                instruments
  216    240   
                    .operation_duration
  217    241   
                    .record(elapsed.as_secs_f64(), Some(&attrs), None);
  218    242   
            }
  219    243   
        }
  220    244   
  221    245   
        Ok(())
  222    246   
    }
  223    247   
  224    248   
    fn read_before_attempt(
  225    249   
        &self,
  226    250   
        _context: &aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextRef<'_>,
  227    251   
        _runtime_components: &aws_smithy_runtime_api::client::runtime_components::RuntimeComponents,
  228    252   
        cfg: &mut aws_smithy_types::config_bag::ConfigBag,
  229    253   
    ) -> Result<(), aws_smithy_runtime_api::box_error::BoxError> {
  230    254   
        let measurements = cfg
  231    255   
            .get_mut::<MeasurementsContainer>()
  232    256   
            .expect("set in `read_before_execution`");
  233    257   
  234    258   
        measurements.attempts += 1;
  235    259   
        measurements.attempt_start = self.time_source.now();
         260  +
        measurements.attempt_capture = None;
         261  +
         262  +
        Ok(())
         263  +
    }
         264  +
         265  +
    fn modify_before_transmit(
         266  +
        &self,
         267  +
        context: &mut BeforeTransmitInterceptorContextMut<'_>,
         268  +
        _runtime_components: &aws_smithy_runtime_api::client::runtime_components::RuntimeComponents,
         269  +
        _cfg: &mut aws_smithy_types::config_bag::ConfigBag,
         270  +
    ) -> Result<(), aws_smithy_runtime_api::box_error::BoxError> {
         271  +
        if context
         272  +
            .request()
         273  +
            .extension::<CaptureHttpAttemptTelemetry>()
         274  +
            .is_none()
         275  +
        {
         276  +
            context
         277  +
                .request_mut()
         278  +
                .add_extension(CaptureHttpAttemptTelemetry::new());
         279  +
        }
         280  +
        Ok(())
         281  +
    }
  236    282   
         283  +
    fn read_before_transmit(
         284  +
        &self,
         285  +
        context: &aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextRef<'_>,
         286  +
        _runtime_components: &aws_smithy_runtime_api::client::runtime_components::RuntimeComponents,
         287  +
        cfg: &mut aws_smithy_types::config_bag::ConfigBag,
         288  +
    ) -> Result<(), aws_smithy_runtime_api::box_error::BoxError> {
         289  +
        let capture = context
         290  +
            .request()
         291  +
            .extension::<CaptureHttpAttemptTelemetry>()
         292  +
            .cloned();
         293  +
        cfg.get_mut::<MeasurementsContainer>()
         294  +
            .expect("set in `read_before_execution`")
         295  +
            .attempt_capture = capture;
  237    296   
        Ok(())
  238    297   
    }
  239    298   
  240    299   
    fn read_after_attempt(
  241    300   
        &self,
  242    301   
        _context: &aws_smithy_runtime_api::client::interceptors::context::FinalizerInterceptorContextRef<'_>,
  243    302   
        _runtime_components: &aws_smithy_runtime_api::client::runtime_components::RuntimeComponents,
  244    303   
        cfg: &mut aws_smithy_types::config_bag::ConfigBag,
  245    304   
    ) -> Result<(), aws_smithy_runtime_api::box_error::BoxError> {
  246         -
        let (measurements, instruments) = self.get_measurements_and_instruments(cfg);
         305  +
        let (attempts, attempt_start, http_telemetry) = {
         306  +
            let measurements = cfg
         307  +
                .get_mut::<MeasurementsContainer>()
         308  +
                .expect("set in `read_before_execution`");
         309  +
            (
         310  +
                measurements.attempts,
         311  +
                measurements.attempt_start,
         312  +
                measurements
         313  +
                    .attempt_capture
         314  +
                    .take()
         315  +
                    .map(|capture| capture.get()),
         316  +
            )
         317  +
        };
         318  +
        let instruments = cfg
         319  +
            .load::<OperationTelemetry>()
         320  +
            .expect("set in RuntimePlugin");
  247    321   
  248    322   
        let attempt_end = self.time_source.now();
  249         -
        let attempt_duration = attempt_end.duration_since(measurements.attempt_start);
         323  +
        let attempt_duration = attempt_end.duration_since(attempt_start);
  250    324   
        let attributes = self.get_attrs_from_cfg(cfg);
  251    325   
         326  +
        if let Some(http_telemetry) = http_telemetry {
         327  +
            instruments.record_connection_acquisition(&http_telemetry);
         328  +
        }
         329  +
  252    330   
        if let (Ok(elapsed), Some(mut attrs)) = (attempt_duration, attributes) {
  253         -
            attrs.set("attempt", AttributeValue::I64(measurements.attempts.into()));
         331  +
            attrs.set("attempt", AttributeValue::I64(attempts.into()));
  254    332   
  255    333   
            instruments
  256    334   
                .attempt_duration
  257    335   
                .record(elapsed.as_secs_f64(), Some(&attrs), None);
  258    336   
        }
  259    337   
        Ok(())
  260    338   
    }
  261    339   
}
  262    340   
  263    341   
/// Runtime plugin that adds an interceptor for collecting metrics
@@ -336,414 +496,782 @@
  356    434   
        } else {
  357    435   
            Err("Scope is required for MetricsRuntimePlugin.".into())
  358    436   
        }
  359    437   
    }
  360    438   
}
  361    439   
  362    440   
#[cfg(test)]
  363    441   
mod test {
  364    442   
    use super::*;
  365    443   
    use aws_smithy_async::time::SystemTimeSource;
         444  +
    use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
         445  +
    use aws_smithy_runtime_api::client::http::telemetry::{
         446  +
        ConnectionAcquisitionTelemetry, ConnectionUsage,
         447  +
    };
         448  +
    use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
         449  +
    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
  366    450   
    use aws_smithy_types::config_bag::ConfigBag;
         451  +
    use std::sync::Mutex;
         452  +
         453  +
    #[derive(Debug, Default)]
         454  +
    struct RecordingHistogram {
         455  +
        records: Mutex<Vec<(f64, Option<Attributes>)>>,
         456  +
    }
         457  +
         458  +
    impl RecordingHistogram {
         459  +
        fn only_record(&self) -> (f64, Option<Attributes>) {
         460  +
            let records = self.records.lock().unwrap();
         461  +
            assert_eq!(records.len(), 1);
         462  +
            records[0].clone()
         463  +
        }
         464  +
         465  +
        fn record_count(&self) -> usize {
         466  +
            self.records.lock().unwrap().len()
         467  +
        }
         468  +
    }
         469  +
         470  +
    impl Histogram for RecordingHistogram {
         471  +
        fn record(
         472  +
            &self,
         473  +
            value: f64,
         474  +
            attributes: Option<&Attributes>,
         475  +
            _context: Option<&dyn aws_smithy_observability::Context>,
         476  +
        ) {
         477  +
            self.records
         478  +
                .lock()
         479  +
                .unwrap()
         480  +
                .push((value, attributes.cloned()));
         481  +
        }
         482  +
    }
  367    483   
  368    484   
    fn interceptor() -> MetricsInterceptor {
  369    485   
        MetricsInterceptor::new(SharedTimeSource::new(SystemTimeSource::new())).unwrap()
  370    486   
    }
  371    487   
  372    488   
    fn cfg_with(layer: Layer) -> ConfigBag {
  373    489   
        ConfigBag::of_layers(vec![layer])
  374    490   
    }
  375    491   
  376    492   
    fn string_attr<'a>(attrs: &'a Attributes, key: &str) -> Option<&'a str> {
  377    493   
        match attrs.get(key) {
  378    494   
            Some(AttributeValue::String(s)) => Some(s.as_str()),
  379    495   
            _ => None,
  380    496   
        }
  381    497   
    }
  382    498   
  383    499   
    #[test]
  384    500   
    fn base_attrs_are_service_and_method() {
  385    501   
        let mut layer = Layer::new("test");
  386    502   
        layer.store_put(Metadata::new("GetObject", "S3"));
  387    503   
  388    504   
        let attrs = interceptor()
  389    505   
            .get_attrs_from_cfg(&cfg_with(layer))
  390    506   
            .expect("metadata present");
  391    507   
  392    508   
        assert_eq!(Some("S3"), string_attr(&attrs, "rpc.service"));
  393    509   
        assert_eq!(Some("GetObject"), string_attr(&attrs, "rpc.method"));
  394    510   
    }
  395    511   
  396    512   
    #[test]
  397    513   
    fn no_attrs_without_metadata() {
  398    514   
        // Nothing to key the metric on, so no attributes are produced.
  399    515   
        assert!(interceptor()
  400    516   
            .get_attrs_from_cfg(&cfg_with(Layer::new("test")))
  401    517   
            .is_none());
  402    518   
    }
  403    519   
         520  +
    fn test_instruments(acquisition: Arc<RecordingHistogram>) -> OperationTelemetry {
         521  +
        let ignored: Arc<dyn Histogram> = Arc::new(RecordingHistogram::default());
         522  +
        OperationTelemetry {
         523  +
            operation_duration: ignored.clone(),
         524  +
            attempt_duration: ignored.clone(),
         525  +
            connection_acquisition_duration: acquisition,
         526  +
            request_body_size: ignored.clone(),
         527  +
            response_body_size: ignored,
         528  +
        }
         529  +
    }
         530  +
         531  +
    fn install_measurements(cfg: &mut ConfigBag) {
         532  +
        cfg.interceptor_state().store_put(MeasurementsContainer {
         533  +
            call_start: SystemTime::UNIX_EPOCH,
         534  +
            attempts: 0,
         535  +
            attempt_start: SystemTime::UNIX_EPOCH,
         536  +
            attempt_capture: None,
         537  +
        });
         538  +
    }
         539  +
         540  +
    #[test]
         541  +
    fn before_transmit_adopts_an_existing_attempt_capture() {
         542  +
        let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
         543  +
        let mut context = InterceptorContext::new(Input::doesnt_matter());
         544  +
        context.set_request(HttpRequest::empty());
         545  +
        let mut cfg = cfg_with(Layer::new("test"));
         546  +
        install_measurements(&mut cfg);
         547  +
        let caller_capture = CaptureHttpAttemptTelemetry::new();
         548  +
        context
         549  +
            .request_mut()
         550  +
            .unwrap()
         551  +
            .add_extension(caller_capture.clone());
         552  +
         553  +
        interceptor()
         554  +
            .modify_before_transmit(&mut (&mut context).into(), &runtime_components, &mut cfg)
         555  +
            .unwrap();
         556  +
        interceptor()
         557  +
            .read_before_transmit(&(&context).into(), &runtime_components, &mut cfg)
         558  +
            .unwrap();
         559  +
         560  +
        let request_capture = context
         561  +
            .request()
         562  +
            .unwrap()
         563  +
            .extension::<CaptureHttpAttemptTelemetry>()
         564  +
            .expect("request capture");
         565  +
        request_capture.record_connector_call_duration(std::time::Duration::from_millis(4));
         566  +
        assert_eq!(
         567  +
            cfg.load::<MeasurementsContainer>()
         568  +
                .and_then(|measurements| measurements.attempt_capture.as_ref())
         569  +
                .expect("attempt capture")
         570  +
                .get()
         571  +
                .connector_call_duration(),
         572  +
            Some(std::time::Duration::from_millis(4))
         573  +
        );
         574  +
        assert_eq!(
         575  +
            caller_capture.get().connector_call_duration(),
         576  +
            Some(std::time::Duration::from_millis(4))
         577  +
        );
         578  +
    }
         579  +
         580  +
    #[test]
         581  +
    fn before_transmit_adopts_the_final_replacement_capture() {
         582  +
        let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
         583  +
        let mut context = InterceptorContext::new(Input::doesnt_matter());
         584  +
        context.set_request(HttpRequest::empty());
         585  +
        let mut cfg = cfg_with(Layer::new("test"));
         586  +
        install_measurements(&mut cfg);
         587  +
         588  +
        interceptor()
         589  +
            .modify_before_transmit(&mut (&mut context).into(), &runtime_components, &mut cfg)
         590  +
            .unwrap();
         591  +
        let replacement = CaptureHttpAttemptTelemetry::new();
         592  +
        context
         593  +
            .request_mut()
         594  +
            .unwrap()
         595  +
            .add_extension(replacement.clone());
         596  +
        interceptor()
         597  +
            .read_before_transmit(&(&context).into(), &runtime_components, &mut cfg)
         598  +
            .unwrap();
         599  +
         600  +
        replacement.record_connector_call_duration(std::time::Duration::from_millis(6));
         601  +
        assert_eq!(
         602  +
            cfg.load::<MeasurementsContainer>()
         603  +
                .and_then(|measurements| measurements.attempt_capture.as_ref())
         604  +
                .expect("attempt capture")
         605  +
                .get()
         606  +
                .connector_call_duration(),
         607  +
            Some(std::time::Duration::from_millis(6))
         608  +
        );
         609  +
    }
         610  +
         611  +
    #[test]
         612  +
    fn attempt_capture_is_consumed_once_and_cleared_before_retry() {
         613  +
        let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
         614  +
        let acquisition = Arc::new(RecordingHistogram::default());
         615  +
        let mut layer = Layer::new("test");
         616  +
        layer.store_put(Metadata::new("GetObject", "S3"));
         617  +
        layer.store_put(test_instruments(acquisition.clone()));
         618  +
        let mut cfg = cfg_with(layer);
         619  +
        install_measurements(&mut cfg);
         620  +
        let mut context = InterceptorContext::new(Input::doesnt_matter());
         621  +
        context.set_request(HttpRequest::empty());
         622  +
        let metrics = interceptor();
         623  +
         624  +
        metrics
         625  +
            .read_before_attempt(&(&context).into(), &runtime_components, &mut cfg)
         626  +
            .unwrap();
         627  +
        metrics
         628  +
            .modify_before_transmit(&mut (&mut context).into(), &runtime_components, &mut cfg)
         629  +
            .unwrap();
         630  +
        metrics
         631  +
            .read_before_transmit(&(&context).into(), &runtime_components, &mut cfg)
         632  +
            .unwrap();
         633  +
        context
         634  +
            .request()
         635  +
            .unwrap()
         636  +
            .extension::<CaptureHttpAttemptTelemetry>()
         637  +
            .expect("request capture")
         638  +
            .record_connection_selection(
         639  +
                ConnectionAcquisitionTelemetry::new(
         640  +
                    std::time::Duration::from_millis(3),
         641  +
                    ConnectionUsage::Fresh,
         642  +
                ),
         643  +
                ConnectionMetadata::builder()
         644  +
                    .proxied(false)
         645  +
                    .poison_fn(|| {})
         646  +
                    .build(),
         647  +
            );
         648  +
        metrics
         649  +
            .read_after_attempt(&(&context).into(), &runtime_components, &mut cfg)
         650  +
            .unwrap();
         651  +
        assert_eq!(acquisition.record_count(), 1);
         652  +
         653  +
        metrics
         654  +
            .read_before_attempt(&(&context).into(), &runtime_components, &mut cfg)
         655  +
            .unwrap();
         656  +
        metrics
         657  +
            .read_after_attempt(&(&context).into(), &runtime_components, &mut cfg)
         658  +
            .unwrap();
         659  +
        assert_eq!(
         660  +
            acquisition.record_count(),
         661  +
            1,
         662  +
            "a pre-transmit retry halt must not repeat the prior acquisition"
         663  +
        );
         664  +
    }
         665  +
         666  +
    #[test]
         667  +
    fn records_connection_acquisition_without_attributes() {
         668  +
        let acquisition = Arc::new(RecordingHistogram::default());
         669  +
        let instruments = test_instruments(acquisition.clone());
         670  +
        let capture = CaptureHttpAttemptTelemetry::new();
         671  +
        capture.record_connector_call_duration(std::time::Duration::from_millis(7));
         672  +
        capture.record_connection_selection(
         673  +
            ConnectionAcquisitionTelemetry::new(
         674  +
                std::time::Duration::from_millis(3),
         675  +
                ConnectionUsage::Reused,
         676  +
            ),
         677  +
            ConnectionMetadata::builder()
         678  +
                .proxied(false)
         679  +
                .poison_fn(|| {})
         680  +
                .build(),
         681  +
        );
         682  +
        instruments.record_connection_acquisition(&capture.get());
         683  +
         684  +
        let (acquisition_value, acquisition_attrs) = acquisition.only_record();
         685  +
        assert_eq!(
         686  +
            acquisition_value,
         687  +
            std::time::Duration::from_millis(3).as_secs_f64()
         688  +
        );
         689  +
        assert!(acquisition_attrs.is_none());
         690  +
    }
         691  +
  404    692   
    #[test]
  405    693   
    fn emitted_members_are_merged_onto_attrs() {
  406    694   
        let mut captured = CapturedTelemetryAttributes::new();
  407    695   
        captured.insert("Bucket", "my-bucket");
  408    696   
  409    697   
        let mut layer = Layer::new("test");
  410    698   
        layer.store_put(Metadata::new("GetObject", "S3"));
  411    699   
        layer.store_put(captured);
  412    700   
        layer.store_put(RequestedTelemetryAttributes::new(["Bucket"]));
  413    701   
  414    702   
        let attrs = interceptor()
  415    703   
            .get_attrs_from_cfg(&cfg_with(layer))
  416    704   
            .expect("metadata present");
  417    705   
  418    706   
        // The emitted input member rides alongside the built-in rpc.* attributes.
  419    707   
        assert_eq!(Some("my-bucket"), string_attr(&attrs, "Bucket"));
  420    708   
        assert_eq!(Some("S3"), string_attr(&attrs, "rpc.service"));
  421    709   
    }
  422    710   
  423    711   
    #[test]
  424    712   
    fn capture_only_members_are_not_emitted() {
  425    713   
        // A value captured for in-process reads must not land on the metric.
  426    714   
        let mut captured = CapturedTelemetryAttributes::new();
  427    715   
        captured.insert("Prefix", "logs/");
  428    716   
  429    717   
        let mut requested = RequestedTelemetryAttributes::default();
  430    718   
        requested.capture_only(["Prefix"]);
  431    719   
  432    720   
        let mut layer = Layer::new("test");
  433    721   
        layer.store_put(Metadata::new("GetObject", "S3"));
  434    722   
        layer.store_put(captured);
  435    723   
        layer.store_put(requested);
  436    724   
  437    725   
        let attrs = interceptor()
  438    726   
            .get_attrs_from_cfg(&cfg_with(layer))
  439    727   
            .expect("metadata present");
  440    728   
  441    729   
        assert!(
  442    730   
            attrs.get("Prefix").is_none(),
  443    731   
            "capture-only member must not be emitted on the metric"
  444    732   
        );
  445    733   
    }
  446    734   
  447    735   
    #[test]
  448    736   
    fn nothing_captured_leaves_only_base_attrs() {
  449    737   
        // Opt-in is off by default: an empty capture set adds nothing.
  450    738   
        let mut layer = Layer::new("test");
  451    739   
        layer.store_put(Metadata::new("GetObject", "S3"));
  452    740   
        layer.store_put(CapturedTelemetryAttributes::new());
  453    741   
  454    742   
        let attrs = interceptor()
  455    743   
            .get_attrs_from_cfg(&cfg_with(layer))
  456    744   
            .expect("metadata present");
  457    745   
  458    746   
        assert_eq!(Some("GetObject"), string_attr(&attrs, "rpc.method"));
  459    747   
        assert!(attrs.get("Bucket").is_none());
  460    748   
    }
  461    749   
  462    750   
    // --- add_outcome_attrs (the `status` dimension) ---
  463    751   
  464         -
    use aws_smithy_runtime_api::client::interceptors::context::{
  465         -
        Error, Input, InterceptorContext, Output,
  466         -
    };
         752  +
    use aws_smithy_runtime_api::client::interceptors::context::{Error, Output};
  467    753   
    use aws_smithy_runtime_api::client::orchestrator::OrchestratorError;
  468    754   
    use aws_smithy_runtime_api::client::result::ConnectorError;
  469    755   
    use aws_smithy_runtime_api::http::{Response, StatusCode};
  470    756   
    use aws_smithy_types::body::SdkBody;
  471    757   
  472    758   
    fn i64_attr(attrs: &Attributes, key: &str) -> Option<i64> {
  473    759   
        match attrs.get(key) {
  474    760   
            Some(AttributeValue::I64(v)) => Some(*v),
  475    761   
            _ => None,
  476    762   
        }