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