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 267fce553b9..9d4b3b0e02e 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(), + EndpointOverrideDecorator(), UserAgentDecorator(), SigV4AuthDecorator(), HttpRequestChecksumDecorator(), @@ -65,6 +66,7 @@ val DECORATORS: List = AwsRequestIdDecorator(), DisabledAuthDecorator(), RecursionDetectionDecorator(), + ObservabilityDetectionDecorator(), InvocationIdDecorator(), RetryInformationHeaderDecorator(), RemoveDefaultsDecorator(), diff --git a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/EndpointOverrideDecorator.kt b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/EndpointOverrideDecorator.kt new file mode 100644 index 00000000000..76dd87799fc --- /dev/null +++ b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/EndpointOverrideDecorator.kt @@ -0,0 +1,49 @@ +/* + * 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.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.writable + +/** + * Registers the EndpointOverrideInterceptor to detect custom endpoint usage for business metrics + */ +class EndpointOverrideDecorator : ClientCodegenDecorator { + override val name: String = "EndpointOverride" + override val order: Byte = 0 + + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations + EndpointOverrideRuntimePluginCustomization(codegenContext) + + private class EndpointOverrideRuntimePluginCustomization(codegenContext: ClientCodegenContext) : + ServiceRuntimePluginCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig) + + override fun section(section: ServiceRuntimePluginSection): Writable = + writable { + when (section) { + is ServiceRuntimePluginSection.RegisterRuntimeComponents -> { + section.registerInterceptor(this) { + rust( + "#T::new()", + awsRuntime.resolve("endpoint_override::EndpointOverrideInterceptor"), + ) + } + } + else -> emptySection + } + } + } +} diff --git a/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/ObservabilityDetectionDecorator.kt b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/ObservabilityDetectionDecorator.kt new file mode 100644 index 00000000000..5d674416151 --- /dev/null +++ b/aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/ObservabilityDetectionDecorator.kt @@ -0,0 +1,49 @@ +/* + * 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.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.rustlang.writable + +/** + * Registers the ObservabilityDetectionInterceptor to detect observability feature usage for business metrics + */ +class ObservabilityDetectionDecorator : ClientCodegenDecorator { + override val name: String = "ObservabilityDetection" + override val order: Byte = 0 + + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations + ObservabilityDetectionRuntimePluginCustomization(codegenContext) + + private class ObservabilityDetectionRuntimePluginCustomization(codegenContext: ClientCodegenContext) : + ServiceRuntimePluginCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig) + + override fun section(section: ServiceRuntimePluginSection): Writable = + writable { + when (section) { + is ServiceRuntimePluginSection.RegisterRuntimeComponents -> { + section.registerInterceptor(this) { + rust( + "#T::new()", + awsRuntime.resolve("observability_detection::ObservabilityDetectionInterceptor"), + ) + } + } + else -> emptySection + } + } + } +} diff --git a/aws/codegen-aws-sdk/src/test/kotlin/software/amazon/smithy/rustsdk/EndpointOverrideDecoratorTest.kt b/aws/codegen-aws-sdk/src/test/kotlin/software/amazon/smithy/rustsdk/EndpointOverrideDecoratorTest.kt new file mode 100644 index 00000000000..c59778a695a --- /dev/null +++ b/aws/codegen-aws-sdk/src/test/kotlin/software/amazon/smithy/rustsdk/EndpointOverrideDecoratorTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk + +import org.junit.jupiter.api.Test +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +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 EndpointOverrideDecoratorTest { + companion object { + private const val PREFIX = "\$version: \"2\"" + val model = + """ + $PREFIX + namespace test + + use aws.api#service + use aws.auth#sigv4 + use aws.protocols#restJson1 + use smithy.rules#endpointRuleSet + + @service(sdkId: "dontcare") + @restJson1 + @sigv4(name: "dontcare") + @auth([sigv4]) + @endpointRuleSet({ + "version": "1.0", + "rules": [{ "type": "endpoint", "conditions": [], "endpoint": { "url": "https://example.com" } }], + "parameters": { + "Region": { "required": false, "type": "String", "builtIn": "AWS::Region" }, + } + }) + service TestService { + version: "2023-01-01", + operations: [SomeOperation] + } + + @http(uri: "/SomeOperation", method: "GET") + @optionalAuth + operation SomeOperation { + input: SomeInput, + output: SomeOutput + } + + @input + structure SomeInput {} + + @output + structure SomeOutput {} + """.asSmithyModel() + } + + @Test + fun `decorator is registered in AwsCodegenDecorator list`() { + // Verify that EndpointOverrideDecorator is in the DECORATORS list + val decoratorNames = DECORATORS.map { it.name } + assert(decoratorNames.contains("EndpointOverride")) { + "EndpointOverrideDecorator should be registered in DECORATORS list" + } + } + + @Test + fun `endpoint override interceptor adds business metric to user agent`() { + val testParams = + awsIntegrationTestParams().copy( + additionalSettings = + awsIntegrationTestParams().additionalSettings.toBuilder() + .withMember( + "codegen", + software.amazon.smithy.model.node.ObjectNode.builder() + .withMember("includeFluentClient", false) + .withMember("includeEndpointUrlConfig", true) + .build(), + ) + .build(), + ) + + awsSdkIntegrationTest(model, testParams) { context, rustCrate -> + val rc = context.runtimeConfig + val moduleName = context.moduleUseName() + rustCrate.integrationTest("endpoint_override_functional") { + tokioTest("interceptor_adds_metric_when_endpoint_overridden") { + 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")) + .endpoint_url("https://custom.example.com") + .http_client(http_client.clone()) + .build(); + let client = Client::from_conf(config); + + // CRITICAL: Actually make a request + let _ = client.some_operation().send().await; + + // Capture and verify the request + let request = rcvr.expect_request(); + + // Verify endpoint was overridden + let uri = request.uri().to_string(); + assert!( + uri.starts_with("https://custom.example.com"), + "Expected custom endpoint, got: {}", + uri + ); + + // Verify x-amz-user-agent contains business metric 'N' (endpoint override) + // The metric appears in the business-metrics section as "m/..." with comma-separated IDs + let x_amz_user_agent = request.headers() + .get("x-amz-user-agent") + .expect("x-amz-user-agent header missing"); + + // Extract the business metrics section (starts with "m/") + let has_endpoint_override_metric = x_amz_user_agent + .split_whitespace() + .find(|part| part.starts_with("m/")) + .map(|metrics| { + // Check if 'N' appears as a metric ID (either alone or in a comma-separated list) + metrics.strip_prefix("m/") + .map(|ids| ids.split(',').any(|id| id == "N")) + .unwrap_or(false) + }) + .unwrap_or(false); + + assert!( + has_endpoint_override_metric, + "Expected metric ID 'N' (endpoint override) in x-amz-user-agent business metrics, got: {}", + x_amz_user_agent + ); + """, + *preludeScope, + "capture_request" to RuntimeType.captureRequest(rc), + ) + } + } + } + } +} diff --git a/aws/rust-runtime/Cargo.lock b/aws/rust-runtime/Cargo.lock index 0de572d74d5..172faae996b 100644 --- a/aws/rust-runtime/Cargo.lock +++ b/aws/rust-runtime/Cargo.lock @@ -45,6 +45,156 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -70,7 +220,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-credential-types" -version = "1.2.9" +version = "1.2.10" dependencies = [ "async-trait", "aws-smithy-async", @@ -121,6 +271,8 @@ dependencies = [ "aws-smithy-async", "aws-smithy-eventstream", "aws-smithy-http", + "aws-smithy-observability", + "aws-smithy-observability-otel", "aws-smithy-protocol-test", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -141,6 +293,7 @@ dependencies = [ "regex-lite", "serde", "serde_json", + "serial_test", "tokio", "tracing", "tracing-subscriber", @@ -262,11 +415,23 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.1.4" +version = "0.2.0" dependencies = [ "aws-smithy-runtime-api", ] +[[package]] +name = "aws-smithy-observability-otel" +version = "0.1.3" +dependencies = [ + "async-global-executor", + "async-task", + "aws-smithy-observability", + "opentelemetry", + "opentelemetry_sdk", + "value-bag", +] + [[package]] name = "aws-smithy-protocol-test" version = "0.63.6" @@ -281,12 +446,12 @@ dependencies = [ "regex-lite", "roxmltree", "serde_json", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "aws-smithy-runtime" -version = "1.9.4" +version = "1.9.5" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -421,6 +586,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bs58" version = "0.5.1" @@ -554,6 +732,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -617,7 +804,7 @@ checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" dependencies = [ "crc", "digest", - "rand", + "rand 0.9.2", "regex", "rustversion", ] @@ -837,6 +1024,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -880,6 +1094,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -887,6 +1116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -895,6 +1125,47 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -913,10 +1184,16 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -952,6 +1229,24 @@ dependencies = [ "wasip2", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "group" version = "0.12.1" @@ -1313,6 +1608,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1351,6 +1655,9 @@ name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +dependencies = [ + "value-bag", +] [[package]] name = "lru" @@ -1485,6 +1792,42 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "opentelemetry" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "570074cc999d1a58184080966e5bd3bf3a9a4af650c3b05047c2621e7405cd17" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror 1.0.69", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c627d9f4c9cdc1f21a29ee4bfbd6028fcb8bcf2a857b43f3abdf72c9c862f3" +dependencies = [ + "async-std", + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "percent-encoding", + "rand 0.8.5", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", +] + [[package]] name = "outref" version = "0.5.2" @@ -1502,6 +1845,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1543,6 +1892,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -1581,6 +1941,20 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1634,8 +2008,8 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand", - "rand_chacha", + "rand 0.9.2", + "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -1664,16 +2038,37 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -1907,6 +2302,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.28" @@ -1932,6 +2336,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "sec1" version = "0.3.0" @@ -2024,6 +2434,31 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2169,13 +2604,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2301,6 +2756,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -2472,6 +2938,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + [[package]] name = "version_check" version = "0.9.5" @@ -2540,6 +3012,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.105" diff --git a/aws/rust-runtime/aws-config/Cargo.lock b/aws/rust-runtime/aws-config/Cargo.lock index 727494630a3..704e7d8f1e2 100644 --- a/aws/rust-runtime/aws-config/Cargo.lock +++ b/aws/rust-runtime/aws-config/Cargo.lock @@ -108,6 +108,7 @@ dependencies = [ "aws-sigv4", "aws-smithy-async", "aws-smithy-http", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -273,7 +274,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.1.4" +version = "0.1.5" dependencies = [ "aws-smithy-runtime-api", ] @@ -305,7 +306,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.4" +version = "1.9.5" dependencies = [ "aws-smithy-async", "aws-smithy-http", diff --git a/aws/rust-runtime/aws-credential-types/Cargo.toml b/aws/rust-runtime/aws-credential-types/Cargo.toml index b9a352d6553..a06192231b7 100644 --- a/aws/rust-runtime/aws-credential-types/Cargo.toml +++ b/aws/rust-runtime/aws-credential-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-credential-types" -version = "1.2.9" +version = "1.2.10" authors = ["AWS Rust SDK Team "] description = "Types for AWS SDK credentials." edition = "2021" diff --git a/aws/rust-runtime/aws-credential-types/src/credentials_impl.rs b/aws/rust-runtime/aws-credential-types/src/credentials_impl.rs index eef3523b72f..8c2666869fb 100644 --- a/aws/rust-runtime/aws-credential-types/src/credentials_impl.rs +++ b/aws/rust-runtime/aws-credential-types/src/credentials_impl.rs @@ -431,7 +431,7 @@ mod test { "debug tester", ); assert_eq!( - format!("{:?}", creds), + format!("{creds:?}"), r#"Credentials { provider_name: "debug tester", access_key_id: "akid", secret_access_key: "** redacted **", expires_after: "2009-02-13T23:31:30Z" }"# ); @@ -445,7 +445,7 @@ mod test { .provider_name("debug tester") .build(); assert_eq!( - format!("{:?}", creds), + format!("{creds:?}"), r#"Credentials { provider_name: "debug tester", access_key_id: "akid", secret_access_key: "** redacted **", expires_after: "2009-02-13T23:31:30Z", account_id: "012345678901" }"# ); } diff --git a/aws/rust-runtime/aws-inlineable/src/aws_chunked.rs b/aws/rust-runtime/aws-inlineable/src/aws_chunked.rs index bb647f248f4..ec8dbc2de07 100644 --- a/aws/rust-runtime/aws-inlineable/src/aws_chunked.rs +++ b/aws/rust-runtime/aws-inlineable/src/aws_chunked.rs @@ -180,7 +180,7 @@ mod tests { let mut file = NamedTempFile::new().unwrap(); for i in 0..10000 { - let line = format!("This is a large file created for testing purposes {}", i); + let line = format!("This is a large file created for testing purposes {i}"); file.as_file_mut().write_all(line.as_bytes()).unwrap(); } @@ -302,7 +302,7 @@ mod tests { async fn streaming_body(path: impl AsRef) -> SdkBody { let file = path.as_ref(); ByteStream::read_from() - .path(&file) + .path(file) .build() .await .unwrap() diff --git a/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs b/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs index c3695840006..f010d98cf51 100644 --- a/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs +++ b/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs @@ -427,6 +427,7 @@ mod tests { use http_body::Body; use tempfile::NamedTempFile; + #[allow(clippy::type_complexity)] fn create_test_interceptor() -> RequestChecksumInterceptor< impl Fn(&Input) -> (Option, bool) + Send + Sync, impl Fn(&mut Request, &ConfigBag) -> Result + Send + Sync, @@ -449,7 +450,7 @@ mod tests { let mut crc32c_checksum = checksum_algorithm.into_impl(); for i in 0..10000 { - let line = format!("This is a large file created for testing purposes {}", i); + let line = format!("This is a large file created for testing purposes {i}"); file.as_file_mut().write_all(line.as_bytes()).unwrap(); crc32c_checksum.update(line.as_bytes()); } @@ -495,7 +496,7 @@ mod tests { body_data.extend_from_slice(&data.unwrap()) } let body_str = std::str::from_utf8(&body_data).unwrap(); - let expected = format!("This is a large file created for testing purposes 9999"); + let expected = "This is a large file created for testing purposes 9999".to_string(); assert!( body_str.ends_with(&expected), "expected '{body_str}' to end with '{expected}'" diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index cc874a6d471..e922b4a137b 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -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-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" } @@ -37,6 +38,9 @@ regex-lite = { version = "0.1.5", optional = true } tracing = "0.1.40" uuid = { version = "1" } +[target.'cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))'.dependencies] +aws-smithy-observability-otel = { version = "0.1.3", path = "../../../rust-runtime/aws-smithy-observability-otel" } + [dev-dependencies] arbitrary = "1.3" aws-credential-types = { path = "../aws-credential-types", features = ["test-util"] } @@ -50,6 +54,7 @@ futures-util = { version = "0.3.29", default-features = false } proptest = "1.2" serde = { version = "1", features = ["derive"]} serde_json = "1" +serial_test = "3" tokio = { version = "1.23.1", features = ["macros", "rt", "time"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-test = "0.2.4" diff --git a/aws/rust-runtime/aws-runtime/src/endpoint_override.rs b/aws/rust-runtime/aws-runtime/src/endpoint_override.rs new file mode 100644 index 00000000000..33b30c6de10 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/endpoint_override.rs @@ -0,0 +1,159 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Endpoint override detection for business metrics tracking + +use aws_smithy_runtime_api::box_error::BoxError; +use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextRef; +use aws_smithy_runtime_api::client::interceptors::Intercept; +use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; +use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin; +use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Layer}; + +use crate::sdk_feature::AwsSdkFeature; + +/// Interceptor that detects custom endpoint URLs for business metrics +/// +/// This interceptor checks at runtime if a `StaticUriEndpointResolver` is configured, +/// which indicates that `.endpoint_url()` was called. When detected, it stores the +/// `AwsSdkFeature::EndpointOverride` feature flag for business metrics tracking. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct EndpointOverrideInterceptor; + +impl EndpointOverrideInterceptor { + /// Creates a new EndpointOverrideInterceptor + pub fn new() -> Self { + Self + } +} + +impl Intercept for EndpointOverrideInterceptor { + fn name(&self) -> &'static str { + "EndpointOverrideInterceptor" + } + + fn read_after_serialization( + &self, + _context: &BeforeTransmitInterceptorContextRef<'_>, + runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Check if the endpoint resolver is a StaticUriEndpointResolver + // This indicates that .endpoint_url() was called + let resolver = runtime_components.endpoint_resolver(); + + // Check the resolver's debug string to see if it's StaticUriEndpointResolver + let debug_str = format!("{resolver:?}"); + + if debug_str.contains("StaticUriEndpointResolver") { + // Store in interceptor_state + cfg.interceptor_state() + .store_append(AwsSdkFeature::EndpointOverride); + } + + Ok(()) + } +} + +/// Runtime plugin that detects when a custom endpoint URL has been configured +/// and tracks it for business metrics. +/// +/// This plugin is created by the codegen decorator when a user explicitly +/// sets an endpoint URL via `.endpoint_url()`. It stores the +/// `AwsSdkFeature::EndpointOverride` feature flag in the ConfigBag for +/// business metrics tracking. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct EndpointOverrideRuntimePlugin { + config: Option, +} + +impl EndpointOverrideRuntimePlugin { + /// Creates a new `EndpointOverrideRuntimePlugin` with the given config layer + pub fn new(config: Option) -> Self { + Self { config } + } + + /// Creates a new `EndpointOverrideRuntimePlugin` and marks that endpoint override is enabled + pub fn new_with_feature_flag() -> Self { + let mut layer = Layer::new("endpoint_override"); + layer.store_append(AwsSdkFeature::EndpointOverride); + Self { + config: Some(layer.freeze()), + } + } +} + +impl RuntimePlugin for EndpointOverrideRuntimePlugin { + fn config(&self) -> Option { + self.config.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sdk_feature::AwsSdkFeature; + + #[test] + fn test_plugin_with_no_config() { + let plugin = EndpointOverrideRuntimePlugin::default(); + assert!(plugin.config().is_none()); + } + + #[test] + fn test_plugin_with_feature_flag() { + let plugin = EndpointOverrideRuntimePlugin::new_with_feature_flag(); + let config = plugin.config().expect("config should be set"); + + // Verify the feature flag is present in the config + let features: Vec<_> = config.load::().cloned().collect(); + assert_eq!(features.len(), 1); + assert_eq!(features[0], AwsSdkFeature::EndpointOverride); + } + + #[test] + fn test_interceptor_detects_static_uri_resolver() { + use aws_smithy_runtime::client::orchestrator::endpoints::StaticUriEndpointResolver; + use aws_smithy_runtime_api::client::endpoint::SharedEndpointResolver; + use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext}; + use aws_smithy_runtime_api::client::orchestrator::HttpRequest; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; + use aws_smithy_types::config_bag::ConfigBag; + + // Create a StaticUriEndpointResolver + let endpoint_resolver = SharedEndpointResolver::new(StaticUriEndpointResolver::uri( + "https://custom.example.com", + )); + + let mut context = InterceptorContext::new(Input::doesnt_matter()); + context.enter_serialization_phase(); + context.set_request(HttpRequest::empty()); + let _ = context.take_input(); + context.enter_before_transmit_phase(); + + let rc = RuntimeComponentsBuilder::for_tests() + .with_endpoint_resolver(Some(endpoint_resolver)) + .build() + .unwrap(); + let mut cfg = ConfigBag::base(); + + let interceptor = EndpointOverrideInterceptor::new(); + let ctx = Into::into(&context); + interceptor + .read_after_serialization(&ctx, &rc, &mut cfg) + .unwrap(); + + // Verify the feature flag was set + let features: Vec<_> = cfg + .interceptor_state() + .load::() + .cloned() + .collect(); + assert_eq!(features.len(), 1, "Expected 1 feature, got: {features:?}"); + assert_eq!(features[0], AwsSdkFeature::EndpointOverride); + } +} diff --git a/aws/rust-runtime/aws-runtime/src/env_config/section.rs b/aws/rust-runtime/aws-runtime/src/env_config/section.rs index bb8c9015e26..6e7b39a7f06 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config/section.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/section.rs @@ -292,7 +292,7 @@ mod test { let tests = fs::read_to_string("test-data/profile-parser-tests.json")?; let tests: ParserTests = serde_json::from_str(&tests)?; for (i, test) in tests.tests.into_iter().enumerate() { - eprintln!("test: {}", i); + eprintln!("test: {i}"); check(test); } Ok(()) @@ -432,22 +432,22 @@ mod test { } } (Err(msg), ParserOutput::ErrorContaining(substr)) => { - if format!("{}", msg).contains(substr) { + if format!("{msg}").contains(substr) { Ok(()) } else { - Err(format!("Expected {} to contain {}", msg, substr)) + Err(format!("Expected {msg} to contain {substr}")) } } (Ok(output), ParserOutput::ErrorContaining(err)) => Err(format!( "expected an error: {err} but parse succeeded:\n{output:#?}", )), (Err(err), ParserOutput::Config { .. }) => { - Err(format!("Expected to succeed but got: {}", err)) + Err(format!("Expected to succeed but got: {err}")) } }; if let Err(e) = res { - eprintln!("Test case failed: {:#?}", copy); - eprintln!("failure: {}", e); + eprintln!("Test case failed: {copy:#?}"); + eprintln!("failure: {e}"); panic!("test failed") } } diff --git a/aws/rust-runtime/aws-runtime/src/env_config/source.rs b/aws/rust-runtime/aws-runtime/src/env_config/source.rs index 8661b938266..0351234c7a6 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config/source.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/source.rs @@ -245,7 +245,7 @@ mod tests { let tests = fs::read_to_string("test-data/file-location-tests.json")?; let tests: SourceTests = serde_json::from_str(&tests)?; for (i, test) in tests.tests.into_iter().enumerate() { - eprintln!("test: {}", i); + eprintln!("test: {i}"); check(test) .now_or_never() .expect("these futures should never poll"); diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index 702b2ee61d5..8716029de31 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -26,6 +26,12 @@ pub mod content_encoding; /// Supporting code for recursion detection in the AWS SDK. pub mod recursion_detection; +/// Supporting code for endpoint override detection in the AWS SDK. +pub mod endpoint_override; + +/// Supporting code for observability feature detection in the AWS SDK. +pub mod observability_detection; + /// Supporting code for user agent headers in the AWS SDK. pub mod user_agent; diff --git a/aws/rust-runtime/aws-runtime/src/observability_detection.rs b/aws/rust-runtime/aws-runtime/src/observability_detection.rs new file mode 100644 index 00000000000..8d3c21c0f0e --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/observability_detection.rs @@ -0,0 +1,219 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Observability feature detection for business metrics tracking +//! +//! This module provides an interceptor for detecting observability features in the AWS SDK: +//! +//! - [`crate::observability_detection::ObservabilityDetectionInterceptor`]: Detects observability features during +//! request processing and tracks them for business metrics in the User-Agent header. + +#[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))] +use crate::sdk_feature::AwsSdkFeature; +#[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))] +use aws_smithy_observability_otel::meter::OtelMeterProvider; +#[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))] +use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; +use aws_smithy_runtime_api::box_error::BoxError; +use aws_smithy_runtime_api::client::interceptors::context::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; + +/// Interceptor that detects when observability features are being used +/// and tracks them for business metrics. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct ObservabilityDetectionInterceptor; + +impl ObservabilityDetectionInterceptor { + /// Creates a new `ObservabilityDetectionInterceptor` + pub fn new() -> Self { + Self + } +} + +impl Intercept for ObservabilityDetectionInterceptor { + fn name(&self) -> &'static str { + "ObservabilityDetectionInterceptor" + } + + fn read_before_signing( + &self, + _context: &BeforeTransmitInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + _cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))] + { + // Try to get the global telemetry provider + if let Ok(provider) = aws_smithy_observability::global::get_telemetry_provider() { + let meter_provider = provider.meter_provider(); + + // Check if this is an OpenTelemetry meter provider + let is_otel = meter_provider + .as_any() + .downcast_ref::() + .is_some(); + + // Check if this is a noop provider (we don't want to track noop) + let is_noop = meter_provider + .as_any() + .downcast_ref::() + .is_some(); + + if !is_noop { + // Track generic observability metrics (for any non-noop provider) + _cfg.interceptor_state() + .store_append(SmithySdkFeature::ObservabilityMetrics); + + // If it's specifically OpenTelemetry, track that too + if is_otel { + _cfg.interceptor_state() + .store_append(AwsSdkFeature::ObservabilityOtelMetrics); + } + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sdk_feature::AwsSdkFeature; + use aws_smithy_observability::TelemetryProvider; + use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext}; + use aws_smithy_runtime_api::client::orchestrator::HttpRequest; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; + use aws_smithy_types::config_bag::ConfigBag; + + #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))] + #[test] + #[serial_test::serial] + fn test_detects_noop_provider() { + let mut context = InterceptorContext::new(Input::doesnt_matter()); + context.enter_serialization_phase(); + context.set_request(HttpRequest::empty()); + let _ = context.take_input(); + context.enter_before_transmit_phase(); + + let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut cfg = ConfigBag::base(); + + // Set a noop provider (ignore error if already set by another test) + let _ = aws_smithy_observability::global::set_telemetry_provider(TelemetryProvider::noop()); + + let interceptor = ObservabilityDetectionInterceptor::new(); + let ctx = Into::into(&context); + interceptor + .read_before_signing(&ctx, &rc, &mut cfg) + .unwrap(); + + // Should not track any features for noop provider + let smithy_features: Vec<_> = cfg + .interceptor_state() + .load::() + .cloned() + .collect(); + assert_eq!( + smithy_features.len(), + 0, + "Should not track Smithy features for noop provider" + ); + + let aws_features: Vec<_> = cfg + .interceptor_state() + .load::() + .cloned() + .collect(); + assert_eq!( + aws_features.len(), + 0, + "Should not track AWS features for noop provider" + ); + } + + #[cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))] + #[test] + #[serial_test::serial] + fn test_custom_provider_not_detected_as_otel() { + use aws_smithy_observability::meter::{Meter, ProvideMeter}; + use aws_smithy_observability::noop::NoopMeterProvider; + use aws_smithy_observability::Attributes; + use std::sync::Arc; + + // Create a custom (non-OTel, non-noop) meter provider + // This simulates a user implementing their own metrics provider + #[derive(Debug)] + struct CustomMeterProvider { + inner: NoopMeterProvider, + } + + impl ProvideMeter for CustomMeterProvider { + fn get_meter(&self, scope: &'static str, attributes: Option<&Attributes>) -> Meter { + // Delegate to noop for simplicity, but this is a distinct type + self.inner.get_meter(scope, attributes) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + let mut context = InterceptorContext::new(Input::doesnt_matter()); + context.enter_serialization_phase(); + context.set_request(HttpRequest::empty()); + let _ = context.take_input(); + context.enter_before_transmit_phase(); + + let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut cfg = ConfigBag::base(); + + // Set the custom provider + let custom_provider = Arc::new(CustomMeterProvider { + inner: NoopMeterProvider, + }); + let telemetry_provider = TelemetryProvider::builder() + .meter_provider(custom_provider) + .build(); + let _ = aws_smithy_observability::global::set_telemetry_provider(telemetry_provider); + + let interceptor = ObservabilityDetectionInterceptor::new(); + let ctx = Into::into(&context); + interceptor + .read_before_signing(&ctx, &rc, &mut cfg) + .unwrap(); + + // Should track generic observability metrics for custom provider + let smithy_features: Vec<_> = cfg + .interceptor_state() + .load::() + .cloned() + .collect(); + assert!( + smithy_features.contains(&SmithySdkFeature::ObservabilityMetrics), + "Should detect custom provider as having observability metrics" + ); + + // Should NOT track AWS-specific observability metrics for custom provider + let aws_features: Vec<_> = cfg + .interceptor_state() + .load::() + .cloned() + .collect(); + assert!( + !aws_features.contains(&AwsSdkFeature::ObservabilityOtelMetrics), + "Should NOT track OTel-specific metrics for custom provider" + ); + assert_eq!( + aws_features.len(), + 0, + "Should not track any AWS-specific features for custom provider" + ); + } +} diff --git a/aws/rust-runtime/aws-runtime/src/sdk_feature.rs b/aws/rust-runtime/aws-runtime/src/sdk_feature.rs index a1d03e4aa23..20713ff689c 100644 --- a/aws/rust-runtime/aws-runtime/src/sdk_feature.rs +++ b/aws/rust-runtime/aws-runtime/src/sdk_feature.rs @@ -24,6 +24,12 @@ pub enum AwsSdkFeature { SsoLoginDevice, /// Calling an SSO-OIDC operation as part of the SSO login flow, when using the OAuth2.0 authorization code grant SsoLoginAuth, + /// An operation called using a user provided endpoint URL + EndpointOverride, + /// An operation called with OpenTelemetry tracing integration enabled + ObservabilityOtelTracing, + /// An operation called with OpenTelemetry metrics integration enabled + ObservabilityOtelMetrics, } impl Storable for AwsSdkFeature { 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 795a17ea2aa..00f7a1448e0 100644 --- a/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs +++ b/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs @@ -437,4 +437,37 @@ mod tests { expect_header(&context, "x-amz-user-agent") ); } + + #[test] + fn test_cfg_load_captures_all_feature_layers() { + use crate::sdk_feature::AwsSdkFeature; + + // Create a ConfigBag with features in both base layer and interceptor_state + let mut base_layer = Layer::new("base"); + base_layer.store_append(AwsSdkFeature::EndpointOverride); + + let mut config = ConfigBag::of_layers(vec![base_layer]); + + // Store a feature in interceptor_state (simulating what interceptors do) + config + .interceptor_state() + .store_append(AwsSdkFeature::SsoLoginDevice); + + // Verify that cfg.load() captures features from all layers + let all_features: Vec<&AwsSdkFeature> = config.load::().collect(); + + assert_eq!( + all_features.len(), + 2, + "cfg.load() should capture features from all layers" + ); + assert!( + all_features.contains(&&AwsSdkFeature::EndpointOverride), + "should contain feature from base layer" + ); + assert!( + all_features.contains(&&AwsSdkFeature::SsoLoginDevice), + "should contain feature from interceptor_state" + ); + } } 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 f8497216aca..08f629615ef 100644 --- a/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs +++ b/aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs @@ -164,7 +164,11 @@ iterable_enum!( CredentialsImds, SsoLoginDevice, SsoLoginAuth, - BearerServiceEnvVars + BearerServiceEnvVars, + ObservabilityTracing, + ObservabilityMetrics, + ObservabilityOtelTracing, + ObservabilityOtelMetrics ); pub(crate) trait ProvideBusinessMetric { @@ -198,6 +202,8 @@ impl ProvideBusinessMetric for SmithySdkFeature { FlexibleChecksumsResWhenRequired => { Some(BusinessMetric::FlexibleChecksumsResWhenRequired) } + ObservabilityTracing => Some(BusinessMetric::ObservabilityTracing), + ObservabilityMetrics => Some(BusinessMetric::ObservabilityMetrics), 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` @@ -222,6 +228,9 @@ impl ProvideBusinessMetric for AwsSdkFeature { S3Transfer => Some(BusinessMetric::S3Transfer), SsoLoginDevice => Some(BusinessMetric::SsoLoginDevice), SsoLoginAuth => Some(BusinessMetric::SsoLoginAuth), + EndpointOverride => Some(BusinessMetric::EndpointOverride), + ObservabilityOtelTracing => Some(BusinessMetric::ObservabilityOtelTracing), + ObservabilityOtelMetrics => Some(BusinessMetric::ObservabilityOtelMetrics), } } } @@ -336,7 +345,7 @@ mod tests { impl Display for BusinessMetric { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str( - &format!("{:?}", self) + &format!("{self:?}") .as_str() .from_case(Case::Pascal) .with_boundaries(&[Boundary::DigitUpper, Boundary::LowerUpper]) @@ -400,4 +409,79 @@ mod tests { let csv = "A,B"; assert_eq!("A,B", drop_unfinished_metrics_to_fit(csv, 5)); } + + #[test] + fn test_aws_sdk_feature_mappings() { + use crate::sdk_feature::AwsSdkFeature; + use crate::user_agent::metrics::ProvideBusinessMetric; + + // Test SsoLoginDevice mapping + assert_eq!( + AwsSdkFeature::SsoLoginDevice.provide_business_metric(), + Some(BusinessMetric::SsoLoginDevice) + ); + + // Test SsoLoginAuth mapping + assert_eq!( + AwsSdkFeature::SsoLoginAuth.provide_business_metric(), + Some(BusinessMetric::SsoLoginAuth) + ); + + // Test EndpointOverride mapping + assert_eq!( + AwsSdkFeature::EndpointOverride.provide_business_metric(), + Some(BusinessMetric::EndpointOverride) + ); + } + + #[test] + fn test_smithy_sdk_feature_observability_mappings() { + use crate::user_agent::metrics::ProvideBusinessMetric; + use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature; + + // Test ObservabilityMetrics mapping + assert_eq!( + SmithySdkFeature::ObservabilityMetrics.provide_business_metric(), + Some(BusinessMetric::ObservabilityMetrics) + ); + } + + #[test] + fn test_metric_id_values() { + // Test that metric IDs match the expected values from FEATURES.md specification + + // SSO Login metrics + assert_eq!( + FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::SsoLoginDevice), + Some(&"1".into()) + ); + assert_eq!( + FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::SsoLoginAuth), + Some(&"2".into()) + ); + + // Observability metrics + assert_eq!( + FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::ObservabilityTracing), + Some(&"4".into()) + ); + assert_eq!( + FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::ObservabilityMetrics), + Some(&"5".into()) + ); + assert_eq!( + FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::ObservabilityOtelTracing), + Some(&"6".into()) + ); + assert_eq!( + FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::ObservabilityOtelMetrics), + Some(&"7".into()) + ); + + // Endpoint Override metric + assert_eq!( + FEATURE_ID_TO_METRIC_VALUE.get(&BusinessMetric::EndpointOverride), + Some(&"N".into()) + ); + } } diff --git a/aws/rust-runtime/aws-runtime/src/user_agent/test_data/feature_id_to_metric_value.json b/aws/rust-runtime/aws-runtime/src/user_agent/test_data/feature_id_to_metric_value.json index 4630f3d270f..bea91c26c9e 100644 --- a/aws/rust-runtime/aws-runtime/src/user_agent/test_data/feature_id_to_metric_value.json +++ b/aws/rust-runtime/aws-runtime/src/user_agent/test_data/feature_id_to_metric_value.json @@ -54,5 +54,9 @@ "CREDENTIALS_IMDS": "0", "SSO_LOGIN_DEVICE": "1", "SSO_LOGIN_AUTH": "2", - "BEARER_SERVICE_ENV_VARS": "3" + "BEARER_SERVICE_ENV_VARS": "3", + "OBSERVABILITY_TRACING": "4", + "OBSERVABILITY_METRICS": "5", + "OBSERVABILITY_OTEL_TRACING": "6", + "OBSERVABILITY_OTEL_METRICS": "7" } diff --git a/aws/rust-runtime/aws-sigv4/Cargo.toml b/aws/rust-runtime/aws-sigv4/Cargo.toml index de172669820..2c47b135138 100644 --- a/aws/rust-runtime/aws-sigv4/Cargo.toml +++ b/aws/rust-runtime/aws-sigv4/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-sigv4" -version = "1.3.6" +version = "1.3.8" authors = ["AWS Rust SDK Team ", "David Barsky "] description = "SigV4 signer for HTTP requests and Event Stream messages." edition = "2021" diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs b/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs index b53f33fb9cf..d875afe265c 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs @@ -881,7 +881,7 @@ mod tests { let creq = CanonicalRequest::from(&req, &signing_params).unwrap(); let expected = test.canonical_request(SignatureLocation::Headers); - let actual = format!("{}", creq); + let actual = format!("{creq}"); assert_eq!(actual, expected); } @@ -894,7 +894,7 @@ mod tests { let signing_params = signing_params(&identity, SigningSettings::default()); let creq = CanonicalRequest::from(&req, &signing_params).unwrap(); let expected = test.canonical_request(SignatureLocation::Headers); - let actual = format!("{}", creq); + let actual = format!("{creq}"); assert_eq!(actual, expected); } diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/test.rs b/aws/rust-runtime/aws-sigv4/src/http_request/test.rs index 86eec201508..cacf4b351ac 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/test.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/test.rs @@ -111,7 +111,7 @@ impl SigningSuiteTest { fn test_parsed_request(path: &str) -> TestRequest { match parse_request(read(path).as_bytes()) { Ok(parsed) => parsed, - Err(err) => panic!("Failed to parse {}: {}", path, err), + Err(err) => panic!("Failed to parse {path}: {err}"), } } @@ -428,14 +428,14 @@ pub(crate) mod v4a { } fn read(path: &str) -> String { - println!("Loading `{}` for test case...", path); + println!("Loading `{path}` for test case..."); let v = { match std::fs::read_to_string(path) { // This replacement is necessary for tests to pass on Windows, as reading the // test snapshots from the file system results in CRLF line endings being inserted. Ok(value) => value.replace("\r\n", "\n"), Err(err) => { - panic!("failed to load test case `{}`: {}", path, err); + panic!("failed to load test case `{path}`: {err}"); } } }; @@ -487,7 +487,7 @@ impl> From> for TestRequest { .values() .find(|h| std::str::from_utf8(h.as_bytes()).is_err()); if let Some(invalid) = invalid { - panic!("invalid header: {:?}", invalid); + panic!("invalid header: {invalid:?}"); } Self { uri: value.uri().to_string(), diff --git a/aws/rust-runtime/aws-types/Cargo.toml b/aws/rust-runtime/aws-types/Cargo.toml index 82d530b7b21..09a7d51f035 100644 --- a/aws/rust-runtime/aws-types/Cargo.toml +++ b/aws/rust-runtime/aws-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-types" -version = "1.3.10" +version = "1.3.11" authors = ["AWS Rust SDK Team ", "Russell Cohen "] description = "Cross-service types for the AWS SDK." edition = "2021" diff --git a/aws/rust-runtime/aws-types/src/sdk_config.rs b/aws/rust-runtime/aws-types/src/sdk_config.rs index fc1f7080a53..5b9674224af 100644 --- a/aws/rust-runtime/aws-types/src/sdk_config.rs +++ b/aws/rust-runtime/aws-types/src/sdk_config.rs @@ -442,7 +442,7 @@ impl Builder { /// /// # Examples /// Disabling identity caching: - /// ```rust + /// ```rust,ignore /// # use aws_types::SdkConfig; /// use aws_smithy_runtime::client::identity::IdentityCache; /// let config = SdkConfig::builder() @@ -450,7 +450,7 @@ impl Builder { /// .build(); /// ``` /// Changing settings on the default cache implementation: - /// ```rust + /// ```rust,ignore /// # use aws_types::SdkConfig; /// use aws_smithy_runtime::client::identity::IdentityCache; /// use std::time::Duration; @@ -475,7 +475,7 @@ impl Builder { /// expires. /// /// # Examples - /// ```rust + /// ```rust,ignore /// # use aws_types::SdkConfig; /// use aws_smithy_runtime::client::identity::IdentityCache; /// diff --git a/aws/sdk/integration-tests/s3/Cargo.toml b/aws/sdk/integration-tests/s3/Cargo.toml index 8125b8d11b3..d88a02a627e 100644 --- a/aws/sdk/integration-tests/s3/Cargo.toml +++ b/aws/sdk/integration-tests/s3/Cargo.toml @@ -29,6 +29,8 @@ aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" } aws-smithy-http-client = { path = "../../build/aws-sdk/sdk/aws-smithy-http-client", features = ["default-client", "rustls-ring", "test-util", "wire-mock"] } aws-smithy-mocks = { path = "../../build/aws-sdk/sdk/aws-smithy-mocks" } aws-types = { path = "../../build/aws-sdk/sdk/aws-types" } +opentelemetry = "0.27" +opentelemetry_sdk = "0.27" bytes = "1" bytes-utils = "0.1.2" fastrand = "2.3.0" @@ -49,6 +51,7 @@ tracing-subscriber = { version = "0.3.15", features = ["env-filter", "json"] } # If you're writing a test with this, take heed! `no-env-filter` means you'll be capturing # logs from everything that speaks, so be specific with your asserts. tracing-test = { version = "0.2.4", features = ["no-env-filter"] } +serial_test = "3" [dependencies] pin-project-lite = "0.2.13" diff --git a/aws/sdk/integration-tests/s3/tests/business_metrics.rs b/aws/sdk/integration-tests/s3/tests/business_metrics.rs deleted file mode 100644 index 2fc8616d589..00000000000 --- a/aws/sdk/integration-tests/s3/tests/business_metrics.rs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use aws_config::Region; -use aws_runtime::{ - sdk_feature::AwsSdkFeature, user_agent::test_util::assert_ua_contains_metric_values, -}; -use aws_sdk_s3::{ - config::{Intercept, IntoShared}, - primitives::ByteStream, - Client, Config, -}; -use aws_smithy_http_client::test_util::capture_request; - -#[derive(Debug)] -struct TransferManagerFeatureInterceptor; - -impl Intercept for TransferManagerFeatureInterceptor { - fn name(&self) -> &'static str { - "TransferManagerFeature" - } - - fn read_before_execution( - &self, - _ctx: &aws_sdk_s3::config::interceptors::BeforeSerializationInterceptorContextRef<'_>, - cfg: &mut aws_sdk_s3::config::ConfigBag, - ) -> Result<(), aws_sdk_s3::error::BoxError> { - cfg.interceptor_state() - .store_append(AwsSdkFeature::S3Transfer); - Ok(()) - } -} - -#[tokio::test] -async fn test_track_metric_for_s3_transfer_manager() { - let (http_client, captured_request) = capture_request(None); - let mut conf_builder = Config::builder() - .region(Region::new("us-east-1")) - .http_client(http_client.clone()) - .with_test_defaults(); - // The S3 Transfer Manager uses a passed-in S3 client SDK for operations. - // By configuring an interceptor at the client level to track metrics, - // all operations executed by the client will automatically include the metric. - // This eliminates the need to apply `.config_override` on individual operations - // to insert the `TransferManagerFeatureInterceptor`. - conf_builder.push_interceptor(TransferManagerFeatureInterceptor.into_shared()); - let client = Client::from_conf(conf_builder.build()); - - let _ = client - .put_object() - .bucket("doesnotmatter") - .key("doesnotmatter") - .body(ByteStream::from_static("Hello, world".as_bytes())) - .send() - .await - .unwrap(); - - 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, &["G"]); -} diff --git a/aws/sdk/integration-tests/webassembly/Cargo.toml b/aws/sdk/integration-tests/webassembly/Cargo.toml index 658288fd404..782430b34ba 100644 --- a/aws/sdk/integration-tests/webassembly/Cargo.toml +++ b/aws/sdk/integration-tests/webassembly/Cargo.toml @@ -33,7 +33,7 @@ aws-smithy-runtime-api = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime-ap aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" } aws-smithy-wasm = { path = "../../build/aws-sdk/sdk/aws-smithy-wasm" } http = "0.2.9" -tokio = { version = "1.32.0", features = ["macros", "rt"] } +tokio = { version = "1.32.0", features = ["macros", "rt", "sync", "time"], default-features = false } # getrandom is a transitive dependency, but requires the wasm_js feature to compile for wasm # also requires a compiler flag which is set in .cargo/config.toml # https://docs.rs/getrandom/0.3.3/getrandom/#webassembly-support diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointResolverGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointResolverGenerator.kt index 3ce3bf3dd69..c32665500a0 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointResolverGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/endpoint/generators/EndpointResolverGenerator.kt @@ -387,6 +387,7 @@ internal class EndpointResolverGenerator( val url = generator.generate(endpoint.url) val headers = endpoint.headers.mapValues { entry -> entry.value.map { generator.generate(it) } } val properties = endpoint.properties.mapValues { entry -> generator.generate(entry.value) } + return writable { rustTemplate("#{SmithyEndpoint}::builder().url(#{url:W})", *codegenScope, "url" to url) headers.forEach { (name, values) -> values.forEach { rust(".header(${name.dq()}, #W)", it) } } diff --git a/codegen-server-test/integration-tests/Cargo.lock b/codegen-server-test/integration-tests/Cargo.lock index b752b100e92..7d3e7979cdf 100644 --- a/codegen-server-test/integration-tests/Cargo.lock +++ b/codegen-server-test/integration-tests/Cargo.lock @@ -120,7 +120,7 @@ dependencies = [ [[package]] name = "aws-smithy-http-server" -version = "0.65.8" +version = "0.65.9" dependencies = [ "aws-smithy-cbor", "aws-smithy-http", @@ -155,7 +155,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.1.4" +version = "0.2.0" dependencies = [ "aws-smithy-runtime-api", ] @@ -179,7 +179,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.4" +version = "1.9.5" dependencies = [ "aws-smithy-async", "aws-smithy-http", diff --git a/rust-runtime/Cargo.lock b/rust-runtime/Cargo.lock index 8dcab055eeb..2f6735ad4ce 100644 --- a/rust-runtime/Cargo.lock +++ b/rust-runtime/Cargo.lock @@ -539,7 +539,7 @@ dependencies = [ [[package]] name = "aws-smithy-mocks" -version = "0.2.0" +version = "0.2.1" dependencies = [ "aws-smithy-async", "aws-smithy-http-client", @@ -552,7 +552,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.1.4" +version = "0.2.0" dependencies = [ "aws-smithy-runtime-api", "serial_test", @@ -560,7 +560,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability-otel" -version = "0.1.2" +version = "0.1.3" dependencies = [ "async-global-executor", "async-task", @@ -600,7 +600,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.4" +version = "1.9.5" dependencies = [ "approx", "aws-smithy-async", @@ -2294,9 +2294,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minicbor" -version = "0.24.2" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f8e213c36148d828083ae01948eed271d03f95f7e72571fa242d78184029af2" +checksum = "29be4f60e41fde478b36998b88821946aafac540e53591e76db53921a0cc225b" dependencies = [ "half", "minicbor-derive", diff --git a/rust-runtime/aws-smithy-observability-otel/Cargo.toml b/rust-runtime/aws-smithy-observability-otel/Cargo.toml index 0c9a790891a..dfa6fee4504 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.2" +version = "0.1.3" authors = [ "AWS Rust SDK Team ", ] @@ -11,16 +11,16 @@ repository = "https://github.com/awslabs/smithy-rs" [dependencies] aws-smithy-observability = { path = "../aws-smithy-observability" } -opentelemetry = {version = "0.26.0", features = ["metrics"]} +opentelemetry = {version = "0.27.0", features = ["metrics"]} # The following dependencies are transitive and pinned for build # compatability purposes value-bag = "1.10.0" async-global-executor = "2.4.1" async-task = "=4.7.1" -# This crate cannot be used on powerpc -[target.'cfg(not(target_arch = "powerpc"))'.dependencies] -opentelemetry_sdk = {version = "0.26.0", features = ["metrics", "testing"]} +# This crate cannot be used on powerpc or WASM (opentelemetry_sdk depends on async-std which doesn't support WASM) +[target.'cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))'.dependencies] +opentelemetry_sdk = {version = "0.27.0", features = ["metrics", "testing"]} [dev-dependencies] tokio = { version = "1.23.1" } diff --git a/rust-runtime/aws-smithy-observability-otel/src/lib.rs b/rust-runtime/aws-smithy-observability-otel/src/lib.rs index a84975f8c27..44ee0dd2464 100644 --- a/rust-runtime/aws-smithy-observability-otel/src/lib.rs +++ b/rust-runtime/aws-smithy-observability-otel/src/lib.rs @@ -13,7 +13,8 @@ rust_2018_idioms )] // The `opentelemetry_sdk` crate uses std::sync::atomic::{AtomicI64, AtomicU64} which are not available on powerpc -#![cfg(not(target_arch = "powerpc"))] +// Additionally, opentelemetry_sdk depends on async-std which is not compatible with WASM targets +#![cfg(all(not(target_arch = "powerpc"), not(target_family = "wasm")))] //! Smithy Observability OpenTelemetry //TODO(smithyobservability): once we have finalized everything and integrated metrics with our runtime diff --git a/rust-runtime/aws-smithy-observability-otel/src/meter.rs b/rust-runtime/aws-smithy-observability-otel/src/meter.rs index e30a4e578e5..b65ce9fcf3a 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 as_any(&self) -> &dyn std::any::Any { + self + } } #[cfg(test)] diff --git a/rust-runtime/aws-smithy-observability/Cargo.toml b/rust-runtime/aws-smithy-observability/Cargo.toml index bcf5d6e8ef1..2ae6578df48 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.4" +version = "0.2.0" authors = [ "AWS Rust SDK Team ", ] diff --git a/rust-runtime/aws-smithy-observability/src/lib.rs b/rust-runtime/aws-smithy-observability/src/lib.rs index c4e53dbb4e0..767cb890af3 100644 --- a/rust-runtime/aws-smithy-observability/src/lib.rs +++ b/rust-runtime/aws-smithy-observability/src/lib.rs @@ -25,7 +25,8 @@ mod error; pub use error::{ErrorKind, GlobalTelemetryProviderError, ObservabilityError}; pub mod global; pub mod meter; -mod noop; +#[doc(hidden)] +pub mod noop; mod provider; pub use provider::{TelemetryProvider, TelemetryProviderBuilder}; pub mod instruments; diff --git a/rust-runtime/aws-smithy-observability/src/meter.rs b/rust-runtime/aws-smithy-observability/src/meter.rs index 2cca5743d46..395a1c64c9b 100644 --- a/rust-runtime/aws-smithy-observability/src/meter.rs +++ b/rust-runtime/aws-smithy-observability/src/meter.rs @@ -14,9 +14,38 @@ use crate::{attributes::Attributes, instruments::ProvideInstrument}; use std::{borrow::Cow, fmt::Debug, sync::Arc}; /// Provides named instances of [Meter]. -pub trait ProvideMeter: Send + Sync + Debug { +pub trait ProvideMeter: Send + Sync + Debug + 'static { /// Get or create a named [Meter]. fn get_meter(&self, scope: &'static str, attributes: Option<&Attributes>) -> Meter; + + /// Downcast to `Any` for type inspection. + /// + /// This method enables runtime type checking via downcasting to concrete types. + /// + /// Implementations must return `self` to enable proper downcasting: + /// + /// ```ignore + /// impl ProvideMeter for MyProvider { + /// fn as_any(&self) -> &dyn std::any::Any { + /// self + /// } + /// } + /// ``` + /// + /// # Example Usage + /// + /// ```ignore + /// use aws_smithy_observability::meter::ProvideMeter; + /// use aws_smithy_observability_otel::meter::OtelMeterProvider; + /// + /// fn check_provider_type(provider: &dyn ProvideMeter) { + /// // Downcast to check if this is an OpenTelemetry provider + /// if provider.as_any().downcast_ref::().is_some() { + /// println!("This is an OpenTelemetry provider"); + /// } + /// } + /// ``` + fn as_any(&self) -> &dyn std::any::Any; } /// The entry point to creating instruments. A grouping of related metrics. @@ -96,3 +125,80 @@ impl Meter { InstrumentBuilder::new(self, name.into()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::noop::NoopMeterProvider; + + #[test] + fn test_as_any_downcasting_works() { + // Create a noop provider + let provider = NoopMeterProvider; + + // Convert to trait object + let provider_dyn: &dyn ProvideMeter = &provider; + + // Test that downcasting works when as_any() is properly implemented + let downcast_result = provider_dyn.as_any().downcast_ref::(); + assert!( + downcast_result.is_some(), + "Downcasting should succeed when as_any() returns self" + ); + } + + /// Custom meter provider for testing extensibility. + /// + /// This demonstrates that users can create their own meter provider implementations + /// and that all required types are publicly accessible. + #[derive(Debug)] + struct CustomMeterProvider; + + impl ProvideMeter for CustomMeterProvider { + fn get_meter(&self, _scope: &'static str, _attributes: Option<&Attributes>) -> Meter { + // Create a meter using NoopMeterProvider's implementation + // This demonstrates that users can compose their own providers + let noop_provider = NoopMeterProvider; + noop_provider.get_meter(_scope, _attributes) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + #[test] + fn test_custom_provider_extensibility() { + // Create a custom provider + let custom = CustomMeterProvider; + let provider_ref: &dyn ProvideMeter = &custom; + + // Verify custom provider doesn't downcast to NoopMeterProvider + assert!( + provider_ref + .as_any() + .downcast_ref::() + .is_none(), + "Custom provider should not downcast to NoopMeterProvider" + ); + + // Verify custom provider downcasts to its own type + assert!( + provider_ref + .as_any() + .downcast_ref::() + .is_some(), + "Custom provider should downcast to CustomMeterProvider" + ); + + // Verify the provider can create meters (demonstrates all required types are accessible) + let meter = custom.get_meter("test_scope", None); + + // Verify we can create instruments from the meter + let _counter = meter.create_monotonic_counter("test_counter").build(); + let _histogram = meter.create_histogram("test_histogram").build(); + let _up_down = meter.create_up_down_counter("test_up_down").build(); + + // If we got here, all required types are publicly accessible + } +} diff --git a/rust-runtime/aws-smithy-observability/src/noop.rs b/rust-runtime/aws-smithy-observability/src/noop.rs index 6a3b9f47307..925cc2d2fac 100644 --- a/rust-runtime/aws-smithy-observability/src/noop.rs +++ b/rust-runtime/aws-smithy-observability/src/noop.rs @@ -18,12 +18,19 @@ use crate::{ meter::{Meter, ProvideMeter}, }; +/// A no-op implementation of [`ProvideMeter`] that creates no-op meters. +/// +/// This provider is useful for testing or when observability is disabled. #[derive(Debug)] -pub(crate) struct NoopMeterProvider; +pub struct NoopMeterProvider; impl ProvideMeter for NoopMeterProvider { fn get_meter(&self, _scope: &'static str, _attributes: Option<&Attributes>) -> Meter { Meter::new(Arc::new(NoopMeter)) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } #[derive(Debug)] diff --git a/rust-runtime/aws-smithy-runtime/Cargo.toml b/rust-runtime/aws-smithy-runtime/Cargo.toml index d426d8f3657..0dfd36fbae6 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.4" +version = "1.9.5" 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..2904c3f5541 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,8 @@ pub enum SmithySdkFeature { FlexibleChecksumsReqWhenRequired, FlexibleChecksumsResWhenSupported, FlexibleChecksumsResWhenRequired, + ObservabilityTracing, + ObservabilityMetrics, } impl Storable for SmithySdkFeature { diff --git a/tools/ci-build/publisher/Cargo.lock b/tools/ci-build/publisher/Cargo.lock index 3ccfc5fbb28..20194c02c03 100644 --- a/tools/ci-build/publisher/Cargo.lock +++ b/tools/ci-build/publisher/Cargo.lock @@ -973,7 +973,7 @@ dependencies = [ [[package]] name = "publisher" -version = "0.4.2" +version = "0.4.3" dependencies = [ "anyhow", "async-recursion", @@ -1314,7 +1314,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithy-rs-tool-common" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "async-trait",