aws_runtime/user_agent/
metrics.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::sdk_feature::AwsSdkFeature;
7use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
8use once_cell::sync::Lazy;
9use std::borrow::Cow;
10use std::collections::HashMap;
11use std::fmt;
12
13const MAX_COMMA_SEPARATED_METRICS_VALUES_LENGTH: usize = 1024;
14#[allow(dead_code)]
15const MAX_METRICS_ID_NUMBER: usize = 350;
16
17macro_rules! iterable_enum {
18    ($docs:tt, $enum_name:ident, $( $variant:ident ),*) => {
19        #[derive(Clone, Debug, Eq, Hash, PartialEq)]
20        #[non_exhaustive]
21        #[doc = $docs]
22        #[allow(missing_docs)] // for variants, not for the Enum itself
23        pub enum $enum_name {
24            $( $variant ),*
25        }
26
27        #[allow(dead_code)]
28        impl $enum_name {
29            pub(crate) fn iter() -> impl Iterator<Item = &'static $enum_name> {
30                const VARIANTS: &[$enum_name] = &[
31                    $( $enum_name::$variant ),*
32                ];
33                VARIANTS.iter()
34            }
35        }
36    };
37}
38
39struct Base64Iterator {
40    current: Vec<usize>,
41    base64_chars: Vec<char>,
42}
43
44impl Base64Iterator {
45    #[allow(dead_code)]
46    fn new() -> Self {
47        Base64Iterator {
48            current: vec![0], // Start with the first character
49            base64_chars: (b'A'..=b'Z') // 'A'-'Z'
50                .chain(b'a'..=b'z') // 'a'-'z'
51                .chain(b'0'..=b'9') // '0'-'9'
52                .chain([b'+', b'-']) // '+' and '-'
53                .map(|c| c as char)
54                .collect(),
55        }
56    }
57
58    fn increment(&mut self) {
59        let mut i = 0;
60        while i < self.current.len() {
61            self.current[i] += 1;
62            if self.current[i] < self.base64_chars.len() {
63                // The value at current position hasn't reached 64
64                return;
65            }
66            self.current[i] = 0;
67            i += 1;
68        }
69        self.current.push(0); // Add new digit if all positions overflowed
70    }
71}
72
73impl Iterator for Base64Iterator {
74    type Item = String;
75
76    fn next(&mut self) -> Option<Self::Item> {
77        if self.current.is_empty() {
78            return None; // No more items
79        }
80
81        // Convert the current indices to characters
82        let result: String = self
83            .current
84            .iter()
85            .rev()
86            .map(|&idx| self.base64_chars[idx])
87            .collect();
88
89        // Increment to the next value
90        self.increment();
91        Some(result)
92    }
93}
94
95pub(super) static FEATURE_ID_TO_METRIC_VALUE: Lazy<HashMap<BusinessMetric, Cow<'static, str>>> =
96    Lazy::new(|| {
97        let mut m = HashMap::new();
98        for (metric, value) in BusinessMetric::iter()
99            .cloned()
100            .zip(Base64Iterator::new())
101            .take(MAX_METRICS_ID_NUMBER)
102        {
103            m.insert(metric, Cow::Owned(value));
104        }
105        m
106    });
107
108iterable_enum!(
109    "Enumerates human readable identifiers for the features tracked by metrics",
110    BusinessMetric,
111    ResourceModel,
112    Waiter,
113    Paginator,
114    RetryModeLegacy,
115    RetryModeStandard,
116    RetryModeAdaptive,
117    S3Transfer,
118    S3CryptoV1n,
119    S3CryptoV2,
120    S3ExpressBucket,
121    S3AccessGrants,
122    GzipRequestCompression,
123    ProtocolRpcV2Cbor,
124    EndpointOverride,
125    AccountIdEndpoint,
126    AccountIdModePreferred,
127    AccountIdModeDisabled,
128    AccountIdModeRequired,
129    Sigv4aSigning,
130    ResolvedAccountId,
131    FlexibleChecksumsReqCrc32,
132    FlexibleChecksumsReqCrc32c,
133    FlexibleChecksumsReqCrc64,
134    FlexibleChecksumsReqSha1,
135    FlexibleChecksumsReqSha256,
136    FlexibleChecksumsReqWhenSupported,
137    FlexibleChecksumsReqWhenRequired,
138    FlexibleChecksumsResWhenSupported,
139    FlexibleChecksumsResWhenRequired
140);
141
142pub(crate) trait ProvideBusinessMetric {
143    fn provide_business_metric(&self) -> Option<BusinessMetric>;
144}
145
146impl ProvideBusinessMetric for SmithySdkFeature {
147    fn provide_business_metric(&self) -> Option<BusinessMetric> {
148        use SmithySdkFeature::*;
149        match self {
150            Waiter => Some(BusinessMetric::Waiter),
151            Paginator => Some(BusinessMetric::Paginator),
152            GzipRequestCompression => Some(BusinessMetric::GzipRequestCompression),
153            ProtocolRpcV2Cbor => Some(BusinessMetric::ProtocolRpcV2Cbor),
154            RetryModeStandard => Some(BusinessMetric::RetryModeStandard),
155            RetryModeAdaptive => Some(BusinessMetric::RetryModeAdaptive),
156            FlexibleChecksumsReqCrc32 => Some(BusinessMetric::FlexibleChecksumsReqCrc32),
157            FlexibleChecksumsReqCrc32c => Some(BusinessMetric::FlexibleChecksumsReqCrc32c),
158            FlexibleChecksumsReqCrc64 => Some(BusinessMetric::FlexibleChecksumsReqCrc64),
159            FlexibleChecksumsReqSha1 => Some(BusinessMetric::FlexibleChecksumsReqSha1),
160            FlexibleChecksumsReqSha256 => Some(BusinessMetric::FlexibleChecksumsReqSha256),
161            FlexibleChecksumsReqWhenSupported => {
162                Some(BusinessMetric::FlexibleChecksumsReqWhenSupported)
163            }
164            FlexibleChecksumsReqWhenRequired => {
165                Some(BusinessMetric::FlexibleChecksumsReqWhenRequired)
166            }
167            FlexibleChecksumsResWhenSupported => {
168                Some(BusinessMetric::FlexibleChecksumsResWhenSupported)
169            }
170            FlexibleChecksumsResWhenRequired => {
171                Some(BusinessMetric::FlexibleChecksumsResWhenRequired)
172            }
173            otherwise => {
174                // This may occur if a customer upgrades only the `aws-smithy-runtime-api` crate
175                // while continuing to use an outdated version of an SDK crate or the `aws-runtime`
176                // crate.
177                tracing::warn!(
178                    "Attempted to provide `BusinessMetric` for `{otherwise:?}`, which is not recognized in the current version of the `aws-runtime` crate. \
179                    Consider upgrading to the latest version to ensure that all tracked features are properly reported in your metrics."
180                );
181                None
182            }
183        }
184    }
185}
186
187impl ProvideBusinessMetric for AwsSdkFeature {
188    fn provide_business_metric(&self) -> Option<BusinessMetric> {
189        use AwsSdkFeature::*;
190        match self {
191            S3Transfer => Some(BusinessMetric::S3Transfer),
192        }
193    }
194}
195
196#[derive(Clone, Debug, Default)]
197pub(super) struct BusinessMetrics(Vec<BusinessMetric>);
198
199impl BusinessMetrics {
200    pub(super) fn push(&mut self, metric: BusinessMetric) {
201        self.0.push(metric);
202    }
203
204    pub(super) fn is_empty(&self) -> bool {
205        self.0.is_empty()
206    }
207}
208
209fn drop_unfinished_metrics_to_fit(csv: &str, max_len: usize) -> Cow<'_, str> {
210    if csv.len() <= max_len {
211        Cow::Borrowed(csv)
212    } else {
213        let truncated = &csv[..max_len];
214        if let Some(pos) = truncated.rfind(',') {
215            Cow::Owned(truncated[..pos].to_owned())
216        } else {
217            Cow::Owned(truncated.to_owned())
218        }
219    }
220}
221
222impl fmt::Display for BusinessMetrics {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        // business-metrics = "m/" metric_id *(comma metric_id)
225        let metrics_values = self
226            .0
227            .iter()
228            .map(|feature_id| {
229                FEATURE_ID_TO_METRIC_VALUE
230                    .get(feature_id)
231                    .expect("{feature_id:?} should be found in `FEATURE_ID_TO_METRIC_VALUE`")
232                    .clone()
233            })
234            .collect::<Vec<_>>()
235            .join(",");
236
237        let metrics_values = drop_unfinished_metrics_to_fit(
238            &metrics_values,
239            MAX_COMMA_SEPARATED_METRICS_VALUES_LENGTH,
240        );
241
242        write!(f, "m/{}", metrics_values)
243    }
244}
245#[cfg(test)]
246mod tests {
247    use crate::user_agent::metrics::{
248        drop_unfinished_metrics_to_fit, Base64Iterator, FEATURE_ID_TO_METRIC_VALUE,
249        MAX_METRICS_ID_NUMBER,
250    };
251    use crate::user_agent::BusinessMetric;
252    use convert_case::{Boundary, Case, Casing};
253    use std::collections::HashMap;
254    use std::fmt::{Display, Formatter};
255
256    impl Display for BusinessMetric {
257        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
258            f.write_str(
259                &format!("{:?}", self)
260                    .as_str()
261                    .from_case(Case::Pascal)
262                    .with_boundaries(&[Boundary::DigitUpper, Boundary::LowerUpper])
263                    .to_case(Case::ScreamingSnake),
264            )
265        }
266    }
267
268    #[test]
269    fn feature_id_to_metric_value() {
270        const EXPECTED: &str = r#"
271{
272  "RESOURCE_MODEL": "A",
273  "WAITER": "B",
274  "PAGINATOR": "C",
275  "RETRY_MODE_LEGACY": "D",
276  "RETRY_MODE_STANDARD": "E",
277  "RETRY_MODE_ADAPTIVE": "F",
278  "S3_TRANSFER": "G",
279  "S3_CRYPTO_V1N": "H",
280  "S3_CRYPTO_V2": "I",
281  "S3_EXPRESS_BUCKET": "J",
282  "S3_ACCESS_GRANTS": "K",
283  "GZIP_REQUEST_COMPRESSION": "L",
284  "PROTOCOL_RPC_V2_CBOR": "M",
285  "ENDPOINT_OVERRIDE": "N",
286  "ACCOUNT_ID_ENDPOINT": "O",
287  "ACCOUNT_ID_MODE_PREFERRED": "P",
288  "ACCOUNT_ID_MODE_DISABLED": "Q",
289  "ACCOUNT_ID_MODE_REQUIRED": "R",
290  "SIGV4A_SIGNING": "S",
291  "RESOLVED_ACCOUNT_ID": "T",
292  "FLEXIBLE_CHECKSUMS_REQ_CRC32" : "U",
293  "FLEXIBLE_CHECKSUMS_REQ_CRC32C" : "V",
294  "FLEXIBLE_CHECKSUMS_REQ_CRC64" : "W",
295  "FLEXIBLE_CHECKSUMS_REQ_SHA1" : "X",
296  "FLEXIBLE_CHECKSUMS_REQ_SHA256" : "Y",
297  "FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED" : "Z",
298  "FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED" : "a",
299  "FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED" : "b",
300  "FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED" : "c"
301}
302        "#;
303
304        let expected: HashMap<&str, &str> = serde_json::from_str(EXPECTED).unwrap();
305        assert_eq!(expected.len(), FEATURE_ID_TO_METRIC_VALUE.len());
306
307        for (feature_id, metric_value) in &*FEATURE_ID_TO_METRIC_VALUE {
308            let expected = expected.get(format!("{feature_id}").as_str());
309            assert_eq!(
310                expected.expect(&format!("Expected {feature_id} to have value `{metric_value}` but it was `{expected:?}` instead.")),
311                metric_value,
312            );
313        }
314    }
315
316    #[test]
317    fn test_base64_iter() {
318        // 350 is the max number of metric IDs we support for now
319        let ids: Vec<String> = Base64Iterator::new()
320            .into_iter()
321            .take(MAX_METRICS_ID_NUMBER)
322            .collect();
323        assert_eq!("A", ids[0]);
324        assert_eq!("Z", ids[25]);
325        assert_eq!("a", ids[26]);
326        assert_eq!("z", ids[51]);
327        assert_eq!("0", ids[52]);
328        assert_eq!("9", ids[61]);
329        assert_eq!("+", ids[62]);
330        assert_eq!("-", ids[63]);
331        assert_eq!("AA", ids[64]);
332        assert_eq!("AB", ids[65]);
333        assert_eq!("A-", ids[127]);
334        assert_eq!("BA", ids[128]);
335        assert_eq!("Ed", ids[349]);
336    }
337
338    #[test]
339    fn test_drop_unfinished_metrics_to_fit() {
340        let csv = "A,10BC,E";
341        assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
342
343        let csv = "A10B,CE";
344        assert_eq!("A10B", drop_unfinished_metrics_to_fit(csv, 5));
345
346        let csv = "A10BC,E";
347        assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
348
349        let csv = "A10BCE";
350        assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
351
352        let csv = "A";
353        assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
354
355        let csv = "A,B";
356        assert_eq!("A,B", drop_unfinished_metrics_to_fit(csv, 5));
357    }
358}