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