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