Skip to content

Commit

Permalink
Provide business metrics for RPC V2 CBOR, Gzip request compression, p…
Browse files Browse the repository at this point in the history
…aginator, and waiter (#3793)

## Motivation and Context
Version `User-Agent` header string and begin tracking business metrics
in that header

## Description
This PR versions `User-Agent` string and the version is set to `2.1`.
Furthermore, we track business metrics for SDK features in `User-Agent`
header. Specifically, we now track the following metrics in the
User-Agent header:
- RPC V2 CBOR (M)
- Gzip request compression (L)
- paginator (C)
- waiter (B)
 
Each letter corresponds to a metric value defined [in the
specification](https://github.com/smithy-lang/smithy-rs/blob/3f3c874c9f16ad65e80e5dfb7a6b8076d6342149/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs#L207-L226)).

#### Overall implementation strategy ####
Since business metrics is an AWS-specific concept, the
`aws-smithy-runtime` crate cannot directly reference it or call
`AwsUserAgent::add_business_metric`. Instead, the crate tracks Smithy
SDK features using the config bag with the `StoreAppend` mode. During
the execution of `UserAgentInterceptor::modify_before_signing`, this
method retrieves the SDK features from the config bag and converts them
into business metrics. This implies that any SDK features—whether
specific to Smithy or AWS—that we intend to track must be added to the
config bag prior to the invocation of the `modify_before_signing`
method.

## Testing
- Added a test-only utility function,
`assert_ua_contains_metric_values`, in the `aws-runtime` crate to verify
the presence of metric values in the `User-Agent` string. Since the
position of metric values in the `business-metrics` string may change as
new metrics are introduced (e.g., previously `m/A` but now `m/C,A,B`),
it is essential that this function accounts for potential variations and
does not rely solely on substring matching.
- Added unit and integration tests to verify tracking of the business
metrics introduced in this PR: RPC V2 CBOR, Gzip request compression,
paginator, and waiter.

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
  • Loading branch information
ysaito1001 authored Aug 20, 2024
1 parent 3ee5dcb commit 5a19a6c
Show file tree
Hide file tree
Showing 23 changed files with 716 additions and 51 deletions.
2 changes: 1 addition & 1 deletion aws/rust-runtime/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions aws/rust-runtime/aws-runtime/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aws-runtime"
version = "1.4.0"
version = "1.4.1"
authors = ["AWS Rust SDK Team <[email protected]>"]
description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly."
edition = "2021"
Expand All @@ -11,7 +11,7 @@ repository = "https://github.com/smithy-lang/smithy-rs"
event-stream = ["dep:aws-smithy-eventstream", "aws-sigv4/sign-eventstream"]
http-02x = []
http-1x = ["dep:http-1x", "dep:http-body-1x"]
test-util = []
test-util = ["dep:regex-lite"]
sigv4a = ["aws-sigv4/sigv4a"]

[dependencies]
Expand All @@ -21,6 +21,7 @@ aws-sigv4 = { path = "../aws-sigv4", features = ["http0-compat"] }
aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" }
aws-smithy-eventstream = { path = "../../../rust-runtime/aws-smithy-eventstream", optional = true }
aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" }
aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime", features = ["client"] }
aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["client"] }
aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" }
aws-types = { path = "../aws-types" }
Expand All @@ -33,6 +34,7 @@ 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"
regex-lite = { version = "0.1.5", optional = true }
tracing = "0.1"
uuid = { version = "1" }

Expand Down
38 changes: 31 additions & 7 deletions aws/rust-runtime/aws-runtime/src/user_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ use std::fmt;

mod interceptor;
mod metrics;
#[cfg(feature = "test-util")]
pub mod test_util;

const USER_AGENT_VERSION: &str = "2.1";

use crate::user_agent::metrics::BusinessMetrics;
pub use interceptor::UserAgentInterceptor;
Expand All @@ -26,6 +30,7 @@ pub use metrics::BusinessMetric;
#[derive(Clone, Debug)]
pub struct AwsUserAgent {
sdk_metadata: SdkMetadata,
ua_metadata: UaMetadata,
api_metadata: ApiMetadata,
os_metadata: OsMetadata,
language_metadata: LanguageMetadata,
Expand All @@ -49,6 +54,9 @@ impl AwsUserAgent {
name: "rust",
version: build_metadata.core_pkg_version,
};
let ua_metadata = UaMetadata {
version: USER_AGENT_VERSION,
};
let os_metadata = OsMetadata {
os_family: &build_metadata.os_family,
version: None,
Expand All @@ -64,6 +72,7 @@ impl AwsUserAgent {

AwsUserAgent {
sdk_metadata,
ua_metadata,
api_metadata,
os_metadata,
language_metadata: LanguageMetadata {
Expand All @@ -89,6 +98,7 @@ impl AwsUserAgent {
name: "rust",
version: "0.123.test",
},
ua_metadata: UaMetadata { version: "0.1" },
api_metadata: ApiMetadata {
service_id: "test-service".into(),
version: "0.123",
Expand Down Expand Up @@ -218,6 +228,7 @@ impl AwsUserAgent {
/*
ABNF for the user agent (see the bottom of the file for complete ABNF):
ua-string = sdk-metadata RWS
ua-metadata RWS
[api-metadata RWS]
os-metadata RWS
language-metadata RWS
Expand All @@ -231,6 +242,7 @@ impl AwsUserAgent {
use std::fmt::Write;
// unwrap calls should never fail because string formatting will always succeed.
write!(ua_value, "{} ", &self.sdk_metadata).unwrap();
write!(ua_value, "{} ", &self.ua_metadata).unwrap();
write!(ua_value, "{} ", &self.api_metadata).unwrap();
write!(ua_value, "{} ", &self.os_metadata).unwrap();
write!(ua_value, "{} ", &self.language_metadata).unwrap();
Expand Down Expand Up @@ -287,6 +299,17 @@ impl fmt::Display for SdkMetadata {
}
}

#[derive(Clone, Copy, Debug)]
struct UaMetadata {
version: &'static str,
}

impl fmt::Display for UaMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ua/{}", self.version)
}
}

/// Metadata about the client that's making the call.
#[derive(Clone, Debug)]
pub struct ApiMetadata {
Expand Down Expand Up @@ -598,6 +621,7 @@ mod test {
fn make_deterministic(ua: &mut AwsUserAgent) {
// hard code some variable things for a deterministic test
ua.sdk_metadata.version = "0.1";
ua.ua_metadata.version = "0.1";
ua.language_metadata.version = "1.50.0";
ua.os_metadata.os_family = &OsFamily::Macos;
ua.os_metadata.version = Some("1.15".to_string());
Expand All @@ -613,7 +637,7 @@ mod test {
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"
"aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
);
assert_eq!(
ua.ua_header(),
Expand All @@ -634,7 +658,7 @@ mod test {
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 exec-env/lambda"
"aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
);
assert_eq!(
ua.ua_header(),
Expand All @@ -658,7 +682,7 @@ mod test {
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 lib/some-framework/1.3 md/something lib/other"
"aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 md/something lib/other"
);
assert_eq!(
ua.ua_header(),
Expand All @@ -677,7 +701,7 @@ mod test {
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 app/my_app"
"aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
);
assert_eq!(
ua.ua_header(),
Expand All @@ -691,7 +715,7 @@ mod test {
ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap());
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 md/asdf"
"aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
);
assert_eq!(
ua.ua_header(),
Expand All @@ -706,7 +730,7 @@ mod test {
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"
"aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
);
assert_eq!(
ua.ua_header(),
Expand All @@ -721,7 +745,7 @@ mod test {
.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"
"aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/F,G,J"
);
assert_eq!(
ua.ua_header(),
Expand Down
93 changes: 62 additions & 31 deletions aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ use std::fmt;

use http_02x::header::{HeaderName, HeaderValue, InvalidHeaderValue, USER_AGENT};

use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_runtime_api::client::http::HttpClient;
use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextMut;
use aws_smithy_runtime_api::client::interceptors::context::{
BeforeTransmitInterceptorContextMut, BeforeTransmitInterceptorContextRef,
};
use aws_smithy_runtime_api::client::interceptors::Intercept;
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
use aws_smithy_types::config_bag::ConfigBag;
use aws_types::app_name::AppName;
use aws_types::os_shim_internal::Env;

use crate::user_agent::metrics::ProvideBusinessMetric;
use crate::user_agent::{AdditionalMetadata, ApiMetadata, AwsUserAgent, InvalidMetadataValue};

#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this
Expand Down Expand Up @@ -88,40 +92,59 @@ impl Intercept for UserAgentInterceptor {
"UserAgentInterceptor"
}

fn modify_before_signing(
fn read_after_serialization(
&self,
context: &mut BeforeTransmitInterceptorContextMut<'_>,
runtime_components: &RuntimeComponents,
_context: &BeforeTransmitInterceptorContextRef<'_>,
_runtime_components: &RuntimeComponents,
cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
// Allow for overriding the user agent by an earlier interceptor (so, for example,
// tests can use `AwsUserAgent::for_tests()`) by attempting to grab one out of the
// config bag before creating one.
let ua: Cow<'_, AwsUserAgent> = cfg
if cfg.load::<AwsUserAgent>().is_some() {
return Ok(());
}

let api_metadata = cfg
.load::<ApiMetadata>()
.ok_or(UserAgentInterceptorError::MissingApiMetadata)?;
let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone());

let maybe_app_name = cfg.load::<AppName>();
if let Some(app_name) = maybe_app_name {
ua.set_app_name(app_name.clone());
}

cfg.interceptor_state().store_put(ua);

Ok(())
}

fn modify_before_signing(
&self,
context: &mut BeforeTransmitInterceptorContextMut<'_>,
runtime_components: &RuntimeComponents,
cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
let mut ua = cfg
.load::<AwsUserAgent>()
.map(Cow::Borrowed)
.map(Result::<_, UserAgentInterceptorError>::Ok)
.unwrap_or_else(|| {
let api_metadata = cfg
.load::<ApiMetadata>()
.ok_or(UserAgentInterceptorError::MissingApiMetadata)?;
let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone());

let maybe_app_name = cfg.load::<AppName>();
if let Some(app_name) = maybe_app_name {
ua.set_app_name(app_name.clone());
}

let maybe_connector_metadata = runtime_components
.http_client()
.and_then(|c| c.connector_metadata());
if let Some(connector_metadata) = maybe_connector_metadata {
let am = AdditionalMetadata::new(Cow::Owned(connector_metadata.to_string()))?;
ua.add_additional_metadata(am);
}

Ok(Cow::Owned(ua))
})?;
.expect("`AwsUserAgent should have been created in `read_before_execution`")
.clone();

let smithy_sdk_features = cfg.load::<SmithySdkFeature>();
for smithy_sdk_feature in smithy_sdk_features {
smithy_sdk_feature
.provide_business_metric()
.map(|m| ua.add_business_metric(m));
}

let maybe_connector_metadata = runtime_components
.http_client()
.and_then(|c| c.connector_metadata());
if let Some(connector_metadata) = maybe_connector_metadata {
let am = AdditionalMetadata::new(Cow::Owned(connector_metadata.to_string()))?;
ua.add_additional_metadata(am);
}

let headers = context.request_mut().headers_mut();
let (user_agent, x_amz_user_agent) = header_values(&ua)?;
Expand Down Expand Up @@ -196,6 +219,10 @@ mod tests {
let mut config = ConfigBag::of_layers(vec![layer]);

let interceptor = UserAgentInterceptor::new();
let ctx = Into::into(&context);
interceptor
.read_after_serialization(&ctx, &rc, &mut config)
.unwrap();
let mut ctx = Into::into(&mut context);
interceptor
.modify_before_signing(&mut ctx, &rc, &mut config)
Expand Down Expand Up @@ -228,6 +255,10 @@ mod tests {
let mut config = ConfigBag::of_layers(vec![layer]);

let interceptor = UserAgentInterceptor::new();
let ctx = Into::into(&context);
interceptor
.read_after_serialization(&ctx, &rc, &mut config)
.unwrap();
let mut ctx = Into::into(&mut context);
interceptor
.modify_before_signing(&mut ctx, &rc, &mut config)
Expand All @@ -250,17 +281,17 @@ mod tests {
#[test]
fn test_api_metadata_missing() {
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
let mut context = context();
let context = context();
let mut config = ConfigBag::base();

let interceptor = UserAgentInterceptor::new();
let mut ctx = Into::into(&mut context);
let ctx = Into::into(&context);

let error = format!(
"{}",
DisplayErrorContext(
&*interceptor
.modify_before_signing(&mut ctx, &rc, &mut config)
.read_after_serialization(&ctx, &rc, &mut config)
.expect_err("it should error")
)
);
Expand Down
27 changes: 27 additions & 0 deletions aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::collections::HashMap;
Expand Down Expand Up @@ -128,6 +129,32 @@ iterable_enum!(
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 => {
// This may occur if a customer upgrades only the `aws-smithy-runtime-api` crate
// while continuing to use an outdated version of an SDK crate or the `aws-runtime`
// crate.
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>);

Expand Down
Loading

0 comments on commit 5a19a6c

Please sign in to comment.