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