Skip to content

Commit a54774e

Browse files
authored
Track endpoint override business metric (metric N) (#4419)
## Motivation and Context Implements business metric tracking for endpoint overrides per SEP User Agent 2.1 specification. The metric 'N' (EndpointOverride) must be tracked when users configure custom endpoint URLs to help AWS understand usage patterns. ## Description This PR adds a new `EndpointOverrideMetricDecorator` that tracks the endpoint override metric when endpoint URLs are configured through any supported method. ### Implementation The decorator hooks into the `SdkConfigSection.CopySdkConfigToClientConfig` section, which is where SdkConfig values are copied to the service-specific Config during client construction. It checks two sources: 1. **Global endpoint**: `SdkConfig.endpoint_url()` 2. **Service-specific endpoint**: `SdkConfig.service_config().load_config()` When either is present, the `EndpointOverrideRuntimePlugin` is added to track the metric in the User-Agent header. ## Checklist - [x] I have updated `CHANGELOG.next.toml` if I made changes to the smithy-rs codegen or runtime crates - [x] I have updated `CHANGELOG.next.toml` if I made changes to the AWS SDK, generated SDK code, or SDK runtime crates
1 parent f404ab9 commit a54774e

File tree

5 files changed

+345
-0
lines changed

5 files changed

+345
-0
lines changed

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ val DECORATORS: List<ClientCodegenDecorator> =
4646
CredentialsProviderDecorator(),
4747
RegionDecorator(),
4848
RequireEndpointRules(),
49+
EndpointOverrideMetricDecorator(),
4950
UserAgentDecorator(),
5051
SigV4AuthDecorator(),
5152
HttpRequestChecksumDecorator(),
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rustsdk
7+
8+
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
9+
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
10+
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
11+
import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginCustomization
12+
import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginSection
13+
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
14+
import software.amazon.smithy.rust.codegen.core.rustlang.writable
15+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
16+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
17+
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
18+
19+
/**
20+
* Decorator that tracks endpoint override business metric when endpoint URL is configured.
21+
*/
22+
class EndpointOverrideMetricDecorator : ClientCodegenDecorator {
23+
override val name: String = "EndpointOverrideMetric"
24+
override val order: Byte = 0
25+
26+
override fun extras(
27+
codegenContext: ClientCodegenContext,
28+
rustCrate: RustCrate,
29+
) {
30+
// Generate the interceptor in the config::endpoint module
31+
rustCrate.withModule(ClientRustModule.Config.endpoint) {
32+
val runtimeConfig = codegenContext.runtimeConfig
33+
val smithyRuntimeApi = RuntimeType.smithyRuntimeApiClient(runtimeConfig)
34+
val smithyTypes = RuntimeType.smithyTypes(runtimeConfig)
35+
val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig)
36+
val awsTypes = AwsRuntimeType.awsTypes(runtimeConfig)
37+
38+
rustTemplate(
39+
"""
40+
/// Interceptor that tracks endpoint override business metric.
41+
##[derive(Debug, Default)]
42+
pub(crate) struct EndpointOverrideFeatureTrackerInterceptor;
43+
44+
impl #{Intercept} for EndpointOverrideFeatureTrackerInterceptor {
45+
fn name(&self) -> &'static str {
46+
"EndpointOverrideFeatureTrackerInterceptor"
47+
}
48+
49+
fn read_before_execution(
50+
&self,
51+
_context: &#{BeforeSerializationInterceptorContextRef}<'_>,
52+
cfg: &mut #{ConfigBag},
53+
) -> #{Result}<(), #{BoxError}> {
54+
if cfg.load::<#{EndpointUrl}>().is_some() {
55+
cfg.interceptor_state()
56+
.store_append(#{AwsSdkFeature}::EndpointOverride);
57+
}
58+
#{Ok}(())
59+
}
60+
}
61+
""",
62+
"Intercept" to smithyRuntimeApi.resolve("client::interceptors::Intercept"),
63+
"BeforeSerializationInterceptorContextRef" to
64+
smithyRuntimeApi.resolve("client::interceptors::context::BeforeSerializationInterceptorContextRef"),
65+
"ConfigBag" to smithyTypes.resolve("config_bag::ConfigBag"),
66+
"BoxError" to smithyRuntimeApi.resolve("box_error::BoxError"),
67+
"EndpointUrl" to awsTypes.resolve("endpoint_config::EndpointUrl"),
68+
"AwsSdkFeature" to awsRuntime.resolve("sdk_feature::AwsSdkFeature"),
69+
*preludeScope,
70+
)
71+
}
72+
}
73+
74+
override fun serviceRuntimePluginCustomizations(
75+
codegenContext: ClientCodegenContext,
76+
baseCustomizations: List<ServiceRuntimePluginCustomization>,
77+
): List<ServiceRuntimePluginCustomization> =
78+
baseCustomizations + listOf(EndpointOverrideFeatureTrackerRegistration(codegenContext))
79+
}
80+
81+
private class EndpointOverrideFeatureTrackerRegistration(
82+
private val codegenContext: ClientCodegenContext,
83+
) : ServiceRuntimePluginCustomization() {
84+
override fun section(section: ServiceRuntimePluginSection) =
85+
writable {
86+
if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) {
87+
section.registerInterceptor(this) {
88+
rustTemplate("crate::config::endpoint::EndpointOverrideFeatureTrackerInterceptor")
89+
}
90+
}
91+
}
92+
}
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rustsdk
7+
8+
import org.junit.jupiter.api.Test
9+
import software.amazon.smithy.rust.codegen.core.rustlang.Feature
10+
import software.amazon.smithy.rust.codegen.core.rustlang.rust
11+
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
12+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
13+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
14+
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
15+
import software.amazon.smithy.rust.codegen.core.testutil.integrationTest
16+
import software.amazon.smithy.rust.codegen.core.testutil.tokioTest
17+
18+
class EndpointOverrideMetricDecoratorTest {
19+
companion object {
20+
private const val PREFIX = "\$version: \"2\""
21+
val model =
22+
"""
23+
$PREFIX
24+
namespace test
25+
26+
use aws.api#service
27+
use aws.auth#sigv4
28+
use aws.protocols#restJson1
29+
use smithy.rules#endpointRuleSet
30+
31+
@service(sdkId: "dontcare")
32+
@restJson1
33+
@sigv4(name: "dontcare")
34+
@auth([sigv4])
35+
@endpointRuleSet({
36+
"version": "1.0",
37+
"rules": [
38+
{
39+
"type": "endpoint",
40+
"conditions": [
41+
{ "fn": "isSet", "argv": [{ "ref": "Endpoint" }] }
42+
],
43+
"endpoint": { "url": { "ref": "Endpoint" } }
44+
},
45+
{
46+
"type": "endpoint",
47+
"conditions": [],
48+
"endpoint": { "url": "https://example.com" }
49+
}
50+
],
51+
"parameters": {
52+
"Region": { "required": false, "type": "String", "builtIn": "AWS::Region" },
53+
"Endpoint": { "required": false, "type": "String", "builtIn": "SDK::Endpoint" }
54+
}
55+
})
56+
service TestService {
57+
version: "2023-01-01",
58+
operations: [SomeOperation]
59+
}
60+
61+
@http(uri: "/SomeOperation", method: "GET")
62+
@optionalAuth
63+
operation SomeOperation {
64+
input: SomeInput,
65+
output: SomeOutput
66+
}
67+
68+
@input
69+
structure SomeInput {}
70+
71+
@output
72+
structure SomeOutput {}
73+
""".asSmithyModel()
74+
}
75+
76+
@Test
77+
fun `decorator is registered in AwsCodegenDecorator list`() {
78+
val decoratorNames = DECORATORS.map { it.name }
79+
assert(decoratorNames.contains("EndpointOverrideMetric")) {
80+
"EndpointOverrideMetricDecorator should be registered in DECORATORS list. Found: $decoratorNames"
81+
}
82+
}
83+
84+
@Test
85+
fun `endpoint override metric appears when set via SdkConfig`() {
86+
val testParams = awsIntegrationTestParams()
87+
88+
awsSdkIntegrationTest(
89+
model,
90+
testParams,
91+
environment = mapOf("RUSTUP_TOOLCHAIN" to "1.88.0"),
92+
) { context, rustCrate ->
93+
val rc = context.runtimeConfig
94+
val moduleName = context.moduleUseName()
95+
96+
// Enable test-util feature for aws-runtime
97+
rustCrate.mergeFeature(Feature("test-util", true, listOf("aws-runtime/test-util")))
98+
99+
rustCrate.integrationTest("endpoint_override_via_sdk_config") {
100+
tokioTest("metric_tracked_when_endpoint_set_via_sdk_config") {
101+
rustTemplate(
102+
"""
103+
use $moduleName::config::Region;
104+
use $moduleName::Client;
105+
use #{capture_request};
106+
use #{assert_ua_contains_metric_values};
107+
108+
let (http_client, rcvr) = capture_request(None);
109+
110+
// Create SdkConfig with endpoint URL
111+
let sdk_config = #{SdkConfig}::builder()
112+
.region(Region::new("us-east-1"))
113+
.endpoint_url("https://sdk-custom.example.com")
114+
.http_client(http_client.clone())
115+
.build();
116+
117+
// Create client from SdkConfig
118+
let client = Client::new(&sdk_config);
119+
120+
// Make a request
121+
let _ = client.some_operation().send().await;
122+
123+
// Verify the request
124+
let request = rcvr.expect_request();
125+
126+
// Verify endpoint was overridden
127+
let uri = request.uri().to_string();
128+
assert!(
129+
uri.starts_with("https://sdk-custom.example.com"),
130+
"Expected SDK custom endpoint, got: {}",
131+
uri
132+
);
133+
134+
// Verify metric 'N' is present in x-amz-user-agent header
135+
let user_agent = request
136+
.headers()
137+
.get("x-amz-user-agent")
138+
.expect("x-amz-user-agent header missing");
139+
140+
assert_ua_contains_metric_values(user_agent, &["N"]);
141+
""",
142+
*preludeScope,
143+
"capture_request" to RuntimeType.captureRequest(rc),
144+
"assert_ua_contains_metric_values" to AwsRuntimeType.awsRuntime(rc).resolve("user_agent::test_util::assert_ua_contains_metric_values"),
145+
"SdkConfig" to AwsRuntimeType.awsTypes(rc).resolve("sdk_config::SdkConfig"),
146+
)
147+
}
148+
}
149+
}
150+
}
151+
152+
@Test
153+
fun `no endpoint override metric when endpoint not set`() {
154+
val testParams = awsIntegrationTestParams()
155+
156+
awsSdkIntegrationTest(
157+
model,
158+
testParams,
159+
environment = mapOf("RUSTUP_TOOLCHAIN" to "1.88.0"),
160+
) { context, rustCrate ->
161+
val rc = context.runtimeConfig
162+
val moduleName = context.moduleUseName()
163+
164+
// Enable test-util feature for aws-runtime
165+
rustCrate.mergeFeature(Feature("test-util", true, listOf("aws-runtime/test-util")))
166+
167+
rustCrate.integrationTest("no_endpoint_override") {
168+
tokioTest("no_metric_when_endpoint_not_overridden") {
169+
rustTemplate(
170+
"""
171+
use $moduleName::config::{Credentials, Region, SharedCredentialsProvider};
172+
use $moduleName::{Config, Client};
173+
use #{capture_request};
174+
175+
let (http_client, rcvr) = capture_request(None);
176+
177+
// Create config WITHOUT endpoint override
178+
let config = Config::builder()
179+
.credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests()))
180+
.region(Region::new("us-east-1"))
181+
.http_client(http_client.clone())
182+
.build();
183+
let client = Client::from_conf(config);
184+
185+
// Make a request
186+
let _ = client.some_operation().send().await;
187+
188+
// Verify the request
189+
let request = rcvr.expect_request();
190+
191+
// Verify default endpoint was used
192+
let uri = request.uri().to_string();
193+
assert!(
194+
uri.starts_with("https://example.com"),
195+
"Expected default endpoint, got: {}",
196+
uri
197+
);
198+
199+
// Verify metric 'N' is NOT present
200+
let user_agent = request
201+
.headers()
202+
.get("x-amz-user-agent")
203+
.expect("x-amz-user-agent header should be present");
204+
205+
assert!(
206+
!user_agent.contains("m/N"),
207+
"Metric 'N' should NOT be present when endpoint not overridden"
208+
);
209+
""",
210+
*preludeScope,
211+
"capture_request" to RuntimeType.captureRequest(rc),
212+
)
213+
}
214+
215+
// Add a should_panic test to verify assert_ua_contains_metric_values panics when metric is not present
216+
rust("##[should_panic(expected = \"metric values\")]")
217+
tokioTest("assert_panics_when_metric_not_present") {
218+
rustTemplate(
219+
"""
220+
use $moduleName::config::{Credentials, Region, SharedCredentialsProvider};
221+
use $moduleName::{Config, Client};
222+
use #{capture_request};
223+
use #{assert_ua_contains_metric_values};
224+
225+
let (http_client, rcvr) = capture_request(None);
226+
227+
let config = Config::builder()
228+
.credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests()))
229+
.region(Region::new("us-east-1"))
230+
.http_client(http_client.clone())
231+
.build();
232+
let client = Client::from_conf(config);
233+
234+
let _ = client.some_operation().send().await;
235+
let request = rcvr.expect_request();
236+
let user_agent = request.headers().get("x-amz-user-agent").unwrap();
237+
238+
// This should panic because 'N' is not present
239+
assert_ua_contains_metric_values(user_agent, &["N"]);
240+
""",
241+
*preludeScope,
242+
"capture_request" to RuntimeType.captureRequest(rc),
243+
"assert_ua_contains_metric_values" to AwsRuntimeType.awsRuntime(rc).resolve("user_agent::test_util::assert_ua_contains_metric_values"),
244+
)
245+
}
246+
}
247+
}
248+
}
249+
}

aws/rust-runtime/aws-runtime/src/sdk_feature.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub enum AwsSdkFeature {
2424
SsoLoginDevice,
2525
/// Calling an SSO-OIDC operation as part of the SSO login flow, when using the OAuth2.0 authorization code grant
2626
SsoLoginAuth,
27+
/// Indicates that a custom endpoint URL was configured
28+
EndpointOverride,
2729
}
2830

2931
impl Storable for AwsSdkFeature {

aws/rust-runtime/aws-runtime/src/user_agent/metrics.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ impl ProvideBusinessMetric for AwsSdkFeature {
234234
S3Transfer => Some(BusinessMetric::S3Transfer),
235235
SsoLoginDevice => Some(BusinessMetric::SsoLoginDevice),
236236
SsoLoginAuth => Some(BusinessMetric::SsoLoginAuth),
237+
EndpointOverride => Some(BusinessMetric::EndpointOverride),
237238
}
238239
}
239240
}

0 commit comments

Comments
 (0)