From 5a19a6c5045801ae1bde34b4cf73253133a55d49 Mon Sep 17 00:00:00 2001 From: ysaito1001 Date: Tue, 20 Aug 2024 16:20:27 -0500 Subject: [PATCH] Provide business metrics for RPC V2 CBOR, Gzip request compression, paginator, and waiter (#3793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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._ --- aws/rust-runtime/Cargo.lock | 2 +- aws/rust-runtime/aws-runtime/Cargo.toml | 6 +- .../aws-runtime/src/user_agent.rs | 38 +++- .../aws-runtime/src/user_agent/interceptor.rs | 93 ++++++---- .../aws-runtime/src/user_agent/metrics.rs | 27 +++ .../aws-runtime/src/user_agent/test_util.rs | 122 ++++++++++++ .../amazon/smithy/rustsdk/AwsRuntimeType.kt | 3 + .../smithy/rustsdk/UserAgentDecoratorTest.kt | 68 +++++++ aws/sdk/integration-tests/ec2/Cargo.toml | 3 +- .../integration-tests/ec2/tests/paginators.rs | 25 ++- .../integration-tests/ec2/tests/waiters.rs | 39 ++++ .../client/smithy/RustClientCodegenPlugin.kt | 2 + .../StaticSdkFeatureTrackerDecorator.kt | 60 ++++++ .../smithy/generators/PaginatorGenerator.kt | 7 +- .../generators/waiters/WaitableGenerator.kt | 7 +- .../codegen/core/rustlang/CargoDependency.kt | 6 + rust-runtime/aws-smithy-runtime/Cargo.toml | 2 +- rust-runtime/aws-smithy-runtime/src/client.rs | 2 + .../src/client/sdk_feature.rs | 19 ++ rust-runtime/inlineable/Cargo.toml | 3 +- .../src/client_request_compression.rs | 57 +++++- rust-runtime/inlineable/src/lib.rs | 2 + .../inlineable/src/sdk_feature_tracker.rs | 174 ++++++++++++++++++ 23 files changed, 716 insertions(+), 51 deletions(-) create mode 100644 aws/rust-runtime/aws-runtime/src/user_agent/test_util.rs create mode 100644 codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/StaticSdkFeatureTrackerDecorator.kt create mode 100644 rust-runtime/aws-smithy-runtime/src/client/sdk_feature.rs create mode 100644 rust-runtime/inlineable/src/sdk_feature_tracker.rs diff --git a/aws/rust-runtime/Cargo.lock b/aws/rust-runtime/Cargo.lock index b94f091e4..970f8f45f 100644 --- a/aws/rust-runtime/Cargo.lock +++ b/aws/rust-runtime/Cargo.lock @@ -341,7 +341,7 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.1" +version = "1.2.2" dependencies = [ "base64-simd", "bytes", diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index 488619893..95a61c4b4 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-runtime" -version = "1.4.0" +version = "1.4.1" authors = ["AWS Rust SDK Team "] description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly." edition = "2021" @@ -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] @@ -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" } @@ -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" } diff --git a/aws/rust-runtime/aws-runtime/src/user_agent.rs b/aws/rust-runtime/aws-runtime/src/user_agent.rs index b7aeeb8f8..c13ce3962 100644 --- a/aws/rust-runtime/aws-runtime/src/user_agent.rs +++ b/aws/rust-runtime/aws-runtime/src/user_agent.rs @@ -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; @@ -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, @@ -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, @@ -64,6 +72,7 @@ impl AwsUserAgent { AwsUserAgent { sdk_metadata, + ua_metadata, api_metadata, os_metadata, language_metadata: LanguageMetadata { @@ -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", @@ -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 @@ -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(); @@ -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 { @@ -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()); @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), @@ -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(), diff --git a/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs b/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs index fdcdd831a..25d5f4ecf 100644 --- a/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs +++ b/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs @@ -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 @@ -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::().is_some() { + return Ok(()); + } + + let api_metadata = cfg + .load::() + .ok_or(UserAgentInterceptorError::MissingApiMetadata)?; + let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone()); + + let maybe_app_name = cfg.load::(); + 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::() - .map(Cow::Borrowed) - .map(Result::<_, UserAgentInterceptorError>::Ok) - .unwrap_or_else(|| { - let api_metadata = cfg - .load::() - .ok_or(UserAgentInterceptorError::MissingApiMetadata)?; - let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone()); - - let maybe_app_name = cfg.load::(); - 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::(); + 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)?; @@ -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) @@ -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) @@ -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") ) ); diff --git a/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs b/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs index 2ecce131f..1acaf86fc 100644 --- a/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs +++ b/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs @@ -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; @@ -128,6 +129,32 @@ iterable_enum!( ResolvedAccountId ); +pub(crate) trait ProvideBusinessMetric { + fn provide_business_metric(&self) -> Option; +} + +impl ProvideBusinessMetric for SmithySdkFeature { + fn provide_business_metric(&self) -> Option { + 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); diff --git a/aws/rust-runtime/aws-runtime/src/user_agent/test_util.rs b/aws/rust-runtime/aws-runtime/src/user_agent/test_util.rs new file mode 100644 index 000000000..a0a5679b6 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/user_agent/test_util.rs @@ -0,0 +1,122 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Utilities for testing the User-Agent header + +use once_cell::sync::Lazy; +use regex_lite::Regex; + +// regular expression pattern for base64 numeric values +#[allow(dead_code)] +static RE: Lazy = Lazy::new(|| Regex::new(r"m/([A-Za-z0-9+/=_,-]+)").unwrap()); + +/// Asserts `user_agent` contains all metric values `values` +/// +/// Refer to the end of the parent module file `user_agent.rs` for the complete ABNF specification +/// of `business-metrics`. +pub fn assert_ua_contains_metric_values(user_agent: &str, values: &[&str]) { + match RE.find(user_agent) { + Some(matched) => { + let csv = matched + .as_str() + .strip_prefix("m/") + .expect("prefix `m/` is guaranteed to exist by regex match"); + let metrics: Vec<&str> = csv.split(',').collect(); + let mut missed = vec![]; + + for value in values.iter() { + if !metrics.contains(value) { + missed.push(value); + } + } + assert!( + missed.is_empty(), + "{}", + format!("metric values {missed:?} not found in `{user_agent}`") + ); + } + None => { + panic!("{}", format!("the pattern for business-metrics `m/(metric_id) *(comma metric_id)` not found in `{user_agent}`")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_assert_ua_contains_metric_values() { + assert_ua_contains_metric_values("m/A", &[]); + assert_ua_contains_metric_values("m/A", &["A"]); + assert_ua_contains_metric_values(" m/A", &["A"]); + assert_ua_contains_metric_values("m/A ", &["A"]); + assert_ua_contains_metric_values(" m/A ", &["A"]); + assert_ua_contains_metric_values("m/A,B", &["B"]); + assert_ua_contains_metric_values("m/A,B", &["A", "B"]); + assert_ua_contains_metric_values("m/A,B", &["B", "A"]); + assert_ua_contains_metric_values("m/A,B,C", &["B"]); + assert_ua_contains_metric_values("m/A,B,C", &["B", "C"]); + assert_ua_contains_metric_values("m/A,B,C", &["A", "B", "C"]); + assert_ua_contains_metric_values("m/A,B,C,AA", &["AA"]); + assert_ua_contains_metric_values("m/A,B,C=,AA", &["C="]); + assert_ua_contains_metric_values( + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A", + &["A"], + ); + assert_ua_contains_metric_values( + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A md/http#capture-request-handler", + &["A"] + ); + } + + #[test] + #[should_panic(expected = "the pattern for business-metrics")] + fn empty_ua_fails_assert() { + assert_ua_contains_metric_values("", &["A"]); + } + + #[test] + #[should_panic(expected = "the pattern for business-metrics")] + fn invalid_business_metrics_pattern_fails_assert() { + assert_ua_contains_metric_values("mA", &["A"]); + } + + #[test] + #[should_panic(expected = "the pattern for business-metrics")] + fn another_invalid_business_metrics_pattern_fails_assert() { + assert_ua_contains_metric_values("m/", &["A"]); + } + + #[test] + #[should_panic(expected = "metric values [\"\"] not found in `m/A`")] + fn empty_metric_value_fails_assert() { + assert_ua_contains_metric_values("m/A", &[""]); + } + + #[test] + #[should_panic(expected = "metric values [\"A\"] not found in `m/AA`")] + fn business_metrics_do_not_contain_given_metric_value() { + assert_ua_contains_metric_values("m/AA", &["A"]); + } + + #[test] + #[should_panic(expected = "the pattern for business-metrics")] + fn ua_containing_no_business_metrics_fails_assert() { + assert_ua_contains_metric_values( + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0", + &["A"], + ); + } + + #[test] + #[should_panic(expected = "the pattern for business-metrics")] + fn ua_containing_invalid_business_metrics_fails_assert() { + assert_ua_contains_metric_values( + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 mA", + &["A"], + ); + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt index 24b13b9c3..5c713ef74 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt @@ -62,5 +62,8 @@ object AwsRuntimeType { fun awsRuntime(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsRuntime(runtimeConfig).toType() + fun awsRuntimeTestUtil(runtimeConfig: RuntimeConfig) = + AwsCargoDependency.awsRuntime(runtimeConfig).toDevDependency().withFeature("test-util").toType() + fun awsRuntimeApi(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsRuntimeApi(runtimeConfig).toType() } diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/UserAgentDecoratorTest.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/UserAgentDecoratorTest.kt index bf2c7b6d7..dbc30c940 100644 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/UserAgentDecoratorTest.kt +++ b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/UserAgentDecoratorTest.kt @@ -12,6 +12,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel import software.amazon.smithy.rust.codegen.core.testutil.integrationTest +import software.amazon.smithy.rust.codegen.core.testutil.tokioTest class UserAgentDecoratorTest { companion object { @@ -142,4 +143,71 @@ class UserAgentDecoratorTest { } } } + + @Test + fun `it emits business metric for RPC v2 CBOR in user agent`() { + val model = + """ + namespace test + + use aws.auth#sigv4 + use aws.api#service + use smithy.protocols#rpcv2Cbor + use smithy.rules#endpointRuleSet + + @auth([sigv4]) + @sigv4(name: "dontcare") + @rpcv2Cbor + @endpointRuleSet({ + "version": "1.0", + "rules": [{ "type": "endpoint", "conditions": [], "endpoint": { "url": "https://example.com" } }], + "parameters": {} + }) + @service(sdkId: "dontcare") + service TestService { version: "2023-01-01", operations: [SomeOperation] } + structure SomeOutput { something: String } + operation SomeOperation { output: SomeOutput } + """.asSmithyModel() + + awsSdkIntegrationTest(model) { ctx, rustCrate -> + rustCrate.integrationTest("business_metric_for_rpc_v2_cbor") { + tokioTest("should_emit_metric_in_user_agent") { + val rc = ctx.runtimeConfig + val moduleName = ctx.moduleUseName() + rustTemplate( + """ + use $moduleName::config::{ + Region, + }; + use $moduleName::{Client, Config}; + + let (http_client, rcvr) = #{capture_request}(#{None}); + let config = Config::builder() + .region(Region::new("us-east-1")) + .http_client(http_client.clone()) + .with_test_defaults() + .build(); + let client = Client::from_conf(config); + let _ = client.some_operation().send().await; + let expected_req = rcvr.expect_request(); + let user_agent = expected_req + .headers() + .get("x-amz-user-agent") + .unwrap(); + #{assert_ua_contains_metric_values}(user_agent, &["M"]); + """, + *preludeScope, + "assert_ua_contains_metric_values" to AwsRuntimeType.awsRuntimeTestUtil(rc).resolve("user_agent::test_util::assert_ua_contains_metric_values"), + "capture_request" to RuntimeType.captureRequest(rc), + "disable_interceptor" to + RuntimeType.smithyRuntimeApiClient(rc) + .resolve("client::interceptors::disable_interceptor"), + "UserAgentInterceptor" to + AwsRuntimeType.awsRuntime(rc) + .resolve("user_agent::UserAgentInterceptor"), + ) + } + } + } + } } diff --git a/aws/sdk/integration-tests/ec2/Cargo.toml b/aws/sdk/integration-tests/ec2/Cargo.toml index f9778a75a..402507a1a 100644 --- a/aws/sdk/integration-tests/ec2/Cargo.toml +++ b/aws/sdk/integration-tests/ec2/Cargo.toml @@ -8,11 +8,12 @@ publish = false [dev-dependencies] aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } +aws-runtime = { path = "../../build/aws-sdk/sdk/aws-runtime", features = ["test-util"] } aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util"] } aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] } aws-smithy-runtime-api = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime-api", features = ["client", "http-02x"] } aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" } -aws-sdk-ec2 = { path = "../../build/aws-sdk/sdk/ec2", features = ["test-util"] } +aws-sdk-ec2 = { path = "../../build/aws-sdk/sdk/ec2", features = ["behavior-version-latest", "test-util"] } tokio = { version = "1.23.1", features = ["full"]} http = "0.2.0" tokio-stream = "0.1.5" diff --git a/aws/sdk/integration-tests/ec2/tests/paginators.rs b/aws/sdk/integration-tests/ec2/tests/paginators.rs index 0ab3be626..36dc3152b 100644 --- a/aws/sdk/integration-tests/ec2/tests/paginators.rs +++ b/aws/sdk/integration-tests/ec2/tests/paginators.rs @@ -3,8 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +use aws_runtime::user_agent::test_util::assert_ua_contains_metric_values; use aws_sdk_ec2::{config::Credentials, config::Region, types::InstanceType, Client, Config}; -use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient}; +use aws_smithy_runtime::client::http::test_util::{ + capture_request, ReplayEvent, StaticReplayClient, +}; use aws_smithy_runtime_api::client::http::HttpClient; use aws_smithy_types::body::SdkBody; @@ -88,3 +91,23 @@ async fn paginators_handle_unset_tokens() { assert_eq!(first_item, None); http_client.assert_requests_match(&[]); } + +#[tokio::test] +async fn should_emit_business_metric_for_paginator_in_user_agent() { + let (http_client, captured_request) = capture_request(None); + let client = Client::from_conf(stub_config(http_client.clone())); + let instance_type = InstanceType::from("g5.48xlarge"); + let _ = client + .describe_spot_price_history() + .instance_types(instance_type) + .product_descriptions("Linux/UNIX") + .availability_zone("eu-north-1a") + .into_paginator() + .items() + .send() + .collect::>() + .await; + let expected_req = captured_request.expect_request(); + let user_agent = expected_req.headers().get("x-amz-user-agent").unwrap(); + assert_ua_contains_metric_values(user_agent, &["C"]); +} diff --git a/aws/sdk/integration-tests/ec2/tests/waiters.rs b/aws/sdk/integration-tests/ec2/tests/waiters.rs index 88d725f0b..8efbbb5ed 100644 --- a/aws/sdk/integration-tests/ec2/tests/waiters.rs +++ b/aws/sdk/integration-tests/ec2/tests/waiters.rs @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +use aws_runtime::user_agent::test_util::assert_ua_contains_metric_values; use aws_sdk_ec2::{client::Waiters, config::Region, error::DisplayErrorContext, Client}; use aws_smithy_async::test_util::tick_advance_sleep::{ tick_advance_time_and_sleep, TickAdvanceTime, @@ -85,3 +86,41 @@ async fn waiters_exceed_max_wait_time() { err => panic!("unexpected error: {}", DisplayErrorContext(&err)), } } + +#[tokio::test] +async fn should_emit_business_metric_for_waiter_in_user_agent() { + // This function has the same setup and execution as `waiters_success`, but differs in the verification step. + // Because `full_validate` consumes the recorded requests after being called, we need a separate test + // to examine these requests. + + let _logs = show_test_logs(); + + let (ec2, http_client, time_source) = prerequisites().await; + + ec2.start_instances() + .instance_ids("i-09fb4224219ac6902") + .send() + .await + .unwrap(); + + let waiter_task = tokio::spawn( + ec2.wait_until_instance_status_ok() + .instance_ids("i-09fb4224219ac6902") + .wait(Duration::from_secs(300)), + ); + + time_source.tick(Duration::from_secs(305)).await; + waiter_task.await.unwrap().unwrap(); + + // Verify the corresponding business metric value has been emitted + let actual_requests = http_client.take_requests().await; + let user_agent_in_last_request = actual_requests + .last() + .unwrap() + .headers() + .get("x-amz-user-agent") + .unwrap() + .to_str() + .unwrap(); + assert_ua_contains_metric_values(user_agent_in_last_request, &["B"]); +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt index 7f9c07c28..26e30a2ea 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/RustClientCodegenPlugin.kt @@ -15,6 +15,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customizations.HttpConn import software.amazon.smithy.rust.codegen.client.smithy.customizations.IdempotencyTokenDecorator import software.amazon.smithy.rust.codegen.client.smithy.customizations.NoAuthDecorator import software.amazon.smithy.rust.codegen.client.smithy.customizations.SensitiveOutputDecorator +import software.amazon.smithy.rust.codegen.client.smithy.customizations.StaticSdkFeatureTrackerDecorator import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator import software.amazon.smithy.rust.codegen.client.smithy.customize.CombinedClientCodegenDecorator import software.amazon.smithy.rust.codegen.client.smithy.customize.RequiredCustomizations @@ -70,6 +71,7 @@ class RustClientCodegenPlugin : ClientDecoratableBuildPlugin() { SensitiveOutputDecorator(), IdempotencyTokenDecorator(), StalledStreamProtectionDecorator(), + StaticSdkFeatureTrackerDecorator(), *decorator, ) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/StaticSdkFeatureTrackerDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/StaticSdkFeatureTrackerDecorator.kt new file mode 100644 index 000000000..8d5cade12 --- /dev/null +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/StaticSdkFeatureTrackerDecorator.kt @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.client.smithy.customizations + +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginSection +import software.amazon.smithy.rust.codegen.core.rustlang.InlineDependency +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType + +/** + * A decorator for tracking Smithy SDK features that are enabled according to the model at code generation time. + * + * Other Smithy SDK features are typically tracked at runtime by their respective interceptors, because whether + * they are enabled is not determined until later during the execution. + */ +class StaticSdkFeatureTrackerDecorator : ClientCodegenDecorator { + override val name: String = "StaticSdkFeatureTrackerDecorator" + override val order: Byte = 0 + + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations + RpcV2CborFeatureTrackerRuntimePluginCustomization(codegenContext) +} + +private class RpcV2CborFeatureTrackerRuntimePluginCustomization(private val codegenContext: ClientCodegenContext) : + ServiceRuntimePluginCustomization() { + private val rpcV2CborProtocolShapeId = ShapeId.from("smithy.protocols#rpcv2Cbor") + + override fun section(section: ServiceRuntimePluginSection): Writable = + writable { + when (section) { + is ServiceRuntimePluginSection.RegisterRuntimeComponents -> { + if (codegenContext.protocol == rpcV2CborProtocolShapeId) { + section.registerInterceptor(this) { + rustTemplate( + "#{RpcV2CborFeatureTrackerInterceptor}::new()", + "RpcV2CborFeatureTrackerInterceptor" to + RuntimeType.forInlineDependency( + InlineDependency.sdkFeatureTracker(codegenContext.runtimeConfig), + ).resolve("rpc_v2_cbor::RpcV2CborFeatureTrackerInterceptor"), + ) + } + } + } + + else -> emptySection + } + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt index 3c3921ecd..1345ce7b6 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt @@ -12,6 +12,7 @@ import software.amazon.smithy.model.traits.IdempotencyTokenTrait import software.amazon.smithy.model.traits.PaginatedTrait import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.traits.IsTruncatedPaginatorTrait +import software.amazon.smithy.rust.codegen.core.rustlang.InlineDependency import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustType import software.amazon.smithy.rust.codegen.core.rustlang.Writable @@ -217,9 +218,13 @@ class PaginatorGenerator private constructor( handle.runtime_plugins.clone(), &handle.conf, #{None}, - ); + ).with_operation_plugin(#{PaginatorFeatureTrackerRuntimePlugin}::new()); """, *codegenScope, + "PaginatorFeatureTrackerRuntimePlugin" to + RuntimeType.forInlineDependency( + InlineDependency.sdkFeatureTracker(runtimeConfig), + ).resolve("paginator::PaginatorFeatureTrackerRuntimePlugin"), "RuntimePlugins" to RuntimeType.runtimePlugins(runtimeConfig), ) }, diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/waiters/WaitableGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/waiters/WaitableGenerator.kt index 00c715b56..e43763a01 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/waiters/WaitableGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/waiters/WaitableGenerator.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentBuilderConfig import software.amazon.smithy.rust.codegen.client.smithy.generators.client.FluentBuilderGenerator import software.amazon.smithy.rust.codegen.core.rustlang.EscapeFor +import software.amazon.smithy.rust.codegen.core.rustlang.InlineDependency import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter @@ -206,7 +207,7 @@ private class WaiterFluentBuilderConfig( self.handle.runtime_plugins.clone(), &self.handle.conf, #{None}, - ); + ).with_operation_plugin(#{WaiterFeatureTrackerRuntimePlugin}::new()); let mut cfg = #{ConfigBag}::base(); let runtime_components_builder = runtime_plugins.apply_client_configuration(&mut cfg) .map_err(#{WaiterError}::construction_failure)?; @@ -241,6 +242,10 @@ private class WaiterFluentBuilderConfig( }, "FinalPollAlias" to finalPollTypeAlias(), "WaiterErrorAlias" to waiterErrorTypeAlias(), + "WaiterFeatureTrackerRuntimePlugin" to + RuntimeType.forInlineDependency( + InlineDependency.sdkFeatureTracker(runtimeConfig), + ).resolve("waiter::WaiterFeatureTrackerRuntimePlugin"), ) } diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt index 24c7e5426..3c82f3505 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt @@ -173,6 +173,12 @@ class InlineDependency( ) fun constrained(): InlineDependency = forRustFile(ConstrainedModule, "/inlineable/src/constrained.rs") + + fun sdkFeatureTracker(runtimeConfig: RuntimeConfig): InlineDependency = + forInlineableRustFile( + "sdk_feature_tracker", + CargoDependency.smithyRuntime(runtimeConfig), + ) } } diff --git a/rust-runtime/aws-smithy-runtime/Cargo.toml b/rust-runtime/aws-smithy-runtime/Cargo.toml index 1a2d2adf4..60433a38d 100644 --- a/rust-runtime/aws-smithy-runtime/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-runtime" -version = "1.6.3" +version = "1.6.4" authors = ["AWS Rust SDK Team ", "Zelda Hessler "] description = "The new smithy runtime crate" edition = "2021" diff --git a/rust-runtime/aws-smithy-runtime/src/client.rs b/rust-runtime/aws-smithy-runtime/src/client.rs index db146e995..473df11e7 100644 --- a/rust-runtime/aws-smithy-runtime/src/client.rs +++ b/rust-runtime/aws-smithy-runtime/src/client.rs @@ -47,5 +47,7 @@ pub mod interceptors; /// Stalled stream protection for clients pub mod stalled_stream_protection; +#[doc(hidden)] +pub mod sdk_feature; /// Smithy support-code for code generated waiters. pub mod waiters; diff --git a/rust-runtime/aws-smithy-runtime/src/client/sdk_feature.rs b/rust-runtime/aws-smithy-runtime/src/client/sdk_feature.rs new file mode 100644 index 000000000..5a8ab95a1 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime/src/client/sdk_feature.rs @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_types::config_bag::{Storable, StoreAppend}; + +#[non_exhaustive] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SmithySdkFeature { + Waiter, + Paginator, + GzipRequestCompression, + ProtocolRpcV2Cbor, +} + +impl Storable for SmithySdkFeature { + type Storer = StoreAppend; +} diff --git a/rust-runtime/inlineable/Cargo.toml b/rust-runtime/inlineable/Cargo.toml index 00a6fca12..2b39b2585 100644 --- a/rust-runtime/inlineable/Cargo.toml +++ b/rust-runtime/inlineable/Cargo.toml @@ -21,7 +21,8 @@ aws-smithy-cbor = { path = "../aws-smithy-cbor" } aws-smithy-compression = { path = "../aws-smithy-compression", features = ["http-body-0-4-x"] } aws-smithy-http = { path = "../aws-smithy-http", features = ["event-stream"] } aws-smithy-json = { path = "../aws-smithy-json" } -aws-smithy-runtime-api = { path = "../aws-smithy-runtime-api", features = ["client"] } +aws-smithy-runtime = { path = "../aws-smithy-runtime", features = ["client"] } +aws-smithy-runtime-api = { path = "../aws-smithy-runtime-api", features = ["client", "test-util"] } aws-smithy-types = { path = "../aws-smithy-types" } aws-smithy-xml = { path = "../aws-smithy-xml" } bytes = "1" diff --git a/rust-runtime/inlineable/src/client_request_compression.rs b/rust-runtime/inlineable/src/client_request_compression.rs index 2ea9c26e6..6211ca88c 100644 --- a/rust-runtime/inlineable/src/client_request_compression.rs +++ b/rust-runtime/inlineable/src/client_request_compression.rs @@ -6,6 +6,7 @@ use aws_smithy_compression::body::compress::CompressedBody; use aws_smithy_compression::http::http_body_0_4_x::CompressRequest; use aws_smithy_compression::{CompressionAlgorithm, CompressionOptions}; +use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; use aws_smithy_runtime_api::box_error::BoxError; use aws_smithy_runtime_api::client::interceptors::context::{ BeforeSerializationInterceptorContextRef, BeforeTransmitInterceptorContextMut, @@ -76,10 +77,9 @@ impl Intercept for RequestCompressionInterceptor { "RequestCompressionInterceptor" } - fn read_before_serialization( + fn read_before_execution( &self, _context: &BeforeSerializationInterceptorContextRef<'_>, - _runtime_components: &RuntimeComponents, cfg: &mut ConfigBag, ) -> Result<(), BoxError> { let disable_request_compression = cfg @@ -103,7 +103,7 @@ impl Intercept for RequestCompressionInterceptor { Ok(()) } - fn modify_before_signing( + fn modify_before_retry_loop( &self, context: &mut BeforeTransmitInterceptorContextMut<'_>, _runtime_components: &RuntimeComponents, @@ -111,7 +111,7 @@ impl Intercept for RequestCompressionInterceptor { ) -> Result<(), BoxError> { let state = cfg .load::() - .expect("set in `read_before_serialization`"); + .expect("set in `read_before_execution`"); let options = state.options.clone().unwrap(); let request = context.request_mut(); @@ -145,6 +145,9 @@ impl Intercept for RequestCompressionInterceptor { CompressionAlgorithm::Gzip.into_impl_http_body_0_4_x(&options), )?; + cfg.interceptor_state() + .store_append::(SmithySdkFeature::GzipRequestCompression); + Ok(()) } } @@ -205,9 +208,17 @@ impl Storable for RequestMinCompressionSizeBytes { #[cfg(test)] mod tests { use super::wrap_request_body_in_compressed_body; + use crate::client_request_compression::{ + RequestCompressionInterceptor, RequestMinCompressionSizeBytes, + }; use aws_smithy_compression::{CompressionAlgorithm, CompressionOptions}; + use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; + use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext}; + use aws_smithy_runtime_api::client::interceptors::Intercept; use aws_smithy_runtime_api::client::orchestrator::HttpRequest; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; use aws_smithy_types::body::SdkBody; + use aws_smithy_types::config_bag::{ConfigBag, Layer}; use http_body::Body; const UNCOMPRESSED_INPUT: &[u8] = b"hello world"; @@ -257,4 +268,42 @@ mod tests { assert_ne!(UNCOMPRESSED_INPUT, body_data.as_slice()); assert_eq!(COMPRESSED_OUTPUT, body_data.as_slice()); } + + fn context() -> InterceptorContext { + let mut context = InterceptorContext::new(Input::doesnt_matter()); + context.enter_serialization_phase(); + context.set_request( + http::Request::builder() + .body(SdkBody::from(UNCOMPRESSED_INPUT)) + .unwrap() + .try_into() + .unwrap(), + ); + let _ = context.take_input(); + context.enter_before_transmit_phase(); + context + } + + #[tokio::test] + async fn test_sdk_feature_gzip_request_compression_should_be_tracked() { + let mut cfg = ConfigBag::base(); + let mut layer = Layer::new("test"); + layer.store_put(RequestMinCompressionSizeBytes::from(0)); + cfg.push_layer(layer); + let mut context = context(); + let ctx = Into::into(&context); + + let sut = RequestCompressionInterceptor::new(); + sut.read_before_execution(&ctx, &mut cfg).unwrap(); + + let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut ctx = Into::into(&mut context); + sut.modify_before_retry_loop(&mut ctx, &rc, &mut cfg) + .unwrap(); + + assert_eq!( + &SmithySdkFeature::GzipRequestCompression, + cfg.load::().next().unwrap() + ); + } } diff --git a/rust-runtime/inlineable/src/lib.rs b/rust-runtime/inlineable/src/lib.rs index 0e2a815e0..bf20f6c09 100644 --- a/rust-runtime/inlineable/src/lib.rs +++ b/rust-runtime/inlineable/src/lib.rs @@ -28,6 +28,8 @@ mod json_errors; mod rest_xml_unwrapped_errors; #[allow(unused)] mod rest_xml_wrapped_errors; +#[allow(dead_code)] +mod sdk_feature_tracker; #[allow(unused)] mod serialization_settings; diff --git a/rust-runtime/inlineable/src/sdk_feature_tracker.rs b/rust-runtime/inlineable/src/sdk_feature_tracker.rs new file mode 100644 index 000000000..2f36e121d --- /dev/null +++ b/rust-runtime/inlineable/src/sdk_feature_tracker.rs @@ -0,0 +1,174 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#[allow(dead_code)] +pub(crate) mod rpc_v2_cbor { + use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; + use aws_smithy_runtime_api::box_error::BoxError; + use aws_smithy_runtime_api::client::interceptors::context::BeforeSerializationInterceptorContextMut; + use aws_smithy_runtime_api::client::interceptors::Intercept; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; + use aws_smithy_types::config_bag::ConfigBag; + + #[derive(Debug)] + pub(crate) struct RpcV2CborFeatureTrackerInterceptor; + + impl RpcV2CborFeatureTrackerInterceptor { + pub(crate) fn new() -> Self { + Self + } + } + + impl Intercept for RpcV2CborFeatureTrackerInterceptor { + fn name(&self) -> &'static str { + "RpcV2CborFeatureTrackerInterceptor" + } + + fn modify_before_serialization( + &self, + _context: &mut BeforeSerializationInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + cfg.interceptor_state() + .store_append::(SmithySdkFeature::ProtocolRpcV2Cbor); + Ok(()) + } + } +} + +#[allow(dead_code)] +pub(crate) mod paginator { + use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; + use aws_smithy_runtime_api::box_error::BoxError; + use aws_smithy_runtime_api::client::interceptors::context::BeforeSerializationInterceptorContextMut; + use aws_smithy_runtime_api::client::interceptors::{Intercept, SharedInterceptor}; + use aws_smithy_runtime_api::client::runtime_components::{ + RuntimeComponents, RuntimeComponentsBuilder, + }; + use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; + use aws_smithy_types::config_bag::ConfigBag; + use std::borrow::Cow; + + #[derive(Debug)] + struct PaginatorFeatureTrackerInterceptor; + + impl PaginatorFeatureTrackerInterceptor { + pub(crate) fn new() -> Self { + Self + } + } + + impl Intercept for PaginatorFeatureTrackerInterceptor { + fn name(&self) -> &'static str { + "PaginatorFeatureTrackerInterceptor" + } + + fn modify_before_serialization( + &self, + _context: &mut BeforeSerializationInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + cfg.interceptor_state() + .store_append::(SmithySdkFeature::Paginator); + Ok(()) + } + } + + #[derive(Debug)] + pub(crate) struct PaginatorFeatureTrackerRuntimePlugin { + runtime_components: RuntimeComponentsBuilder, + } + + impl PaginatorFeatureTrackerRuntimePlugin { + pub(crate) fn new() -> Self { + Self { + runtime_components: RuntimeComponentsBuilder::new( + "PaginatorFeatureTrackerRuntimePlugin", + ) + .with_interceptor(SharedInterceptor::new( + PaginatorFeatureTrackerInterceptor::new(), + )), + } + } + } + + impl RuntimePlugin for PaginatorFeatureTrackerRuntimePlugin { + fn runtime_components( + &self, + _: &RuntimeComponentsBuilder, + ) -> Cow<'_, RuntimeComponentsBuilder> { + Cow::Borrowed(&self.runtime_components) + } + } +} + +#[allow(dead_code)] +pub(crate) mod waiter { + use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; + use aws_smithy_runtime_api::box_error::BoxError; + use aws_smithy_runtime_api::client::interceptors::context::BeforeSerializationInterceptorContextMut; + use aws_smithy_runtime_api::client::interceptors::{Intercept, SharedInterceptor}; + use aws_smithy_runtime_api::client::runtime_components::{ + RuntimeComponents, RuntimeComponentsBuilder, + }; + use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; + use aws_smithy_types::config_bag::ConfigBag; + use std::borrow::Cow; + + #[derive(Debug)] + struct WaiterFeatureTrackerInterceptor; + + impl WaiterFeatureTrackerInterceptor { + pub(crate) fn new() -> Self { + Self + } + } + + impl Intercept for WaiterFeatureTrackerInterceptor { + fn name(&self) -> &'static str { + "WaiterFeatureTrackerInterceptor" + } + + fn modify_before_serialization( + &self, + _context: &mut BeforeSerializationInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + cfg.interceptor_state() + .store_append::(SmithySdkFeature::Waiter); + Ok(()) + } + } + + #[derive(Debug)] + pub(crate) struct WaiterFeatureTrackerRuntimePlugin { + runtime_components: RuntimeComponentsBuilder, + } + + impl WaiterFeatureTrackerRuntimePlugin { + pub(crate) fn new() -> Self { + Self { + runtime_components: RuntimeComponentsBuilder::new( + "WaiterFeatureTrackerRuntimePlugin", + ) + .with_interceptor(SharedInterceptor::new( + WaiterFeatureTrackerInterceptor::new(), + )), + } + } + } + + impl RuntimePlugin for WaiterFeatureTrackerRuntimePlugin { + fn runtime_components( + &self, + _: &RuntimeComponentsBuilder, + ) -> Cow<'_, RuntimeComponentsBuilder> { + Cow::Borrowed(&self.runtime_components) + } + } +} -- GitLab