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