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