1use crate::sdk_feature::AwsSdkFeature;
7use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
8use once_cell::sync::Lazy;
9use std::borrow::Cow;
10use std::collections::HashMap;
11use std::fmt;
12
13const MAX_COMMA_SEPARATED_METRICS_VALUES_LENGTH: usize = 1024;
14#[allow(dead_code)]
15const MAX_METRICS_ID_NUMBER: usize = 350;
16
17macro_rules! iterable_enum {
18 ($docs:tt, $enum_name:ident, $( $variant:ident ),*) => {
19 #[derive(Clone, Debug, Eq, Hash, PartialEq)]
20 #[non_exhaustive]
21 #[doc = $docs]
22 #[allow(missing_docs)] pub enum $enum_name {
24 $( $variant ),*
25 }
26
27 #[allow(dead_code)]
28 impl $enum_name {
29 pub(crate) fn iter() -> impl Iterator<Item = &'static $enum_name> {
30 const VARIANTS: &[$enum_name] = &[
31 $( $enum_name::$variant ),*
32 ];
33 VARIANTS.iter()
34 }
35 }
36 };
37}
38
39struct Base64Iterator {
40 current: Vec<usize>,
41 base64_chars: Vec<char>,
42}
43
44impl Base64Iterator {
45 #[allow(dead_code)]
46 fn new() -> Self {
47 Base64Iterator {
48 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)
54 .collect(),
55 }
56 }
57
58 fn increment(&mut self) {
59 let mut i = 0;
60 while i < self.current.len() {
61 self.current[i] += 1;
62 if self.current[i] < self.base64_chars.len() {
63 return;
65 }
66 self.current[i] = 0;
67 i += 1;
68 }
69 self.current.push(0); }
71}
72
73impl Iterator for Base64Iterator {
74 type Item = String;
75
76 fn next(&mut self) -> Option<Self::Item> {
77 if self.current.is_empty() {
78 return None; }
80
81 let result: String = self
83 .current
84 .iter()
85 .rev()
86 .map(|&idx| self.base64_chars[idx])
87 .collect();
88
89 self.increment();
91 Some(result)
92 }
93}
94
95pub(super) static FEATURE_ID_TO_METRIC_VALUE: Lazy<HashMap<BusinessMetric, Cow<'static, str>>> =
96 Lazy::new(|| {
97 let mut m = HashMap::new();
98 for (metric, value) in BusinessMetric::iter()
99 .cloned()
100 .zip(Base64Iterator::new())
101 .take(MAX_METRICS_ID_NUMBER)
102 {
103 m.insert(metric, Cow::Owned(value));
104 }
105 m
106 });
107
108iterable_enum!(
109 "Enumerates human readable identifiers for the features tracked by metrics",
110 BusinessMetric,
111 ResourceModel,
112 Waiter,
113 Paginator,
114 RetryModeLegacy,
115 RetryModeStandard,
116 RetryModeAdaptive,
117 S3Transfer,
118 S3CryptoV1n,
119 S3CryptoV2,
120 S3ExpressBucket,
121 S3AccessGrants,
122 GzipRequestCompression,
123 ProtocolRpcV2Cbor,
124 EndpointOverride,
125 AccountIdEndpoint,
126 AccountIdModePreferred,
127 AccountIdModeDisabled,
128 AccountIdModeRequired,
129 Sigv4aSigning,
130 ResolvedAccountId,
131 FlexibleChecksumsReqCrc32,
132 FlexibleChecksumsReqCrc32c,
133 FlexibleChecksumsReqCrc64,
134 FlexibleChecksumsReqSha1,
135 FlexibleChecksumsReqSha256,
136 FlexibleChecksumsReqWhenSupported,
137 FlexibleChecksumsReqWhenRequired,
138 FlexibleChecksumsResWhenSupported,
139 FlexibleChecksumsResWhenRequired
140);
141
142pub(crate) trait ProvideBusinessMetric {
143 fn provide_business_metric(&self) -> Option<BusinessMetric>;
144}
145
146impl ProvideBusinessMetric for SmithySdkFeature {
147 fn provide_business_metric(&self) -> Option<BusinessMetric> {
148 use SmithySdkFeature::*;
149 match self {
150 Waiter => Some(BusinessMetric::Waiter),
151 Paginator => Some(BusinessMetric::Paginator),
152 GzipRequestCompression => Some(BusinessMetric::GzipRequestCompression),
153 ProtocolRpcV2Cbor => Some(BusinessMetric::ProtocolRpcV2Cbor),
154 RetryModeStandard => Some(BusinessMetric::RetryModeStandard),
155 RetryModeAdaptive => Some(BusinessMetric::RetryModeAdaptive),
156 FlexibleChecksumsReqCrc32 => Some(BusinessMetric::FlexibleChecksumsReqCrc32),
157 FlexibleChecksumsReqCrc32c => Some(BusinessMetric::FlexibleChecksumsReqCrc32c),
158 FlexibleChecksumsReqCrc64 => Some(BusinessMetric::FlexibleChecksumsReqCrc64),
159 FlexibleChecksumsReqSha1 => Some(BusinessMetric::FlexibleChecksumsReqSha1),
160 FlexibleChecksumsReqSha256 => Some(BusinessMetric::FlexibleChecksumsReqSha256),
161 FlexibleChecksumsReqWhenSupported => {
162 Some(BusinessMetric::FlexibleChecksumsReqWhenSupported)
163 }
164 FlexibleChecksumsReqWhenRequired => {
165 Some(BusinessMetric::FlexibleChecksumsReqWhenRequired)
166 }
167 FlexibleChecksumsResWhenSupported => {
168 Some(BusinessMetric::FlexibleChecksumsResWhenSupported)
169 }
170 FlexibleChecksumsResWhenRequired => {
171 Some(BusinessMetric::FlexibleChecksumsResWhenRequired)
172 }
173 otherwise => {
174 tracing::warn!(
178 "Attempted to provide `BusinessMetric` for `{otherwise:?}`, which is not recognized in the current version of the `aws-runtime` crate. \
179 Consider upgrading to the latest version to ensure that all tracked features are properly reported in your metrics."
180 );
181 None
182 }
183 }
184 }
185}
186
187impl ProvideBusinessMetric for AwsSdkFeature {
188 fn provide_business_metric(&self) -> Option<BusinessMetric> {
189 use AwsSdkFeature::*;
190 match self {
191 S3Transfer => Some(BusinessMetric::S3Transfer),
192 }
193 }
194}
195
196#[derive(Clone, Debug, Default)]
197pub(super) struct BusinessMetrics(Vec<BusinessMetric>);
198
199impl BusinessMetrics {
200 pub(super) fn push(&mut self, metric: BusinessMetric) {
201 self.0.push(metric);
202 }
203
204 pub(super) fn is_empty(&self) -> bool {
205 self.0.is_empty()
206 }
207}
208
209fn drop_unfinished_metrics_to_fit(csv: &str, max_len: usize) -> Cow<'_, str> {
210 if csv.len() <= max_len {
211 Cow::Borrowed(csv)
212 } else {
213 let truncated = &csv[..max_len];
214 if let Some(pos) = truncated.rfind(',') {
215 Cow::Owned(truncated[..pos].to_owned())
216 } else {
217 Cow::Owned(truncated.to_owned())
218 }
219 }
220}
221
222impl fmt::Display for BusinessMetrics {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 let metrics_values = self
226 .0
227 .iter()
228 .map(|feature_id| {
229 FEATURE_ID_TO_METRIC_VALUE
230 .get(feature_id)
231 .expect("{feature_id:?} should be found in `FEATURE_ID_TO_METRIC_VALUE`")
232 .clone()
233 })
234 .collect::<Vec<_>>()
235 .join(",");
236
237 let metrics_values = drop_unfinished_metrics_to_fit(
238 &metrics_values,
239 MAX_COMMA_SEPARATED_METRICS_VALUES_LENGTH,
240 );
241
242 write!(f, "m/{}", metrics_values)
243 }
244}
245#[cfg(test)]
246mod tests {
247 use crate::user_agent::metrics::{
248 drop_unfinished_metrics_to_fit, Base64Iterator, FEATURE_ID_TO_METRIC_VALUE,
249 MAX_METRICS_ID_NUMBER,
250 };
251 use crate::user_agent::BusinessMetric;
252 use convert_case::{Boundary, Case, Casing};
253 use std::collections::HashMap;
254 use std::fmt::{Display, Formatter};
255
256 impl Display for BusinessMetric {
257 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
258 f.write_str(
259 &format!("{:?}", self)
260 .as_str()
261 .from_case(Case::Pascal)
262 .with_boundaries(&[Boundary::DigitUpper, Boundary::LowerUpper])
263 .to_case(Case::ScreamingSnake),
264 )
265 }
266 }
267
268 #[test]
269 fn feature_id_to_metric_value() {
270 const EXPECTED: &str = r#"
271{
272 "RESOURCE_MODEL": "A",
273 "WAITER": "B",
274 "PAGINATOR": "C",
275 "RETRY_MODE_LEGACY": "D",
276 "RETRY_MODE_STANDARD": "E",
277 "RETRY_MODE_ADAPTIVE": "F",
278 "S3_TRANSFER": "G",
279 "S3_CRYPTO_V1N": "H",
280 "S3_CRYPTO_V2": "I",
281 "S3_EXPRESS_BUCKET": "J",
282 "S3_ACCESS_GRANTS": "K",
283 "GZIP_REQUEST_COMPRESSION": "L",
284 "PROTOCOL_RPC_V2_CBOR": "M",
285 "ENDPOINT_OVERRIDE": "N",
286 "ACCOUNT_ID_ENDPOINT": "O",
287 "ACCOUNT_ID_MODE_PREFERRED": "P",
288 "ACCOUNT_ID_MODE_DISABLED": "Q",
289 "ACCOUNT_ID_MODE_REQUIRED": "R",
290 "SIGV4A_SIGNING": "S",
291 "RESOLVED_ACCOUNT_ID": "T",
292 "FLEXIBLE_CHECKSUMS_REQ_CRC32" : "U",
293 "FLEXIBLE_CHECKSUMS_REQ_CRC32C" : "V",
294 "FLEXIBLE_CHECKSUMS_REQ_CRC64" : "W",
295 "FLEXIBLE_CHECKSUMS_REQ_SHA1" : "X",
296 "FLEXIBLE_CHECKSUMS_REQ_SHA256" : "Y",
297 "FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED" : "Z",
298 "FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED" : "a",
299 "FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED" : "b",
300 "FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED" : "c"
301}
302 "#;
303
304 let expected: HashMap<&str, &str> = serde_json::from_str(EXPECTED).unwrap();
305 assert_eq!(expected.len(), FEATURE_ID_TO_METRIC_VALUE.len());
306
307 for (feature_id, metric_value) in &*FEATURE_ID_TO_METRIC_VALUE {
308 let expected = expected.get(format!("{feature_id}").as_str());
309 assert_eq!(
310 expected.expect(&format!("Expected {feature_id} to have value `{metric_value}` but it was `{expected:?}` instead.")),
311 metric_value,
312 );
313 }
314 }
315
316 #[test]
317 fn test_base64_iter() {
318 let ids: Vec<String> = Base64Iterator::new()
320 .into_iter()
321 .take(MAX_METRICS_ID_NUMBER)
322 .collect();
323 assert_eq!("A", ids[0]);
324 assert_eq!("Z", ids[25]);
325 assert_eq!("a", ids[26]);
326 assert_eq!("z", ids[51]);
327 assert_eq!("0", ids[52]);
328 assert_eq!("9", ids[61]);
329 assert_eq!("+", ids[62]);
330 assert_eq!("-", ids[63]);
331 assert_eq!("AA", ids[64]);
332 assert_eq!("AB", ids[65]);
333 assert_eq!("A-", ids[127]);
334 assert_eq!("BA", ids[128]);
335 assert_eq!("Ed", ids[349]);
336 }
337
338 #[test]
339 fn test_drop_unfinished_metrics_to_fit() {
340 let csv = "A,10BC,E";
341 assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
342
343 let csv = "A10B,CE";
344 assert_eq!("A10B", drop_unfinished_metrics_to_fit(csv, 5));
345
346 let csv = "A10BC,E";
347 assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
348
349 let csv = "A10BCE";
350 assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
351
352 let csv = "A";
353 assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
354
355 let csv = "A,B";
356 assert_eq!("A,B", drop_unfinished_metrics_to_fit(csv, 5));
357 }
358}