use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
const MAX_COMMA_SEPARATED_METRICS_VALUES_LENGTH: usize = 1024;
#[allow(dead_code)]
const MAX_METRICS_ID_NUMBER: usize = 350;
macro_rules! iterable_enum {
($docs:tt, $enum_name:ident, $( $variant:ident ),*) => {
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
#[doc = $docs]
#[allow(missing_docs)] pub enum $enum_name {
$( $variant ),*
}
#[allow(dead_code)]
impl $enum_name {
pub(crate) fn iter() -> impl Iterator<Item = &'static $enum_name> {
const VARIANTS: &[$enum_name] = &[
$( $enum_name::$variant ),*
];
VARIANTS.iter()
}
}
};
}
struct Base64Iterator {
current: Vec<usize>,
base64_chars: Vec<char>,
}
impl Base64Iterator {
#[allow(dead_code)]
fn new() -> Self {
Base64Iterator {
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)
.collect(),
}
}
fn increment(&mut self) {
let mut i = 0;
while i < self.current.len() {
self.current[i] += 1;
if self.current[i] < self.base64_chars.len() {
return;
}
self.current[i] = 0;
i += 1;
}
self.current.push(0); }
}
impl Iterator for Base64Iterator {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if self.current.is_empty() {
return None; }
let result: String = self
.current
.iter()
.rev()
.map(|&idx| self.base64_chars[idx])
.collect();
self.increment();
Some(result)
}
}
pub(super) static FEATURE_ID_TO_METRIC_VALUE: Lazy<HashMap<BusinessMetric, Cow<'static, str>>> =
Lazy::new(|| {
let mut m = HashMap::new();
for (metric, value) in BusinessMetric::iter()
.cloned()
.zip(Base64Iterator::new())
.take(MAX_METRICS_ID_NUMBER)
{
m.insert(metric, Cow::Owned(value));
}
m
});
iterable_enum!(
"Enumerates human readable identifiers for the features tracked by metrics",
BusinessMetric,
ResourceModel,
Waiter,
Paginator,
RetryModeLegacy,
RetryModeStandard,
RetryModeAdaptive,
S3Transfer,
S3CryptoV1n,
S3CryptoV2,
S3ExpressBucket,
S3AccessGrants,
GzipRequestCompression,
ProtocolRpcV2Cbor,
EndpointOverride,
AccountIdEndpoint,
AccountIdModePreferred,
AccountIdModeDisabled,
AccountIdModeRequired,
Sigv4aSigning,
ResolvedAccountId
);
pub(crate) trait ProvideBusinessMetric {
fn provide_business_metric(&self) -> Option<BusinessMetric>;
}
impl ProvideBusinessMetric for SmithySdkFeature {
fn provide_business_metric(&self) -> Option<BusinessMetric> {
use SmithySdkFeature::*;
match self {
Waiter => Some(BusinessMetric::Waiter),
Paginator => Some(BusinessMetric::Paginator),
GzipRequestCompression => Some(BusinessMetric::GzipRequestCompression),
ProtocolRpcV2Cbor => Some(BusinessMetric::ProtocolRpcV2Cbor),
otherwise => {
tracing::warn!(
"Attempted to provide `BusinessMetric` for `{otherwise:?}`, which is not recognized in the current version of the `aws-runtime` crate. \
Consider upgrading to the latest version to ensure that all tracked features are properly reported in your metrics."
);
None
}
}
}
}
#[derive(Clone, Debug, Default)]
pub(super) struct BusinessMetrics(Vec<BusinessMetric>);
impl BusinessMetrics {
pub(super) fn push(&mut self, metric: BusinessMetric) {
self.0.push(metric);
}
pub(super) fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
fn drop_unfinished_metrics_to_fit(csv: &str, max_len: usize) -> Cow<'_, str> {
if csv.len() <= max_len {
Cow::Borrowed(csv)
} else {
let truncated = &csv[..max_len];
if let Some(pos) = truncated.rfind(',') {
Cow::Owned(truncated[..pos].to_owned())
} else {
Cow::Owned(truncated.to_owned())
}
}
}
impl fmt::Display for BusinessMetrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let metrics_values = self
.0
.iter()
.map(|feature_id| {
FEATURE_ID_TO_METRIC_VALUE
.get(feature_id)
.expect("{feature_id:?} should be found in `FEATURE_ID_TO_METRIC_VALUE`")
.clone()
})
.collect::<Vec<_>>()
.join(",");
let metrics_values = drop_unfinished_metrics_to_fit(
&metrics_values,
MAX_COMMA_SEPARATED_METRICS_VALUES_LENGTH,
);
write!(f, "m/{}", metrics_values)
}
}
#[cfg(test)]
mod tests {
use crate::user_agent::metrics::{
drop_unfinished_metrics_to_fit, Base64Iterator, FEATURE_ID_TO_METRIC_VALUE,
MAX_METRICS_ID_NUMBER,
};
use crate::user_agent::BusinessMetric;
use convert_case::{Boundary, Case, Casing};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
impl Display for BusinessMetric {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(
&format!("{:?}", self)
.as_str()
.from_case(Case::Pascal)
.with_boundaries(&[Boundary::DigitUpper, Boundary::LowerUpper])
.to_case(Case::ScreamingSnake),
)
}
}
#[test]
fn feature_id_to_metric_value() {
const EXPECTED: &str = r#"
{
"RESOURCE_MODEL": "A",
"WAITER": "B",
"PAGINATOR": "C",
"RETRY_MODE_LEGACY": "D",
"RETRY_MODE_STANDARD": "E",
"RETRY_MODE_ADAPTIVE": "F",
"S3_TRANSFER": "G",
"S3_CRYPTO_V1N": "H",
"S3_CRYPTO_V2": "I",
"S3_EXPRESS_BUCKET": "J",
"S3_ACCESS_GRANTS": "K",
"GZIP_REQUEST_COMPRESSION": "L",
"PROTOCOL_RPC_V2_CBOR": "M",
"ENDPOINT_OVERRIDE": "N",
"ACCOUNT_ID_ENDPOINT": "O",
"ACCOUNT_ID_MODE_PREFERRED": "P",
"ACCOUNT_ID_MODE_DISABLED": "Q",
"ACCOUNT_ID_MODE_REQUIRED": "R",
"SIGV4A_SIGNING": "S",
"RESOLVED_ACCOUNT_ID": "T"
}
"#;
let expected: HashMap<&str, &str> = serde_json::from_str(EXPECTED).unwrap();
assert_eq!(expected.len(), FEATURE_ID_TO_METRIC_VALUE.len());
for (feature_id, metric_value) in &*FEATURE_ID_TO_METRIC_VALUE {
assert_eq!(
expected.get(format!("{feature_id}").as_str()).unwrap(),
metric_value,
);
}
}
#[test]
fn test_base64_iter() {
let ids: Vec<String> = Base64Iterator::new()
.into_iter()
.take(MAX_METRICS_ID_NUMBER)
.collect();
assert_eq!("A", ids[0]);
assert_eq!("Z", ids[25]);
assert_eq!("a", ids[26]);
assert_eq!("z", ids[51]);
assert_eq!("0", ids[52]);
assert_eq!("9", ids[61]);
assert_eq!("+", ids[62]);
assert_eq!("-", ids[63]);
assert_eq!("AA", ids[64]);
assert_eq!("AB", ids[65]);
assert_eq!("A-", ids[127]);
assert_eq!("BA", ids[128]);
assert_eq!("Ed", ids[349]);
}
#[test]
fn test_drop_unfinished_metrics_to_fit() {
let csv = "A,10BC,E";
assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
let csv = "A10B,CE";
assert_eq!("A10B", drop_unfinished_metrics_to_fit(csv, 5));
let csv = "A10BC,E";
assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
let csv = "A10BCE";
assert_eq!("A10BC", drop_unfinished_metrics_to_fit(csv, 5));
let csv = "A";
assert_eq!("A", drop_unfinished_metrics_to_fit(csv, 5));
let csv = "A,B";
assert_eq!("A,B", drop_unfinished_metrics_to_fit(csv, 5));
}
}