diff --git a/aws/rust-runtime/Cargo.lock b/aws/rust-runtime/Cargo.lock index b94f091e46557e84be27b39e6c9360e337bd7b0f..970f8f45f1e335922b6b47635e3945781a1f00f1 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 4886198937335f1a677b1971a468fd9d63ecb49f..95a61c4b4ca8b98e04282e63ed5784da0373ad32 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 b7aeeb8f8f7a5e356301d795b5bd7c7837ef0bcf..c13ce3962667cf8dd957deeae8047115cd679f57 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 fdcdd831a827095201d1aee08d0408eed81287bb..25d5f4ecf42d84fcc73a58edfd994ccaf2fafb80 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 2ecce131f9cc683bfcdac04d18981210a0baa034..1acaf86fc9519477b158f835e769010c161e08f6 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 0000000000000000000000000000000000000000..a0a5679b63b3167587af23c2b3b2d452dd1a4f7b --- /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 24b13b9c388971419553abeb3cafae2c69321040..5c713ef744897b4fabea084f016ba06def35d442 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 bf2c7b6d7ff5c841753eac81ab8c99349198a671..dbc30c940f32797702b3c0aef4ce91c520811ebb 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 f9778a75a43d8128d056d1dbf766a4dce303c8d5..402507a1a996ccfa036a851a7c50bb7d51079bcb 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 0ab3be626cb0bb0cc8cd89ab139d6a3351415fed..36dc3152b298c41d19d5abf8870b1a9e58c51e28 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 88d725f0b3922bc46b5a05f4eb177854fbc73ffd..8efbbb5edeceb044ae14a8f3e87644d12aa188d7 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 7f9c07c28882b03948a824c1cb0ef5b3370ce7a4..26e30a2ead7788bab7a59cd004c96d6db9d57a4e 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 0000000000000000000000000000000000000000..8d5cade12f240dbb45fbb079def66f1c5a6bbbc4 --- /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 3c3921ecd750713a8eb4482f652d60bebe8c76e1..1345ce7b6bf41bfeb614b8d51fb4d421b0167509 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 00c715b56bfd19671177d7e354682d48479f5922..e43763a0105f658f056ab708c24a456260404daf 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 24c7e54268c41af2984dcc78c527de21b310306a..3c82f3505d90446b945144aaf09c9e4c54808a08 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 1a2d2adf427dddfdf0c72490ff1b9d1f828cab72..60433a38d537d0079abe96cc00b013df245ca39e 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 db146e9959f3f22b6c8c3d706045527ce9123458..473df11e732301a4aafb03098791b9cdee358f66 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 0000000000000000000000000000000000000000..5a8ab95a13048e6ea097372655f89dfa3f0bbb7b --- /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 00a6fca12290361d31c918b2be0d634fcbdd7f0b..2b39b2585483fb29508a20af6adc6b04003f557e 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 2ea9c26e64676692fbfcf4c3e24659b44556dcb0..6211ca88cacb43e6ecfdb6e1c56c2a6fdb12a4db 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 0e2a815e056c3ca975d8b0247f9f70e4c2b5f8f1..bf20f6c0917a36fb5d1c1ae6e8758c4c612d09cf 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 0000000000000000000000000000000000000000..2f36e121df046afc52749b0e8936a2a4c6bf5662 --- /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) + } + } +}