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 ObservabilityTracing => Some(BusinessMetric::ObservabilityTracing),
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 ObservabilityMetrics => Some(BusinessMetric::ObservabilityMetrics),
231 ObservabilityOtelTracing => Some(BusinessMetric::ObservabilityOtelTracing),
232 ObservabilityOtelMetrics => Some(BusinessMetric::ObservabilityOtelMetrics),
233 EndpointOverride => Some(BusinessMetric::EndpointOverride),
234 }
235 }
236}
237
238impl ProvideBusinessMetric for AwsCredentialFeature {
239 fn provide_business_metric(&self) -> Option<BusinessMetric> {
240 use AwsCredentialFeature::*;
241 match self {
242 ResolvedAccountId => Some(BusinessMetric::ResolvedAccountId),
243 CredentialsCode => Some(BusinessMetric::CredentialsCode),
244 CredentialsEnvVars => Some(BusinessMetric::CredentialsEnvVars),
245 CredentialsEnvVarsStsWebIdToken => {
246 Some(BusinessMetric::CredentialsEnvVarsStsWebIdToken)
247 }
248 CredentialsStsAssumeRole => Some(BusinessMetric::CredentialsStsAssumeRole),
249 CredentialsStsAssumeRoleSaml => Some(BusinessMetric::CredentialsStsAssumeRoleSaml),
250 CredentialsStsAssumeRoleWebId => Some(BusinessMetric::CredentialsStsAssumeRoleWebId),
251 CredentialsStsFederationToken => Some(BusinessMetric::CredentialsStsFederationToken),
252 CredentialsStsSessionToken => Some(BusinessMetric::CredentialsStsSessionToken),
253 CredentialsProfile => Some(BusinessMetric::CredentialsProfile),
254 CredentialsProfileSourceProfile => {
255 Some(BusinessMetric::CredentialsProfileSourceProfile)
256 }
257 CredentialsProfileNamedProvider => {
258 Some(BusinessMetric::CredentialsProfileNamedProvider)
259 }
260 CredentialsProfileStsWebIdToken => {
261 Some(BusinessMetric::CredentialsProfileStsWebIdToken)
262 }
263 CredentialsProfileSso => Some(BusinessMetric::CredentialsProfileSso),
264 CredentialsSso => Some(BusinessMetric::CredentialsSso),
265 CredentialsProfileProcess => Some(BusinessMetric::CredentialsProfileProcess),
266 CredentialsProcess => Some(BusinessMetric::CredentialsProcess),
267 CredentialsHttp => Some(BusinessMetric::CredentialsHttp),
268 CredentialsImds => Some(BusinessMetric::CredentialsImds),
269 BearerServiceEnvVars => Some(BusinessMetric::BearerServiceEnvVars),
270 S3ExpressBucket => Some(BusinessMetric::S3ExpressBucket),
271 otherwise => {
272 tracing::warn!(
276 "Attempted to provide `BusinessMetric` for `{otherwise:?}`, which is not recognized in the current version of the `aws-runtime` crate. \
277 Consider upgrading to the latest version to ensure that all tracked features are properly reported in your metrics."
278 );
279 None
280 }
281 }
282 }
283}
284
285#[derive(Clone, Debug, Default)]
286pub(super) struct BusinessMetrics(Vec<BusinessMetric>);
287
288impl BusinessMetrics {
289 pub(super) fn push(&mut self, metric: BusinessMetric) {
290 self.0.push(metric);
291 }
292
293 pub(super) fn is_empty(&self) -> bool {
294 self.0.is_empty()
295 }
296}
297
298fn drop_unfinished_metrics_to_fit(csv: &str, max_len: usize) -> Cow<'_, str> {
299 if csv.len() <= max_len {
300 Cow::Borrowed(csv)
301 } else {
302 let truncated = &csv[..max_len];
303 if let Some(pos) = truncated.rfind(',') {
304 Cow::Owned(truncated[..pos].to_owned())
305 } else {
306 Cow::Owned(truncated.to_owned())
307 }
308 }
309}
310
311impl fmt::Display for BusinessMetrics {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 let metrics_values = self
315 .0
316 .iter()
317 .map(|feature_id| {
318 FEATURE_ID_TO_METRIC_VALUE
319 .get(feature_id)
320 .expect("{feature_id:?} should be found in `FEATURE_ID_TO_METRIC_VALUE`")
321 .clone()
322 })
323 .collect::<Vec<_>>()
324 .join(",");
325
326 let metrics_values = drop_unfinished_metrics_to_fit(
327 &metrics_values,
328 MAX_COMMA_SEPARATED_METRICS_VALUES_LENGTH,
329 );
330
331 write!(f, "m/{metrics_values}")
332 }
333}
334#[cfg(test)]
335mod tests {
336 use crate::user_agent::metrics::{
337 drop_unfinished_metrics_to_fit, Base64Iterator, FEATURE_ID_TO_METRIC_VALUE,
338 MAX_METRICS_ID_NUMBER,
339 };
340 use crate::user_agent::BusinessMetric;
341 use convert_case::{Boundary, Case, Casing};
342 use std::collections::HashMap;
343 use std::fmt::{Display, Formatter};
344
345 impl Display for BusinessMetric {
346 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
347 f.write_str(
348 &format!("{:?}", self)
349 .as_str()
350 .from_case(Case::Pascal)
351 .with_boundaries(&[Boundary::DigitUpper, Boundary::LowerUpper])
352 .to_case(Case::ScreamingSnake),
353 )
354 }
355 }
356
357 #[test]
358 fn feature_id_to_metric_value() {
359 const EXPECTED: &str = include_str!("test_data/feature_id_to_metric_value.json");
360
361 let expected: HashMap<&str, &str> = serde_json::from_str(EXPECTED).unwrap();
362 assert_eq!(expected.len(), FEATURE_ID_TO_METRIC_VALUE.len());
363
364 for (feature_id, metric_value) in &*FEATURE_ID_TO_METRIC_VALUE {
365 let expected = expected.get(format!("{feature_id}").as_str());
366 assert_eq!(
367 expected.unwrap_or_else(|| panic!("Expected {feature_id} to have value `{metric_value}` but it was `{expected:?}` instead.")),
368 metric_value,
369 );
370 }
371 }
372
373 #[test]
374 fn test_base64_iter() {
375 let ids: Vec<String> = Base64Iterator::new().take(MAX_METRICS_ID_NUMBER).collect();
377 assert_eq!("A", ids[0]);
378 assert_eq!("Z", ids[25]);
379 assert_eq!("a", ids[26]);
380 assert_eq!("z", ids[51]);
381 assert_eq!("0", ids[52]);
382 assert_eq!("9", ids[61]);
383 assert_eq!("+", ids[62]);
384 assert_eq!("-", ids[63]);
385 assert_eq!("AA", ids[64]);
386 assert_eq!("AB", ids[65]);
387 assert_eq!("A-", ids[127]);
388 assert_eq!("BA", ids[128]);
389 assert_eq!("Ed", ids[349]);
390 }
391
392 #[test]
393 fn test_drop_unfinished_metrics_to_fit() {
394 let csv = "A,10BC,E";
395 assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
396
397 let csv = "A10B,CE";
398 assert_eq!("A10B", drop_unfinished_metrics_to_fit(csv, 5));
399
400 let csv = "A10BC,E";
401 assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
402
403 let csv = "A10BCE";
404 assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
405
406 let csv = "A";
407 assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
408
409 let csv = "A,B";
410 assert_eq!("A,B", drop_unfinished_metrics_to_fit(csv, 5));
411 }
412
413 #[test]
414 fn test_aws_sdk_feature_mappings() {
415 use crate::sdk_feature::AwsSdkFeature;
416 use crate::user_agent::metrics::ProvideBusinessMetric;
417
418 assert_eq!(
420 AwsSdkFeature::ObservabilityMetrics.provide_business_metric(),
421 Some(BusinessMetric::ObservabilityMetrics)
422 );
423
424 assert_eq!(
426 AwsSdkFeature::ObservabilityOtelTracing.provide_business_metric(),
427 Some(BusinessMetric::ObservabilityOtelTracing)
428 );
429
430 assert_eq!(
432 AwsSdkFeature::ObservabilityOtelMetrics.provide_business_metric(),
433 Some(BusinessMetric::ObservabilityOtelMetrics)
434 );
435
436 assert_eq!(
438 AwsSdkFeature::SsoLoginDevice.provide_business_metric(),
439 Some(BusinessMetric::SsoLoginDevice)
440 );
441
442 assert_eq!(
444 AwsSdkFeature::SsoLoginAuth.provide_business_metric(),
445 Some(BusinessMetric::SsoLoginAuth)
446 );
447
448 assert_eq!(
450 AwsSdkFeature::EndpointOverride.provide_business_metric(),
451 Some(BusinessMetric::EndpointOverride)
452 );
453 }
454
455 #[test]
456 fn test_smithy_sdk_feature_observability_tracing_mapping() {
457 use crate::user_agent::metrics::ProvideBusinessMetric;
458 use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
459
460 assert_eq!(
462 SmithySdkFeature::ObservabilityTracing.provide_business_metric(),
463 Some(BusinessMetric::ObservabilityTracing)
464 );
465 }
466
467 #[test]
468 fn test_metric_id_values() {
469 assert_eq!(
473 FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::SsoLoginDevice),
474 Some(&"1".into())
475 );
476 assert_eq!(
477 FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::SsoLoginAuth),
478 Some(&"2".into())
479 );
480
481 assert_eq!(
483 FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::ObservabilityTracing),
484 Some(&"4".into())
485 );
486 assert_eq!(
487 FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::ObservabilityMetrics),
488 Some(&"5".into())
489 );
490 assert_eq!(
491 FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::ObservabilityOtelTracing),
492 Some(&"6".into())
493 );
494 assert_eq!(
495 FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::ObservabilityOtelMetrics),
496 Some(&"7".into())
497 );
498
499 assert_eq!(
501 FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::EndpointOverride),
502 Some(&"N".into())
503 );
504 }
505}