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