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