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