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