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