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