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            AccountIdModePreferred => Some(BusinessMetric::AccountIdModePreferred),
220            AccountIdModeDisabled => Some(BusinessMetric::AccountIdModeDisabled),
221            AccountIdModeRequired => Some(BusinessMetric::AccountIdModeRequired),
222            S3Transfer => Some(BusinessMetric::S3Transfer),
223            SsoLoginDevice => Some(BusinessMetric::SsoLoginDevice),
224            SsoLoginAuth => Some(BusinessMetric::SsoLoginAuth),
225        }
226    }
227}
228
229impl ProvideBusinessMetric for AwsCredentialFeature {
230    fn provide_business_metric(&self) -> Option<BusinessMetric> {
231        use AwsCredentialFeature::*;
232        match self {
233            ResolvedAccountId => Some(BusinessMetric::ResolvedAccountId),
234            CredentialsCode => Some(BusinessMetric::CredentialsCode),
235            CredentialsEnvVars => Some(BusinessMetric::CredentialsEnvVars),
236            CredentialsEnvVarsStsWebIdToken => {
237                Some(BusinessMetric::CredentialsEnvVarsStsWebIdToken)
238            }
239            CredentialsStsAssumeRole => Some(BusinessMetric::CredentialsStsAssumeRole),
240            CredentialsStsAssumeRoleSaml => Some(BusinessMetric::CredentialsStsAssumeRoleSaml),
241            CredentialsStsAssumeRoleWebId => Some(BusinessMetric::CredentialsStsAssumeRoleWebId),
242            CredentialsStsFederationToken => Some(BusinessMetric::CredentialsStsFederationToken),
243            CredentialsStsSessionToken => Some(BusinessMetric::CredentialsStsSessionToken),
244            CredentialsProfile => Some(BusinessMetric::CredentialsProfile),
245            CredentialsProfileSourceProfile => {
246                Some(BusinessMetric::CredentialsProfileSourceProfile)
247            }
248            CredentialsProfileNamedProvider => {
249                Some(BusinessMetric::CredentialsProfileNamedProvider)
250            }
251            CredentialsProfileStsWebIdToken => {
252                Some(BusinessMetric::CredentialsProfileStsWebIdToken)
253            }
254            CredentialsProfileSso => Some(BusinessMetric::CredentialsProfileSso),
255            CredentialsSso => Some(BusinessMetric::CredentialsSso),
256            CredentialsProfileProcess => Some(BusinessMetric::CredentialsProfileProcess),
257            CredentialsProcess => Some(BusinessMetric::CredentialsProcess),
258            CredentialsHttp => Some(BusinessMetric::CredentialsHttp),
259            CredentialsImds => Some(BusinessMetric::CredentialsImds),
260            BearerServiceEnvVars => Some(BusinessMetric::BearerServiceEnvVars),
261            S3ExpressBucket => Some(BusinessMetric::S3ExpressBucket),
262            otherwise => {
263                // This may occur if a customer upgrades only the `aws-smithy-runtime-api` crate
264                // while continuing to use an outdated version of an SDK crate or the `aws-credential-types`
265                // crate.
266                tracing::warn!(
267                    "Attempted to provide `BusinessMetric` for `{otherwise:?}`, which is not recognized in the current version of the `aws-runtime` crate. \
268                    Consider upgrading to the latest version to ensure that all tracked features are properly reported in your metrics."
269                );
270                None
271            }
272        }
273    }
274}
275
276#[derive(Clone, Debug, Default)]
277pub(super) struct BusinessMetrics(Vec<BusinessMetric>);
278
279impl BusinessMetrics {
280    pub(super) fn push(&mut self, metric: BusinessMetric) {
281        self.0.push(metric);
282    }
283
284    pub(super) fn is_empty(&self) -> bool {
285        self.0.is_empty()
286    }
287}
288
289fn drop_unfinished_metrics_to_fit(csv: &str, max_len: usize) -> Cow<'_, str> {
290    if csv.len() <= max_len {
291        Cow::Borrowed(csv)
292    } else {
293        let truncated = &csv[..max_len];
294        if let Some(pos) = truncated.rfind(',') {
295            Cow::Owned(truncated[..pos].to_owned())
296        } else {
297            Cow::Owned(truncated.to_owned())
298        }
299    }
300}
301
302impl fmt::Display for BusinessMetrics {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        // business-metrics = "m/" metric_id *(comma metric_id)
305        let metrics_values = self
306            .0
307            .iter()
308            .map(|feature_id| {
309                FEATURE_ID_TO_METRIC_VALUE
310                    .get(feature_id)
311                    .expect("{feature_id:?} should be found in `FEATURE_ID_TO_METRIC_VALUE`")
312                    .clone()
313            })
314            .collect::<Vec<_>>()
315            .join(",");
316
317        let metrics_values = drop_unfinished_metrics_to_fit(
318            &metrics_values,
319            MAX_COMMA_SEPARATED_METRICS_VALUES_LENGTH,
320        );
321
322        write!(f, "m/{}", metrics_values)
323    }
324}
325#[cfg(test)]
326mod tests {
327    use crate::user_agent::metrics::{
328        drop_unfinished_metrics_to_fit, Base64Iterator, FEATURE_ID_TO_METRIC_VALUE,
329        MAX_METRICS_ID_NUMBER,
330    };
331    use crate::user_agent::BusinessMetric;
332    use convert_case::{Boundary, Case, Casing};
333    use std::collections::HashMap;
334    use std::fmt::{Display, Formatter};
335
336    impl Display for BusinessMetric {
337        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
338            f.write_str(
339                &format!("{:?}", self)
340                    .as_str()
341                    .from_case(Case::Pascal)
342                    .with_boundaries(&[Boundary::DigitUpper, Boundary::LowerUpper])
343                    .to_case(Case::ScreamingSnake),
344            )
345        }
346    }
347
348    #[test]
349    fn feature_id_to_metric_value() {
350        const EXPECTED: &str = include_str!("test_data/feature_id_to_metric_value.json");
351
352        let expected: HashMap<&str, &str> = serde_json::from_str(EXPECTED).unwrap();
353        assert_eq!(expected.len(), FEATURE_ID_TO_METRIC_VALUE.len());
354
355        for (feature_id, metric_value) in &*FEATURE_ID_TO_METRIC_VALUE {
356            let expected = expected.get(format!("{feature_id}").as_str());
357            assert_eq!(
358                expected.unwrap_or_else(|| panic!("Expected {feature_id} to have value `{metric_value}` but it was `{expected:?}` instead.")),
359                metric_value,
360            );
361        }
362    }
363
364    #[test]
365    fn test_base64_iter() {
366        // 350 is the max number of metric IDs we support for now
367        let ids: Vec<String> = Base64Iterator::new().take(MAX_METRICS_ID_NUMBER).collect();
368        assert_eq!("A", ids[0]);
369        assert_eq!("Z", ids[25]);
370        assert_eq!("a", ids[26]);
371        assert_eq!("z", ids[51]);
372        assert_eq!("0", ids[52]);
373        assert_eq!("9", ids[61]);
374        assert_eq!("+", ids[62]);
375        assert_eq!("-", ids[63]);
376        assert_eq!("AA", ids[64]);
377        assert_eq!("AB", ids[65]);
378        assert_eq!("A-", ids[127]);
379        assert_eq!("BA", ids[128]);
380        assert_eq!("Ed", ids[349]);
381    }
382
383    #[test]
384    fn test_drop_unfinished_metrics_to_fit() {
385        let csv = "A,10BC,E";
386        assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
387
388        let csv = "A10B,CE";
389        assert_eq!("A10B", drop_unfinished_metrics_to_fit(csv, 5));
390
391        let csv = "A10BC,E";
392        assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
393
394        let csv = "A10BCE";
395        assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
396
397        let csv = "A";
398        assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
399
400        let csv = "A,B";
401        assert_eq!("A,B", drop_unfinished_metrics_to_fit(csv, 5));
402    }
403}