Skip to main content

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