Unverified Commit 5a19a6c5 authored by ysaito1001's avatar ysaito1001 Committed by GitHub
Browse files

Provide business metrics for RPC V2 CBOR, Gzip request compression, paginator, and waiter (#3793)

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

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

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

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

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
parent 3ee5dcbd
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -341,7 +341,7 @@ dependencies = [

[[package]]
name = "aws-smithy-types"
version = "1.2.1"
version = "1.2.2"
dependencies = [
 "base64-simd",
 "bytes",
+4 −2
Original line number Diff line number Diff line
[package]
name = "aws-runtime"
version = "1.4.0"
version = "1.4.1"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly."
edition = "2021"
@@ -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" }

+31 −7
Original line number Diff line number Diff line
@@ -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(),
+62 −31
Original line number Diff line number Diff line
@@ -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,20 +92,19 @@ 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
            .load::<AwsUserAgent>()
            .map(Cow::Borrowed)
            .map(Result::<_, UserAgentInterceptorError>::Ok)
            .unwrap_or_else(|| {
        if cfg.load::<AwsUserAgent>().is_some() {
            return Ok(());
        }

        let api_metadata = cfg
            .load::<ApiMetadata>()
            .ok_or(UserAgentInterceptorError::MissingApiMetadata)?;
@@ -112,6 +115,29 @@ impl Intercept for UserAgentInterceptor {
            ua.set_app_name(app_name.clone());
        }

        cfg.interceptor_state().store_put(ua);

        Ok(())
    }

    fn modify_before_signing(
        &self,
        context: &mut BeforeTransmitInterceptorContextMut<'_>,
        runtime_components: &RuntimeComponents,
        cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        let mut ua = cfg
            .load::<AwsUserAgent>()
            .expect("`AwsUserAgent should have been created in `read_before_execution`")
            .clone();

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

        let maybe_connector_metadata = runtime_components
            .http_client()
            .and_then(|c| c.connector_metadata());
@@ -120,9 +146,6 @@ impl Intercept for UserAgentInterceptor {
            ua.add_additional_metadata(am);
        }

                Ok(Cow::Owned(ua))
            })?;

        let headers = context.request_mut().headers_mut();
        let (user_agent, x_amz_user_agent) = header_values(&ua)?;
        headers.append(USER_AGENT, user_agent);
@@ -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")
            )
        );
+27 −0
Original line number Diff line number Diff line
@@ -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<BusinessMetric>;
}

impl ProvideBusinessMetric for SmithySdkFeature {
    fn provide_business_metric(&self) -> Option<BusinessMetric> {
        use SmithySdkFeature::*;
        match self {
            Waiter => Some(BusinessMetric::Waiter),
            Paginator => Some(BusinessMetric::Paginator),
            GzipRequestCompression => Some(BusinessMetric::GzipRequestCompression),
            ProtocolRpcV2Cbor => Some(BusinessMetric::ProtocolRpcV2Cbor),
            otherwise => {
                // This may occur if a customer upgrades only the `aws-smithy-runtime-api` crate
                // while continuing to use an outdated version of an SDK crate or the `aws-runtime`
                // crate.
                tracing::warn!(
                    "Attempted to provide `BusinessMetric` for `{otherwise:?}`, which is not recognized in the current version of the `aws-runtime` crate. \
                    Consider upgrading to the latest version to ensure that all tracked features are properly reported in your metrics."
                );
                None
            }
        }
    }
}

#[derive(Clone, Debug, Default)]
pub(super) struct BusinessMetrics(Vec<BusinessMetric>);

Loading