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