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