diff --git a/.changelog/observability-metrics.md b/.changelog/observability-metrics.md new file mode 100644 index 00000000000..855b342b51e --- /dev/null +++ b/.changelog/observability-metrics.md @@ -0,0 +1,9 @@ +--- +applies_to: ["client"] +authors: ["vcjana"] +references: [] +breaking: false +new_feature: false +bug_fix: false +--- +Add support for tracking observability business metrics (OBSERVABILITY_TRACING, OBSERVABILITY_OTEL_TRACING, OBSERVABILITY_OTEL_METRICS) in User-Agent headers when telemetry providers are configured. diff --git a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt index 8b20508b1ca..ff5440b4288 100644 --- a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt +++ b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt @@ -46,6 +46,7 @@ val DECORATORS: List = CredentialsProviderDecorator(), RegionDecorator(), RequireEndpointRules(), + ObservabilityMetricDecorator(), EndpointOverrideMetricDecorator(), UserAgentDecorator(), SigV4AuthDecorator(), diff --git a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/ObservabilityMetricDecorator.kt b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/ObservabilityMetricDecorator.kt new file mode 100644 index 00000000000..2a4e0d41140 --- /dev/null +++ b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/ObservabilityMetricDecorator.kt @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk + +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.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility +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 + +/** + * Decorator that tracks observability business metrics when tracing/metrics providers are configured. + */ +class ObservabilityMetricDecorator : ClientCodegenDecorator { + override val name: String = "ObservabilityMetric" + override val order: Byte = 0 + + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations + listOf(ObservabilityFeatureTrackerInterceptor(codegenContext)) +} + +private class ObservabilityFeatureTrackerInterceptor(private val codegenContext: ClientCodegenContext) : + ServiceRuntimePluginCustomization() { + override fun section(section: ServiceRuntimePluginSection) = + writable { + if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) { + section.registerInterceptor(this) { + val runtimeConfig = codegenContext.runtimeConfig + rustTemplate( + "#{Interceptor}", + "Interceptor" to + RuntimeType.forInlineDependency( + InlineAwsDependency.forRustFile( + "observability_feature", + Visibility.PRIVATE, + CargoDependency.smithyObservability(runtimeConfig), + ), + ).resolve("ObservabilityFeatureTrackerInterceptor"), + ) + } + } + } +} diff --git a/aws/rust-runtime/Cargo.lock b/aws/rust-runtime/Cargo.lock index 07af7d9b8cf..8a72e61e017 100644 --- a/aws/rust-runtime/Cargo.lock +++ b/aws/rust-runtime/Cargo.lock @@ -90,6 +90,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-checksums", "aws-smithy-http", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -113,7 +114,7 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.17" +version = "1.5.18" dependencies = [ "arbitrary", "aws-credential-types", @@ -263,7 +264,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.1.5" +version = "0.2.0" dependencies = [ "aws-smithy-runtime-api", ] @@ -287,7 +288,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.5" +version = "1.9.6" dependencies = [ "aws-smithy-async", "aws-smithy-http", diff --git a/aws/rust-runtime/aws-inlineable/Cargo.toml b/aws/rust-runtime/aws-inlineable/Cargo.toml index 1cac5fbeafb..e8601d6d7c1 100644 --- a/aws/rust-runtime/aws-inlineable/Cargo.toml +++ b/aws/rust-runtime/aws-inlineable/Cargo.toml @@ -23,6 +23,7 @@ aws-types = { path = "../aws-types" } aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["rt-tokio"] } aws-smithy-checksums = { path = "../../../rust-runtime/aws-smithy-checksums" } aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } +aws-smithy-observability = { path = "../../../rust-runtime/aws-smithy-observability" } 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", features = ["http-body-0-4-x"] } diff --git a/aws/rust-runtime/aws-inlineable/src/lib.rs b/aws/rust-runtime/aws-inlineable/src/lib.rs index fd5c34775f8..caa37959738 100644 --- a/aws/rust-runtime/aws-inlineable/src/lib.rs +++ b/aws/rust-runtime/aws-inlineable/src/lib.rs @@ -26,6 +26,10 @@ #[allow(dead_code)] pub mod account_id_endpoint; +/// Supporting code for tracking observability features (tracing/metrics). +#[allow(dead_code)] +pub mod observability_feature; + /// Supporting code for the aws-chunked content encoding. pub mod aws_chunked; diff --git a/aws/rust-runtime/aws-inlineable/src/observability_feature.rs b/aws/rust-runtime/aws-inlineable/src/observability_feature.rs new file mode 100644 index 00000000000..7885aecfc51 --- /dev/null +++ b/aws/rust-runtime/aws-inlineable/src/observability_feature.rs @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; +use aws_smithy_runtime_api::{ + box_error::BoxError, + client::interceptors::{context::BeforeSerializationInterceptorContextRef, Intercept}, +}; +use aws_smithy_types::config_bag::ConfigBag; + +// Interceptor that tracks Smithy SDK features for observability (tracing/metrics). +#[derive(Debug, Default)] +pub(crate) struct ObservabilityFeatureTrackerInterceptor; + +impl Intercept for ObservabilityFeatureTrackerInterceptor { + fn name(&self) -> &'static str { + "ObservabilityFeatureTrackerInterceptor" + } + + fn read_before_execution( + &self, + _context: &BeforeSerializationInterceptorContextRef<'_>, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Check if an OpenTelemetry meter provider is configured via the global provider + if let Ok(telemetry_provider) = aws_smithy_observability::global::get_telemetry_provider() { + let meter_provider = telemetry_provider.meter_provider(); + + // Use provider_name() to detect OpenTelemetry without importing the otel crate. + if meter_provider.provider_name() == "AwsSmithyObservabilityOtelProvider" { + cfg.interceptor_state() + .store_append(SmithySdkFeature::ObservabilityOtelMetrics); + } + } + + Ok(()) + } +} diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index edf75750e10..163f6d79232 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.5.17" +version = "1.5.18" authors = ["AWS Rust SDK Team "] description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly." edition = "2021" 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 eee7c7ec46b..66f4d33a7e4 100644 --- a/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs +++ b/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs @@ -210,6 +210,7 @@ impl ProvideBusinessMetric for SmithySdkFeature { FlexibleChecksumsResWhenRequired => { Some(BusinessMetric::FlexibleChecksumsResWhenRequired) } + ObservabilityOtelMetrics => Some(BusinessMetric::ObservabilityOtelMetrics), 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` diff --git a/aws/sdk/integration-tests/telemetry/Cargo.toml b/aws/sdk/integration-tests/telemetry/Cargo.toml index 221ea8780dd..dab849fd8b4 100644 --- a/aws/sdk/integration-tests/telemetry/Cargo.toml +++ b/aws/sdk/integration-tests/telemetry/Cargo.toml @@ -12,6 +12,7 @@ publish = false [dev-dependencies] aws-config = { path = "../../build/aws-sdk/sdk/aws-config", features = ["test-util", "behavior-version-latest"] } +aws-runtime = { path = "../../build/aws-sdk/sdk/aws-runtime", features = ["test-util"] } aws-sdk-dynamodb = { path = "../../build/aws-sdk/sdk/dynamodb", features = ["test-util", "behavior-version-latest"] } aws-sdk-s3 = { path = "../../build/aws-sdk/sdk/s3", features = ["test-util", "behavior-version-latest"] } aws-smithy-observability = { path = "../../build/aws-sdk/sdk/aws-smithy-observability" } diff --git a/aws/sdk/integration-tests/telemetry/tests/observability_feature_metrics.rs b/aws/sdk/integration-tests/telemetry/tests/observability_feature_metrics.rs new file mode 100644 index 00000000000..7df706f91a6 --- /dev/null +++ b/aws/sdk/integration-tests/telemetry/tests/observability_feature_metrics.rs @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_config::Region; +use aws_runtime::user_agent::test_util::{ + assert_ua_contains_metric_values, assert_ua_does_not_contain_metric_values, +}; +use aws_sdk_s3::config::{Credentials, SharedCredentialsProvider}; +use aws_smithy_observability::TelemetryProvider; +use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient}; +use aws_smithy_types::body::SdkBody; +use serial_test::serial; +use utils::init_metrics; + +mod utils; + +// Note: These tests are written with a multi-threaded runtime since OTel requires that to work +// and they are all run serially since they touch global state + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial] +async fn observability_otel_metrics_feature_tracked_in_user_agent() { + let (meter_provider, _exporter) = init_metrics(); + + // Create a replay client to capture the actual HTTP request + let http_client = StaticReplayClient::new(vec![ReplayEvent::new( + http::Request::builder().body(SdkBody::empty()).unwrap(), + http::Response::builder().body(SdkBody::empty()).unwrap(), + )]); + + let config = aws_config::SdkConfig::builder() + .credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests())) + .region(Region::new("us-east-1")) + .http_client(http_client.clone()) + .build(); + + let s3_client = aws_sdk_s3::Client::new(&config); + let _ = s3_client + .get_object() + .bucket("test-bucket") + .key("test.txt") + .send() + .await; + + // Get the actual HTTP request that was made + let requests = http_client.actual_requests(); + let last_request = requests.last().expect("should have made a request"); + + let user_agent = last_request + .headers() + .get("x-amz-user-agent") + .expect("should have user-agent header"); + + // Should contain OBSERVABILITY_OTEL_METRICS metric (value "7") + assert_ua_contains_metric_values(user_agent, &["7"]); + + meter_provider.flush().unwrap(); + + // Reset to noop for other tests + aws_smithy_observability::global::set_telemetry_provider(TelemetryProvider::noop()).unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial] +async fn noop_provider_does_not_track_observability_metrics() { + // Reset to noop provider + aws_smithy_observability::global::set_telemetry_provider(TelemetryProvider::noop()).unwrap(); + + // Create a replay client to capture the actual HTTP request + let http_client = StaticReplayClient::new(vec![ReplayEvent::new( + http::Request::builder().body(SdkBody::empty()).unwrap(), + http::Response::builder().body(SdkBody::empty()).unwrap(), + )]); + + let config = aws_config::SdkConfig::builder() + .credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests())) + .region(Region::new("us-east-1")) + .http_client(http_client.clone()) + .build(); + + let s3_client = aws_sdk_s3::Client::new(&config); + let _ = s3_client + .get_object() + .bucket("test-bucket") + .key("test.txt") + .send() + .await; + + // Get the actual HTTP request that was made + let requests = http_client.actual_requests(); + let last_request = requests.last().expect("should have made a request"); + + let user_agent = last_request + .headers() + .get("x-amz-user-agent") + .expect("should have user-agent header"); + + // Should NOT contain OBSERVABILITY_OTEL_METRICS metric when using noop provider + assert_ua_does_not_contain_metric_values(user_agent, &["7"]); +} diff --git a/aws/sdk/integration-tests/telemetry/tests/utils/mod.rs b/aws/sdk/integration-tests/telemetry/tests/utils/mod.rs index dd4c70dec8d..0a68268b364 100644 --- a/aws/sdk/integration-tests/telemetry/tests/utils/mod.rs +++ b/aws/sdk/integration-tests/telemetry/tests/utils/mod.rs @@ -43,6 +43,7 @@ pub(crate) fn init_metrics() -> (Arc, InMemoryMetricsExporter (sdk_ref, exporter) } +#[allow(dead_code)] pub(crate) fn new_replay_client(num_requests: usize, with_retry: bool) -> StaticReplayClient { let mut events = Vec::with_capacity(num_requests); let mut start = 0; @@ -95,6 +96,7 @@ pub(crate) fn extract_metric_attributes<'a>( .collect() } +#[allow(dead_code)] pub(crate) async fn make_s3_call(config: &SdkConfig) { let s3_client = aws_sdk_s3::Client::new(config); let _ = s3_client @@ -105,6 +107,7 @@ pub(crate) async fn make_s3_call(config: &SdkConfig) { .await; } +#[allow(dead_code)] pub(crate) async fn make_ddb_call(config: &SdkConfig) { let ddb_client = aws_sdk_dynamodb::Client::new(&config); let _ = ddb_client @@ -115,6 +118,7 @@ pub(crate) async fn make_ddb_call(config: &SdkConfig) { .await; } +#[allow(dead_code)] pub(crate) fn make_config(with_retry: bool) -> SdkConfig { SdkConfig::builder() .credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests())) 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 72ece5e46c2..7ac1e841491 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 @@ -413,6 +413,11 @@ data class CargoDependency( fun smithyMocks(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-mocks", scope = DependencyScope.Dev) + fun smithyObservability(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-observability") + + fun smithyObservabilityOtel(runtimeConfig: RuntimeConfig) = + runtimeConfig.smithyRuntimeCrate("smithy-observability-otel") + // behind feature-gate val Serde = CargoDependency("serde", CratesIo("1.0"), features = setOf("derive"), scope = DependencyScope.CfgUnstable) diff --git a/codegen-server-test/integration-tests/Cargo.lock b/codegen-server-test/integration-tests/Cargo.lock index be271443640..faf25fab9fb 100644 --- a/codegen-server-test/integration-tests/Cargo.lock +++ b/codegen-server-test/integration-tests/Cargo.lock @@ -155,7 +155,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.1.5" +version = "0.2.0" dependencies = [ "aws-smithy-runtime-api", ] diff --git a/gradle.properties b/gradle.properties index c4e124790a5..0bca2ea1f41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,4 @@ allowLocalDeps=false # Avoid registering dependencies/plugins/tasks that are only used for testing purposes isTestingEnabled=true # codegen publication version -codegenVersion=0.1.8 +codegenVersion=0.1.9 diff --git a/rust-runtime/Cargo.lock b/rust-runtime/Cargo.lock index aed0b60191c..1575dcfd348 100644 --- a/rust-runtime/Cargo.lock +++ b/rust-runtime/Cargo.lock @@ -605,7 +605,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.1.5" +version = "0.2.0" dependencies = [ "aws-smithy-runtime-api", "serial_test", @@ -613,7 +613,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability-otel" -version = "0.1.3" +version = "0.1.4" dependencies = [ "async-global-executor", "async-task", @@ -653,7 +653,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.5" +version = "1.9.7" dependencies = [ "approx", "aws-smithy-async", diff --git a/rust-runtime/aws-smithy-observability-otel/Cargo.toml b/rust-runtime/aws-smithy-observability-otel/Cargo.toml index 8895dfbcd26..ee4fc725318 100644 --- a/rust-runtime/aws-smithy-observability-otel/Cargo.toml +++ b/rust-runtime/aws-smithy-observability-otel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-observability-otel" -version = "0.1.3" +version = "0.1.4" authors = [ "AWS Rust SDK Team ", ] diff --git a/rust-runtime/aws-smithy-observability-otel/src/meter.rs b/rust-runtime/aws-smithy-observability-otel/src/meter.rs index e30a4e578e5..cef8fdf093e 100644 --- a/rust-runtime/aws-smithy-observability-otel/src/meter.rs +++ b/rust-runtime/aws-smithy-observability-otel/src/meter.rs @@ -286,6 +286,10 @@ impl ProvideMeter for OtelMeterProvider { fn get_meter(&self, scope: &'static str, _attributes: Option<&Attributes>) -> Meter { Meter::new(Arc::new(MeterWrap(self.meter_provider.meter(scope)))) } + + fn provider_name(&self) -> &'static str { + "AwsSmithyObservabilityOtelProvider" + } } #[cfg(test)] diff --git a/rust-runtime/aws-smithy-observability/Cargo.toml b/rust-runtime/aws-smithy-observability/Cargo.toml index 9fa7b16223a..6cc94dd6d16 100644 --- a/rust-runtime/aws-smithy-observability/Cargo.toml +++ b/rust-runtime/aws-smithy-observability/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-observability" -version = "0.1.5" +version = "0.2.0" authors = [ "AWS Rust SDK Team ", ] @@ -23,3 +23,4 @@ targets = ["x86_64-unknown-linux-gnu"] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] rustdoc-args = ["--cfg", "docsrs"] # End of docs.rs metadata + diff --git a/rust-runtime/aws-smithy-observability/src/meter.rs b/rust-runtime/aws-smithy-observability/src/meter.rs index 2cca5743d46..e97625be9cf 100644 --- a/rust-runtime/aws-smithy-observability/src/meter.rs +++ b/rust-runtime/aws-smithy-observability/src/meter.rs @@ -17,6 +17,12 @@ use std::{borrow::Cow, fmt::Debug, sync::Arc}; pub trait ProvideMeter: Send + Sync + Debug { /// Get or create a named [Meter]. fn get_meter(&self, scope: &'static str, attributes: Option<&Attributes>) -> Meter; + + /// Returns the name of this provider implementation. + /// This is used for feature tracking without requiring type imports. + fn provider_name(&self) -> &'static str { + "unknown" + } } /// The entry point to creating instruments. A grouping of related metrics. diff --git a/rust-runtime/aws-smithy-observability/src/noop.rs b/rust-runtime/aws-smithy-observability/src/noop.rs index 6a3b9f47307..2964d65e074 100644 --- a/rust-runtime/aws-smithy-observability/src/noop.rs +++ b/rust-runtime/aws-smithy-observability/src/noop.rs @@ -24,6 +24,10 @@ impl ProvideMeter for NoopMeterProvider { fn get_meter(&self, _scope: &'static str, _attributes: Option<&Attributes>) -> Meter { Meter::new(Arc::new(NoopMeter)) } + + fn provider_name(&self) -> &'static str { + "noop" + } } #[derive(Debug)] diff --git a/rust-runtime/aws-smithy-runtime/Cargo.toml b/rust-runtime/aws-smithy-runtime/Cargo.toml index 7b9fb7cde8d..5a034f75176 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.9.6" +version = "1.9.7" 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/sdk_feature.rs b/rust-runtime/aws-smithy-runtime/src/client/sdk_feature.rs index 254458dc914..70bf42fa7dd 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/sdk_feature.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/sdk_feature.rs @@ -23,6 +23,7 @@ pub enum SmithySdkFeature { FlexibleChecksumsReqWhenRequired, FlexibleChecksumsResWhenSupported, FlexibleChecksumsResWhenRequired, + ObservabilityOtelMetrics, } impl Storable for SmithySdkFeature {