Allow User Agent to accept business metrics (#3781)

## Motivation and Context
This PR lays the groundwork for emitting and tracking business metrics
related to features like waiters, request compression, and S3 express
within the Rust SDKs. It introduces `BusinessMetric` to the `user_agent`
module and updates `AwsUserAgent` to support its integration.

## Description
While establishing the groundwork for the said motivation, this PR does
not send metrics from features yet.
The PR deprecates existing feature and config metadata, which are now
superseded by the new business metrics.

Related tasks include
- implementing versioning for the user agent string format, e.g.
`ua/2.0` (note that including a user agent version string to
`x-amz-user-agent` could cause many connection recording tests to fail
and may also break semver check in CI just like the [content-length
enforcement test](https://github.com/smithy-lang/smithy-rs/pull/3620)
needed to be disabled to work around it)
- sending metrics from recently implement features, maybe starting with
request compression and RPC V2 Cbor (this will also modify
`x-amz-user-agent` in connection recording tests, so the argument above
applies)

## Testing
- Added unit tests for `BusinessMetric` and `AwsUserAgent`
- Existing tests in CI

## Checklist
- [x] For changes to the AWS SDK, generated SDK code, or SDK runtime
crates, I have created a changelog entry Markdown file in the
`.changelog` directory, specifying "aws-sdk-rust" in the `applies_to`
key.

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
This commit is contained in:
ysaito1001 2024-08-05 22:02:05 -05:00 committed by GitHub
parent bb1971a1ba
commit c0f817330c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 586 additions and 121 deletions

12
.changelog/1722623288.md Normal file
View File

@ -0,0 +1,12 @@
---
applies_to:
- aws-sdk-rust
authors:
- ysaito1001
references:
- smithy-rs#3781
breaking: false
new_feature: false
bug_fix: false
---
Allow [AwsUserAgent](https://docs.rs/aws-runtime/1.3.1/aws_runtime/user_agent/struct.AwsUserAgent.html) to incorporate business metrics, which now deprecates the existing feature and config metadata.

View File

@ -108,7 +108,7 @@ version = "0.60.3"
[[package]]
name = "aws-http"
version = "0.60.5"
version = "0.60.6"
dependencies = [
"aws-runtime",
]
@ -149,7 +149,7 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.3.1"
version = "1.4.0"
dependencies = [
"arbitrary",
"aws-credential-types",
@ -163,12 +163,14 @@ dependencies = [
"aws-types",
"bytes",
"bytes-utils",
"convert_case",
"fastrand",
"futures-util",
"http 0.2.12",
"http 1.1.0",
"http-body 0.4.6",
"http-body 1.0.0",
"once_cell",
"percent-encoding",
"pin-project-lite",
"proptest",
@ -282,21 +284,24 @@ dependencies = [
[[package]]
name = "aws-smithy-protocol-test"
version = "0.60.7"
version = "0.62.0"
dependencies = [
"assert-json-diff",
"aws-smithy-runtime-api",
"base64-simd",
"cbor-diag",
"http 0.2.12",
"pretty_assertions",
"regex-lite",
"roxmltree",
"serde_cbor",
"serde_json",
"thiserror",
]
[[package]]
name = "aws-smithy-runtime"
version = "1.6.1"
version = "1.6.2"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@ -449,6 +454,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -477,6 +491,25 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cbor-diag"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc245b6ecd09b23901a4fbad1ad975701fd5061ceaef6afa93a2d70605a64429"
dependencies = [
"bs58",
"chrono",
"data-encoding",
"half 2.4.1",
"nom",
"num-bigint",
"num-rational",
"num-traits",
"separator",
"url",
"uuid",
]
[[package]]
name = "cc"
version = "1.0.99"
@ -494,6 +527,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"num-traits",
]
[[package]]
name = "ciborium"
version = "0.2.2"
@ -518,7 +560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
"half 2.4.1",
]
[[package]]
@ -552,6 +594,15 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -694,6 +745,12 @@ dependencies = [
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "der"
version = "0.6.1"
@ -923,6 +980,12 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "half"
version = "2.4.1"
@ -1079,6 +1142,16 @@ dependencies = [
"webpki-roots",
]
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.2.6"
@ -1218,6 +1291,12 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.4"
@ -1238,6 +1317,16 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -1248,6 +1337,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@ -1263,6 +1362,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1821,6 +1931,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "separator"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5"
[[package]]
name = "serde"
version = "1.0.203"
@ -1830,6 +1946,16 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half 1.8.3",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
@ -2053,6 +2179,21 @@ dependencies = [
"serde_json",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.38.0"
@ -2212,18 +2353,50 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "uuid"
version = "1.8.0"

View File

@ -1,6 +1,6 @@
[package]
name = "aws-http"
version = "0.60.5"
version = "0.60.6"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>", "Russell Cohen <rcoh@amazon.com>"]
description = "This crate is no longer used by the AWS SDK and is deprecated."
edition = "2021"

View File

@ -31,18 +31,20 @@ pub type InvalidMetadataValue = aws_runtime::user_agent::InvalidMetadataValue;
)]
pub type AdditionalMetadata = aws_runtime::user_agent::AdditionalMetadata;
/// Use aws_runtime::user_agent::FeatureMetadata instead.
/// Use aws_runtime::user_agent::BusinessMetric instead.
#[deprecated(
since = "0.60.2",
note = "Use aws_runtime::user_agent::FeatureMetadata instead."
note = "Use aws_runtime::user_agent::BusinessMetric instead."
)]
#[allow(deprecated)]
pub type FeatureMetadata = aws_runtime::user_agent::FeatureMetadata;
/// Use aws_runtime::user_agent::ConfigMetadata instead.
/// Use aws_runtime::user_agent::BusinessMetric instead.
#[deprecated(
since = "0.60.2",
note = "Use aws_runtime::user_agent::ConfigMetadata instead."
note = "Use aws_runtime::user_agent::BusinessMetric instead."
)]
#[allow(deprecated)]
pub type ConfigMetadata = aws_runtime::user_agent::ConfigMetadata;
/// Use aws_runtime::user_agent::FrameworkMetadata instead.

View File

@ -1,6 +1,6 @@
[package]
name = "aws-runtime"
version = "1.3.1"
version = "1.4.0"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly."
edition = "2021"
@ -30,6 +30,7 @@ http-02x = { package = "http", version = "0.2.3" }
http-body-04x = { package = "http-body", version = "0.4.5" }
http-1x = { package = "http", version = "1.1.0", optional = true }
http-body-1x = { package = "http-body", version = "1.0.0", optional = true }
once_cell = "1.18.0"
percent-encoding = "2.1.0"
pin-project-lite = "0.2.9"
tracing = "0.1"
@ -43,6 +44,7 @@ aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-t
aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["test-util"] }
aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types", features = ["test-util"] }
bytes-utils = "0.1.2"
convert_case = "0.6.0"
futures-util = { version = "0.3.29", default-features = false }
proptest = "1.2"
serde = { version = "1", features = ["derive"]}

View File

@ -12,8 +12,11 @@ use std::error::Error;
use std::fmt;
mod interceptor;
mod metrics;
use crate::user_agent::metrics::BusinessMetrics;
pub use interceptor::UserAgentInterceptor;
pub use metrics::BusinessMetric;
/// AWS User Agent
///
@ -27,8 +30,7 @@ pub struct AwsUserAgent {
os_metadata: OsMetadata,
language_metadata: LanguageMetadata,
exec_env_metadata: Option<ExecEnvMetadata>,
feature_metadata: Vec<FeatureMetadata>,
config_metadata: Vec<ConfigMetadata>,
business_metrics: BusinessMetrics,
framework_metadata: Vec<FrameworkMetadata>,
app_name: Option<AppName>,
build_env_additional_metadata: Option<AdditionalMetadata>,
@ -70,9 +72,8 @@ impl AwsUserAgent {
extras: Default::default(),
},
exec_env_metadata,
feature_metadata: Default::default(),
config_metadata: Default::default(),
framework_metadata: Default::default(),
business_metrics: Default::default(),
app_name: Default::default(),
build_env_additional_metadata,
additional_metadata: Default::default(),
@ -102,8 +103,7 @@ impl AwsUserAgent {
extras: Default::default(),
},
exec_env_metadata: None,
feature_metadata: Vec::new(),
config_metadata: Vec::new(),
business_metrics: Default::default(),
framework_metadata: Vec::new(),
app_name: None,
build_env_additional_metadata: None,
@ -111,31 +111,65 @@ impl AwsUserAgent {
}
}
#[deprecated(
since = "1.4.0",
note = "This is a no-op; use `with_business_metric` instead."
)]
#[allow(unused_mut)]
#[allow(deprecated)]
#[doc(hidden)]
/// Adds feature metadata to the user agent.
pub fn with_feature_metadata(mut self, metadata: FeatureMetadata) -> Self {
self.feature_metadata.push(metadata);
pub fn with_feature_metadata(mut self, _metadata: FeatureMetadata) -> Self {
self
}
#[deprecated(
since = "1.4.0",
note = "This is a no-op; use `add_business_metric` instead."
)]
#[allow(deprecated)]
#[allow(unused_mut)]
#[doc(hidden)]
/// Adds feature metadata to the user agent.
pub fn add_feature_metadata(&mut self, metadata: FeatureMetadata) -> &mut Self {
self.feature_metadata.push(metadata);
pub fn add_feature_metadata(&mut self, _metadata: FeatureMetadata) -> &mut Self {
self
}
#[deprecated(
since = "1.4.0",
note = "This is a no-op; use `with_business_metric` instead."
)]
#[allow(deprecated)]
#[allow(unused_mut)]
#[doc(hidden)]
/// Adds config metadata to the user agent.
pub fn with_config_metadata(mut self, _metadata: ConfigMetadata) -> Self {
self
}
#[deprecated(
since = "1.4.0",
note = "This is a no-op; use `add_business_metric` instead."
)]
#[allow(deprecated)]
#[allow(unused_mut)]
#[doc(hidden)]
/// Adds config metadata to the user agent.
pub fn add_config_metadata(&mut self, _metadata: ConfigMetadata) -> &mut Self {
self
}
#[doc(hidden)]
/// Adds config metadata to the user agent.
pub fn with_config_metadata(mut self, metadata: ConfigMetadata) -> Self {
self.config_metadata.push(metadata);
/// Adds business metric to the user agent.
pub fn with_business_metric(mut self, metric: BusinessMetric) -> Self {
self.business_metrics.push(metric);
self
}
#[doc(hidden)]
/// Adds config metadata to the user agent.
pub fn add_config_metadata(&mut self, metadata: ConfigMetadata) -> &mut Self {
self.config_metadata.push(metadata);
/// Adds business metric to the user agent.
pub fn add_business_metric(&mut self, metric: BusinessMetric) -> &mut Self {
self.business_metrics.push(metric);
self
}
@ -188,10 +222,10 @@ impl AwsUserAgent {
os-metadata RWS
language-metadata RWS
[env-metadata RWS]
*(feat-metadata RWS)
*(config-metadata RWS)
*(framework-metadata RWS)
; ordering is not strictly required in the following section
[business-metrics]
[appId]
*(framework-metadata RWS)
*/
let mut ua_value = String::new();
use std::fmt::Write;
@ -203,11 +237,8 @@ impl AwsUserAgent {
if let Some(ref env_meta) = self.exec_env_metadata {
write!(ua_value, "{} ", env_meta).unwrap();
}
for feature in &self.feature_metadata {
write!(ua_value, "{} ", feature).unwrap();
}
for config in &self.config_metadata {
write!(ua_value, "{} ", config).unwrap();
if !self.business_metrics.is_empty() {
write!(ua_value, "{} ", &self.business_metrics).unwrap()
}
for framework in &self.framework_metadata {
write!(ua_value, "{} ", framework).unwrap();
@ -370,6 +401,7 @@ impl fmt::Display for AdditionalMetadataList {
}
}
#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
#[doc(hidden)]
/// Metadata about a feature that is being used in the SDK.
#[derive(Clone, Debug)]
@ -380,6 +412,7 @@ pub struct FeatureMetadata {
additional: AdditionalMetadataList,
}
#[allow(deprecated)]
impl FeatureMetadata {
/// Creates `FeatureMetadata`.
///
@ -406,6 +439,7 @@ impl FeatureMetadata {
}
}
#[allow(deprecated)]
impl fmt::Display for FeatureMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// feat-metadata = "ft/" name ["/" version] *(RWS additional-metadata)
@ -417,6 +451,7 @@ impl fmt::Display for FeatureMetadata {
}
}
#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
#[doc(hidden)]
/// Metadata about a config value that is being used in the SDK.
#[derive(Clone, Debug)]
@ -426,6 +461,7 @@ pub struct ConfigMetadata {
value: Option<Cow<'static, str>>,
}
#[allow(deprecated)]
impl ConfigMetadata {
/// Creates `ConfigMetadata`.
///
@ -445,6 +481,7 @@ impl ConfigMetadata {
}
}
#[allow(deprecated)]
impl fmt::Display for ConfigMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// config-metadata = "cfg/" config ["/" value]
@ -605,54 +642,6 @@ mod test {
);
}
#[test]
fn generate_a_valid_ua_with_features() {
let api_metadata = ApiMetadata {
service_id: "dynamodb".into(),
version: "123",
};
let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
.with_feature_metadata(
FeatureMetadata::new("test-feature", Some(Cow::Borrowed("1.0"))).unwrap(),
)
.with_feature_metadata(
FeatureMetadata::new("other-feature", None)
.unwrap()
.with_additional(AdditionalMetadata::new("asdf").unwrap()),
);
make_deterministic(&mut ua);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 ft/test-feature/1.0 ft/other-feature md/asdf"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
);
}
#[test]
fn generate_a_valid_ua_with_config() {
let api_metadata = ApiMetadata {
service_id: "dynamodb".into(),
version: "123",
};
let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
.with_config_metadata(
ConfigMetadata::new("some-config", Some(Cow::Borrowed("5"))).unwrap(),
)
.with_config_metadata(ConfigMetadata::new("other-config", None).unwrap());
make_deterministic(&mut ua);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 cfg/some-config/5 cfg/other-config"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
);
}
#[test]
fn generate_a_valid_ua_with_frameworks() {
let api_metadata = ApiMetadata {
@ -709,6 +698,37 @@ mod test {
"aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
);
}
#[test]
fn generate_a_valid_ua_with_business_metrics() {
// single metric ID
{
let ua = AwsUserAgent::for_tests().with_business_metric(BusinessMetric::ResourceModel);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
);
}
// multiple metric IDs
{
let ua = AwsUserAgent::for_tests()
.with_business_metric(BusinessMetric::RetryModeAdaptive)
.with_business_metric(BusinessMetric::S3Transfer)
.with_business_metric(BusinessMetric::S3ExpressBucket);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/F,G,J"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
);
}
}
}
/*
@ -729,21 +749,25 @@ api-metadata = "api/" service-id "/" version
os-metadata = "os/" os-family ["/" version]
language-metadata = "lang/" language "/" version *(RWS additional-metadata)
env-metadata = "exec-env/" name
feat-metadata = "ft/" name ["/" version] *(RWS additional-metadata)
config-metadata = "cfg/" config ["/" value]
framework-metadata = "lib/" name ["/" version] *(RWS additional-metadata)
app-id = "app/" name
build-env-additional-metadata = "md/" value
ua-metadata = "ua/2.1"
business-metrics = "m/" metric_id *(comma metric_id)
metric_id = 1*m_char
m_char = DIGIT / ALPHA / "+" / "-"
comma = ","
ua-string = sdk-metadata RWS
ua-metadata RWS
[api-metadata RWS]
os-metadata RWS
language-metadata RWS
[env-metadata RWS]
*(feat-metadata RWS)
*(config-metadata RWS)
*(framework-metadata RWS)
; ordering is not strictly required in the following section
[business-metrics]
[app-id]
[build-env-additional-metadata]
*(framework-metadata RWS)
# New metadata field might be added in the future and they must follow this format
prefix = token

View File

@ -0,0 +1,283 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
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)] // for variants, not for the Enum itself
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], // Start with the first character
base64_chars: (b'A'..=b'Z') // 'A'-'Z'
.chain(b'a'..=b'z') // 'a'-'z'
.chain(b'0'..=b'9') // '0'-'9'
.chain([b'+', b'-']) // '+' and '-'
.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() {
// The value at current position hasn't reached 64
return;
}
self.current[i] = 0;
i += 1;
}
self.current.push(0); // Add new digit if all positions overflowed
}
}
impl Iterator for Base64Iterator {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if self.current.is_empty() {
return None; // No more items
}
// Convert the current indices to characters
let result: String = self
.current
.iter()
.rev()
.map(|&idx| self.base64_chars[idx])
.collect();
// Increment to the next value
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
);
#[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 {
// business-metrics = "m/" metric_id *(comma metric_id)
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() {
// 350 is the max number of metric IDs we support for now
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));
}
}

View File

@ -529,17 +529,11 @@ mod test {
**New this release:**
- :tada: (all, [smithy-rs#446](https://github.com/smithy-lang/smithy-rs/issues/446), [aws-sdk#123](https://github.com/aws/aws-sdk/issues/123), @external-contrib, @other-external-dev) I made a change to update the code generator
- :tada: (all, [smithy-rs#446](https://github.com/smithy-lang/smithy-rs/issues/446), [smithy-rs#447](https://github.com/smithy-lang/smithy-rs/issues/447), @external-contrib, @other-external-dev) I made a change to update the code generator
**Update guide:**
blah blah
- (all, [smithy-rs#200](https://github.com/smithy-lang/smithy-rs/issues/200), @another-contrib) I made a minor change
**Contributors**
Thank you for your contributions!
- @another-contrib ([smithy-rs#200](https://github.com/smithy-lang/smithy-rs/issues/200))
- @external-contrib ([smithy-rs#446](https://github.com/smithy-lang/smithy-rs/issues/446), [smithy-rs#447](https://github.com/smithy-lang/smithy-rs/issues/447))
- @other-external-dev ([smithy-rs#446](https://github.com/smithy-lang/smithy-rs/issues/446), [smithy-rs#447](https://github.com/smithy-lang/smithy-rs/issues/447))
- @external-contrib ([smithy-rs#446](https://github.com/smithy-lang/smithy-rs/issues/446))
- @other-external-dev ([smithy-rs#446](https://github.com/smithy-lang/smithy-rs/issues/446))
"#;
@ -586,16 +580,6 @@ new_feature: true
bug_fix: false
---
I made a change to update the code generator
"#;
let smithy_rs_entry3 = r#"---
applies_to: ["client", "server"]
authors: ["another-contrib"]
references: ["smithy-rs#200"]
breaking: false
new_feature: false
bug_fix: false
---
I made a minor change
"#;
let aws_sdk_entry1 = r#"---
applies_to: ["aws-sdk-rust"]
@ -616,19 +600,6 @@ new_feature: true
bug_fix: false
---
I made a change to update the code generator
"#;
let smithy_rs_entry4 = r#"---
applies_to: ["client", "server"]
authors: ["external-contrib", "other-external-dev"]
references: ["smithy-rs#446", "smithy-rs#447"]
breaking: false
new_feature: true
bug_fix: false
---
I made a change to update the code generator
**Update guide:**
blah blah
"#;
// We won't handwrite changelog entries for model updates, and they are still provided in
@ -656,10 +627,8 @@ message = "Some API change"
[
smithy_rs_entry1,
smithy_rs_entry2,
smithy_rs_entry3,
aws_sdk_entry1,
aws_sdk_entry2,
smithy_rs_entry4,
]
.iter()
.enumerate()