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 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 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 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 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 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}