From 4c85a385fd6f45fd5ca42f27b62f6179b2c49e3b Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Wed, 8 Apr 2026 13:58:10 -0700 Subject: [PATCH 1/6] Track default vs explicit http client usage Add metadata in the UA to track whether the HTTP client in use by the service client was picked up by default, or explicitly configured by the user. --- .../feature-AWSSDKforJavav2-83dd6ab.json | 6 + .../internal/AwsExecutionContextBuilder.java | 70 ++++--- .../AwsExecutionContextBuilderTest.java | 122 +++++++++++ .../builder/SdkDefaultClientBuilder.java | 28 ++- .../core/client/config/SdkClientOption.java | 12 ++ .../builder/DefaultClientBuilderTest.java | 83 +++++++- docs/user-agent.md | 3 + .../HttpClientConfigTypeTrackingTest.java | 193 ++++++++++++++++++ 8 files changed, 478 insertions(+), 39 deletions(-) create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-83dd6ab.json create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java diff --git a/.changes/next-release/feature-AWSSDKforJavav2-83dd6ab.json b/.changes/next-release/feature-AWSSDKforJavav2-83dd6ab.json new file mode 100644 index 000000000000..4945e59bcc0a --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-83dd6ab.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Add HTTP client configuration type metadata to the User-Agent header, tracking whether the HTTP client was auto-detected from the classpath or explicitly configured by the user." +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java index 32273f16019c..ac661d1a217f 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java @@ -171,6 +171,7 @@ private AwsExecutionContextBuilder() { AwsSignerExecutionAttribute.AWS_CREDENTIALS).orElse(null))); putStreamingInputOutputTypesMetadata(executionAttributes, executionParams); + putHttpClientConfigTypeMetadata(executionAttributes, clientConfig); return ExecutionContext.builder() .interceptorChain(executionInterceptorChain) @@ -183,53 +184,54 @@ private AwsExecutionContextBuilder() { private static void putStreamingInputOutputTypesMetadata( ExecutionAttributes executionAttributes, ClientExecutionParams executionParams) { - List userAgentMetadata = new ArrayList<>(); if (executionParams.getRequestBody() != null) { - userAgentMetadata.add( - AdditionalMetadata - .builder() - .name("rb") - .value(ContentStreamProvider.ProviderType.shortValueFromName( - executionParams.getRequestBody().contentStreamProvider().name()) - ) - .build()); + addUserAgentMetadata(executionAttributes, "rb", + ContentStreamProvider.ProviderType.shortValueFromName( + executionParams.getRequestBody().contentStreamProvider().name())); } if (executionParams.getAsyncRequestBody() != null) { - userAgentMetadata.add( - AdditionalMetadata - .builder() - .name("rb") - .value(AsyncRequestBody.BodyType.shortValueFromName( - executionParams.getAsyncRequestBody().body()) - ) - .build()); + addUserAgentMetadata(executionAttributes, "rb", + AsyncRequestBody.BodyType.shortValueFromName( + executionParams.getAsyncRequestBody().body())); } if (executionParams.getResponseTransformer() != null) { - userAgentMetadata.add( - AdditionalMetadata - .builder() - .name("rt") - .value(ResponseTransformer.TransformerType.shortValueFromName( - executionParams.getResponseTransformer().name()) - ) - .build()); + addUserAgentMetadata(executionAttributes, "rt", + ResponseTransformer.TransformerType.shortValueFromName( + executionParams.getResponseTransformer().name())); } if (executionParams.getAsyncResponseTransformer() != null) { - userAgentMetadata.add( - AdditionalMetadata - .builder() - .name("rt") - .value(AsyncResponseTransformer.TransformerType.shortValueFromName( - executionParams.getAsyncResponseTransformer().name()) - ) - .build()); + addUserAgentMetadata(executionAttributes, "rt", + AsyncResponseTransformer.TransformerType.shortValueFromName( + executionParams.getAsyncResponseTransformer().name())); } + } - executionAttributes.putAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA, userAgentMetadata); + private static void putHttpClientConfigTypeMetadata(ExecutionAttributes executionAttributes, + SdkClientConfiguration clientConfig) { + String httpClientConfigType = clientConfig.option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE); + if (httpClientConfigType == null) { + return; + } + addUserAgentMetadata(executionAttributes, "hc", httpClientConfigType); + } + + private static void addUserAgentMetadata(ExecutionAttributes executionAttributes, String name, String value) { + List metadata = executionAttributes.getAttribute( + SdkInternalExecutionAttribute.USER_AGENT_METADATA); + if (metadata == null) { + metadata = new ArrayList<>(); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA, metadata); + } + metadata.add( + AdditionalMetadata + .builder() + .name(name) + .value(value) + .build()); } /** diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java index e6ab211de5da..27098f5cdec7 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java @@ -510,6 +510,128 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransfo ); } + @Test + public void invokeInterceptorsAndCreateExecutionContext_withDefaultHttpClient_addsHcMetadata() { + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Collections.singletonList(AdditionalMetadata.builder().name("hc").value("d").build()) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClient_addsHcMetadata() { + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Collections.singletonList(AdditionalMetadata.builder().name("hc").value("e").build()) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withRequestBodyAndHcMetadata_addsBoth() throws IOException { + ClientExecutionParams executionParams = clientExecutionParams(); + File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); + testFile.deleteOnExit(); + executionParams.withRequestBody(RequestBody.fromFile(testFile)); + + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Arrays.asList( + AdditionalMetadata.builder().name("rb").value("f").build(), + AdditionalMetadata.builder().name("hc").value("d").build() + ) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withAsyncRequestBodyAndHcMetadata_addsBoth() throws IOException { + ClientExecutionParams executionParams = clientExecutionParams(); + File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); + testFile.deleteOnExit(); + executionParams.withAsyncRequestBody(AsyncRequestBody.fromFile(testFile)); + + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Arrays.asList( + AdditionalMetadata.builder().name("rb").value("f").build(), + AdditionalMetadata.builder().name("hc").value("e").build() + ) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withResponseTransformerAndHcMetadata_addsBoth() throws IOException { + ClientExecutionParams executionParams = clientExecutionParams(); + File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); + testFile.deleteOnExit(); + executionParams.withResponseTransformer(ResponseTransformer.toFile(testFile)); + + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Arrays.asList( + AdditionalMetadata.builder().name("rt").value("f").build(), + AdditionalMetadata.builder().name("hc").value("d").build() + ) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransformerAndHcMetadata_addsBoth() throws IOException { + ClientExecutionParams executionParams = clientExecutionParams(); + File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); + testFile.deleteOnExit(); + executionParams.withAsyncResponseTransformer(AsyncResponseTransformer.toFile(testFile)); + + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Arrays.asList( + AdditionalMetadata.builder().name("rt").value("f").build(), + AdditionalMetadata.builder().name("hc").value("e").build() + ) + ); + } + private ClientExecutionParams clientExecutionParams() { return clientExecutionParams(sdkRequest); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index 076f79b44c89..e5c0e7ee509d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -39,6 +39,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.DEFAULT_RETRY_MODE; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS; import static software.amazon.awssdk.core.client.config.SdkClientOption.HTTP_CLIENT_CONFIG; +import static software.amazon.awssdk.core.client.config.SdkClientOption.HTTP_CLIENT_CONFIG_TYPE; import static software.amazon.awssdk.core.client.config.SdkClientOption.IDENTITY_PROVIDERS; import static software.amazon.awssdk.core.client.config.SdkClientOption.INTERNAL_USER_AGENT; import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_PUBLISHERS; @@ -310,34 +311,55 @@ protected SdkClientConfiguration finalizeChildConfiguration(SdkClientConfigurati */ private SdkClientConfiguration finalizeSyncConfiguration(SdkClientConfiguration config) { return config.toBuilder() + .option(HTTP_CLIENT_CONFIG_TYPE, resolveSyncHttpClientConfigType(config)) .lazyOption(SdkClientOption.SYNC_HTTP_CLIENT, c -> resolveSyncHttpClient(c, config)) .option(SdkClientOption.CLIENT_TYPE, SYNC) .build(); } + private String resolveSyncHttpClientConfigType(SdkClientConfiguration config) { + SdkHttpClient httpClient = config.option(CONFIGURED_SYNC_HTTP_CLIENT); + SdkHttpClient.Builder httpClientBuilder = config.option(CONFIGURED_SYNC_HTTP_CLIENT_BUILDER); + if (!(httpClient == null && httpClientBuilder == null)) { + return "e"; + } + return "d"; + } + /** * Finalize async-specific configuration from the default-applied configuration. */ private SdkClientConfiguration finalizeAsyncConfiguration(SdkClientConfiguration config) { return config.toBuilder() .lazyOptionIfAbsent(FUTURE_COMPLETION_EXECUTOR, this::resolveAsyncFutureCompletionExecutor) + .option(HTTP_CLIENT_CONFIG_TYPE, resolveAsyncHttpClientConfigType(config)) .lazyOption(ASYNC_HTTP_CLIENT, c -> resolveAsyncHttpClient(c, config)) .option(SdkClientOption.CLIENT_TYPE, ASYNC) .build(); } + private String resolveAsyncHttpClientConfigType(SdkClientConfiguration config) { + SdkAsyncHttpClient httpClient = config.option(CONFIGURED_ASYNC_HTTP_CLIENT); + SdkAsyncHttpClient.Builder httpClientBuilder = config.option(CONFIGURED_ASYNC_HTTP_CLIENT_BUILDER); + if (!(httpClient == null && httpClientBuilder == null)) { + return "e"; + } + return "d"; + } + /** * Finalize global configuration from the default-applied configuration. */ private SdkClientConfiguration finalizeConfiguration(SdkClientConfiguration config) { - return config.toBuilder() + SdkClientConfiguration.Builder builder = config.toBuilder() .lazyOption(SCHEDULED_EXECUTOR_SERVICE, this::resolveScheduledExecutorService) .lazyOptionIfAbsent(RETRY_STRATEGY, this::resolveRetryStrategy) .option(EXECUTION_INTERCEPTORS, resolveExecutionInterceptors(config)) .lazyOption(CLIENT_USER_AGENT, this::resolveClientUserAgent) .lazyOption(COMPRESSION_CONFIGURATION, this::resolveCompressionConfiguration) - .lazyOptionIfAbsent(IDENTITY_PROVIDERS, c -> IdentityProviders.builder().build()) - .build(); + .lazyOptionIfAbsent(IDENTITY_PROVIDERS, c -> IdentityProviders.builder().build()); + builder.computeOptionIfAbsent(HTTP_CLIENT_CONFIG_TYPE, () -> "d"); + return builder.build(); } private CompressionConfiguration resolveCompressionConfiguration(LazyValueSource config) { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index e05753513ad4..70997cf998f4 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -196,6 +196,18 @@ public final class SdkClientOption extends ClientOption { public static final SdkClientOption> CONFIGURED_SYNC_HTTP_CLIENT_BUILDER = new SdkClientOption<>(new UnsafeValueType(SdkAsyncHttpClient.Builder.class)); + /** + * The HTTP client configuration type indicating how the HTTP client was selected. + *

+ * Possible values: + *

    + *
  • {@code "d"} - Default: HTTP client was auto-detected from the classpath
  • + *
  • {@code "e"} - Explicit: HTTP client was explicitly configured by the user via + * {@code httpClient()} or {@code httpClientBuilder()} methods
  • + *
+ */ + public static final SdkClientOption HTTP_CLIENT_CONFIG_TYPE = new SdkClientOption<>(String.class); + /** * Configuration that should be used to build the {@link #SYNC_HTTP_CLIENT} or {@link #ASYNC_HTTP_CLIENT}. */ diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java index 973fc5a92ce5..118f26e16174 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java @@ -32,6 +32,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.API_CALL_TIMEOUT; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_ATTRIBUTES; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS; +import static software.amazon.awssdk.core.client.config.SdkClientOption.HTTP_CLIENT_CONFIG_TYPE; import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_PUBLISHERS; import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE; import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE_SUPPLIER; @@ -87,8 +88,6 @@ import software.amazon.awssdk.metrics.MetricCollection; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.profiles.ProfileFile; -import software.amazon.awssdk.testutils.EnvironmentVariableHelper; -import software.amazon.awssdk.testutils.Waiter; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.StringInputStream; @@ -389,6 +388,86 @@ public void explicitAsyncHttpClientProvided_ClientIsNotManagedBySdk() { verify(defaultAsyncHttpClientFactory, never()).buildWithDefaults(any()); } + @Test + public void noHttpClientProvided_httpClientConfigTypeIsDefault() { + TestClient client = testClientBuilder().build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void noAsyncHttpClientProvided_httpClientConfigTypeIsDefault() { + TestAsyncClient client = testAsyncClientBuilder().build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void explicitSyncHttpClientProvided_httpClientConfigTypeIsExplicit() { + TestClient client = testClientBuilder() + .httpClient(mock(SdkHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + } + + @Test + public void explicitSyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicit() { + TestClient client = testClientBuilder() + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> mock(SdkHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + } + + @Test + public void explicitAsyncHttpClientProvided_httpClientConfigTypeIsExplicit() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClient(mock(SdkAsyncHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + } + + @Test + public void explicitAsyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicit() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClientBuilder((SdkAsyncHttpClient.Builder) serviceDefaults -> mock(SdkAsyncHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + } + + @Test + public void syncHttpClientSetThenCleared_httpClientConfigTypeIsDefault() { + TestClient client = testClientBuilder() + .httpClient(mock(SdkHttpClient.class)) + .httpClient((SdkHttpClient) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void syncHttpClientBuilderSetThenCleared_httpClientConfigTypeIsDefault() { + TestClient client = testClientBuilder() + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> mock(SdkHttpClient.class)) + .httpClientBuilder((SdkHttpClient.Builder) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void asyncHttpClientSetThenCleared_httpClientConfigTypeIsDefault() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClient(mock(SdkAsyncHttpClient.class)) + .httpClient((SdkAsyncHttpClient) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void asyncHttpClientBuilderSetThenCleared_httpClientConfigTypeIsDefault() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClientBuilder((SdkAsyncHttpClient.Builder) serviceDefaults -> mock(SdkAsyncHttpClient.class)) + .httpClientBuilder((SdkAsyncHttpClient.Builder) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + @Test public void clientBuilderFieldsHaveBeanEquivalents() throws Exception { // Mutating properties might not have bean equivalents. This is probably fine, since very few customers require diff --git a/docs/user-agent.md b/docs/user-agent.md index bb1dfde3a401..5a3b336c76c8 100644 --- a/docs/user-agent.md +++ b/docs/user-agent.md @@ -19,3 +19,6 @@ The table below documents additional metadata the SDK may include in the `User-A ||`s`|"Stream". The response transformer adapts the response body to an `InputStream`.| ||`p`|"Publisher". The response transformer adapts the response body to an `SdkPublisher`.| ||`u`|"unknown"| +|`hc`||The HTTP client configuration type. Indicates whether the HTTP client was selected by default or explicitly configured by the user.| +||`d`|"Default". The HTTP client was auto-detected from the classpath.| +||`e`|"Explicit". The HTTP client was explicitly configured by the user via `httpClient()` or `httpClientBuilder()`.| diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java new file mode 100644 index 000000000000..3a7d6a052a59 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java @@ -0,0 +1,193 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.services.useragent; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.matching.RequestPatternBuilder.allRequests; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.verification.LoggedRequest; +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjsonwithconfig.ProtocolRestJsonWithConfigAsyncClient; +import software.amazon.awssdk.services.protocolrestjsonwithconfig.ProtocolRestJsonWithConfigClient; +import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Tests that the HTTP client configuration type metadata (md/hc#d or md/hc#e) is correctly + * included in the User-Agent header based on whether the HTTP client was auto-detected or + * explicitly configured. + */ +public class HttpClientConfigTypeTrackingTest { + + private static final StaticCredentialsProvider CREDENTIALS = + StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid")); + + private WireMockServer wireMock; + private MockSyncHttpClient mockSyncHttpClient; + private MockAsyncHttpClient mockAsyncHttpClient; + + @BeforeEach + public void setup() { + wireMock = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); + wireMock.start(); + wireMock.stubFor(post(anyUrl()).willReturn(aResponse().withStatus(200).withBody("{}"))); + + mockSyncHttpClient = new MockSyncHttpClient(); + mockSyncHttpClient.stubNextResponse(mockResponse()); + + mockAsyncHttpClient = new MockAsyncHttpClient(); + mockAsyncHttpClient.stubNextResponse(mockResponse()); + } + + @AfterEach + public void teardown() { + wireMock.stop(); + } + + // --- Default HTTP client tests (no httpClient() call, auto-detected from classpath) --- + + @Test + public void syncClient_defaultHttpClient_containsHcDefault() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .build(); + client.allTypes(r -> {}); + assertThat(lastWireMockUserAgent()).contains("md/hc#d"); + } + + @Test + public void asyncClient_defaultHttpClient_containsHcDefault() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .build(); + client.allTypes(r -> {}).join(); + assertThat(lastWireMockUserAgent()).contains("md/hc#d"); + } + + // --- Explicit HTTP client tests (mock HTTP clients) --- + + @Test + public void syncClient_explicitHttpClient_containsHcExplicit() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockSyncHttpClient) + .build(); + client.allTypes(r -> {}); + assertThat(syncUserAgent()).contains("md/hc#e"); + } + + @Test + public void asyncClient_explicitHttpClient_containsHcExplicit() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockAsyncHttpClient) + .build(); + client.allTypes(r -> {}).join(); + assertThat(asyncUserAgent()).contains("md/hc#e"); + } + + // --- Persistence tests --- + + @Test + public void syncClient_persistsAcrossRequests() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockSyncHttpClient) + .build(); + client.allTypes(r -> {}); + String firstUserAgent = syncUserAgent(); + mockSyncHttpClient.stubNextResponse(mockResponse()); + client.allTypes(r -> {}); + String secondUserAgent = syncUserAgent(); + assertThat(firstUserAgent).contains("md/hc#e"); + assertThat(secondUserAgent).contains("md/hc#e"); + } + + @Test + public void asyncClient_persistsAcrossRequests() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockAsyncHttpClient) + .build(); + client.allTypes(r -> {}).join(); + String firstUserAgent = asyncUserAgent(); + mockAsyncHttpClient.stubNextResponse(mockResponse()); + client.allTypes(r -> {}).join(); + String secondUserAgent = asyncUserAgent(); + assertThat(firstUserAgent).contains("md/hc#e"); + assertThat(secondUserAgent).contains("md/hc#e"); + } + + // --- Helpers --- + + private String lastWireMockUserAgent() { + List requests = wireMock.findAll(allRequests()); + assertThat(requests).isNotEmpty(); + return requests.get(requests.size() - 1).getHeader("User-Agent"); + } + + private String syncUserAgent() { + SdkHttpRequest lastRequest = mockSyncHttpClient.getLastRequest(); + List headers = lastRequest.headers().get("User-Agent"); + assertThat(headers).isNotNull().hasSize(1); + return headers.get(0); + } + + private String asyncUserAgent() { + SdkHttpRequest lastRequest = mockAsyncHttpClient.getLastRequest(); + List headers = lastRequest.headers().get("User-Agent"); + assertThat(headers).isNotNull().hasSize(1); + return headers.get(0); + } + + private static HttpExecuteResponse mockResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream("{}"))) + .build(); + } +} From 2c79154fe56a125315f7223a423bcfe1f4a55396 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Fri, 10 Apr 2026 14:55:33 -0700 Subject: [PATCH 2/6] Switch to discrete feature IDs --- .../internal/AwsExecutionContextBuilder.java | 8 +- .../AwsExecutionContextBuilderTest.java | 58 ++--- .../builder/SdkDefaultClientBuilder.java | 29 +-- .../core/client/config/SdkClientOption.java | 12 +- .../useragent/BusinessMetricFeatureId.java | 3 + .../builder/DefaultClientBuilderTest.java | 51 +++-- .../HttpClientConfigTypeTrackingTest.java | 213 ++++++++++++++++-- 7 files changed, 288 insertions(+), 86 deletions(-) diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java index ac661d1a217f..1fa59e119cef 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java @@ -212,11 +212,15 @@ private static void put private static void putHttpClientConfigTypeMetadata(ExecutionAttributes executionAttributes, SdkClientConfiguration clientConfig) { - String httpClientConfigType = clientConfig.option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE); + BusinessMetricFeatureId httpClientConfigType = clientConfig.option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE); if (httpClientConfigType == null) { return; } - addUserAgentMetadata(executionAttributes, "hc", httpClientConfigType); + BusinessMetricCollection businessMetrics = executionAttributes.getAttribute( + SdkInternalExecutionAttribute.BUSINESS_METRICS); + if (businessMetrics != null) { + businessMetrics.addMetric(httpClientConfigType.value()); + } } private static void addUserAgentMetadata(ExecutionAttributes executionAttributes, String name, String value) { diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java index 27098f5cdec7..0552d7df31db 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java @@ -66,6 +66,8 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.core.sync.ResponseTransformer; import software.amazon.awssdk.core.useragent.AdditionalMetadata; +import software.amazon.awssdk.core.useragent.BusinessMetricCollection; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.http.auth.aws.scheme.AwsV4AuthScheme; import software.amazon.awssdk.http.auth.scheme.NoAuthAuthScheme; import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme; @@ -513,31 +515,31 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransfo @Test public void invokeInterceptorsAndCreateExecutionContext_withDefaultHttpClient_addsHcMetadata() { SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_AUTO) .build(); ExecutionContext executionContext = AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); ExecutionAttributes executionAttributes = executionContext.executionAttributes(); - assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Collections.singletonList(AdditionalMetadata.builder().name("hc").value("d").build()) - ); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + assertThat(businessMetrics.recordedMetrics()).contains("AJ"); } @Test public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClient_addsHcMetadata() { SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE) .build(); ExecutionContext executionContext = AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); ExecutionAttributes executionAttributes = executionContext.executionAttributes(); - assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Collections.singletonList(AdditionalMetadata.builder().name("hc").value("e").build()) - ); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + assertThat(businessMetrics.recordedMetrics()).contains("AK"); } @Test @@ -548,7 +550,7 @@ public void invokeInterceptorsAndCreateExecutionContext_withRequestBodyAndHcMeta executionParams.withRequestBody(RequestBody.fromFile(testFile)); SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_AUTO) .build(); ExecutionContext executionContext = @@ -556,11 +558,11 @@ public void invokeInterceptorsAndCreateExecutionContext_withRequestBodyAndHcMeta ExecutionAttributes executionAttributes = executionContext.executionAttributes(); assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Arrays.asList( - AdditionalMetadata.builder().name("rb").value("f").build(), - AdditionalMetadata.builder().name("hc").value("d").build() - ) + Collections.singletonList(AdditionalMetadata.builder().name("rb").value("f").build()) ); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + assertThat(businessMetrics.recordedMetrics()).contains("AJ"); } @Test @@ -571,7 +573,7 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncRequestBodyAndH executionParams.withAsyncRequestBody(AsyncRequestBody.fromFile(testFile)); SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE) .build(); ExecutionContext executionContext = @@ -579,11 +581,11 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncRequestBodyAndH ExecutionAttributes executionAttributes = executionContext.executionAttributes(); assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Arrays.asList( - AdditionalMetadata.builder().name("rb").value("f").build(), - AdditionalMetadata.builder().name("hc").value("e").build() - ) + Collections.singletonList(AdditionalMetadata.builder().name("rb").value("f").build()) ); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + assertThat(businessMetrics.recordedMetrics()).contains("AK"); } @Test @@ -594,7 +596,7 @@ public void invokeInterceptorsAndCreateExecutionContext_withResponseTransformerA executionParams.withResponseTransformer(ResponseTransformer.toFile(testFile)); SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_AUTO) .build(); ExecutionContext executionContext = @@ -602,11 +604,11 @@ public void invokeInterceptorsAndCreateExecutionContext_withResponseTransformerA ExecutionAttributes executionAttributes = executionContext.executionAttributes(); assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Arrays.asList( - AdditionalMetadata.builder().name("rt").value("f").build(), - AdditionalMetadata.builder().name("hc").value("d").build() - ) + Collections.singletonList(AdditionalMetadata.builder().name("rt").value("f").build()) ); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + assertThat(businessMetrics.recordedMetrics()).contains("AJ"); } @Test @@ -617,7 +619,7 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransfo executionParams.withAsyncResponseTransformer(AsyncResponseTransformer.toFile(testFile)); SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE) .build(); ExecutionContext executionContext = @@ -625,11 +627,11 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransfo ExecutionAttributes executionAttributes = executionContext.executionAttributes(); assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Arrays.asList( - AdditionalMetadata.builder().name("rt").value("f").build(), - AdditionalMetadata.builder().name("hc").value("e").build() - ) + Collections.singletonList(AdditionalMetadata.builder().name("rt").value("f").build()) ); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + assertThat(businessMetrics.recordedMetrics()).contains("AK"); } private ClientExecutionParams clientExecutionParams() { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index e5c0e7ee509d..34f165dd1767 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -97,6 +97,7 @@ import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder; import software.amazon.awssdk.core.internal.useragent.UserAgentConstant; import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.core.util.SystemUserAgent; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; @@ -317,13 +318,14 @@ private SdkClientConfiguration finalizeSyncConfiguration(SdkClientConfiguration .build(); } - private String resolveSyncHttpClientConfigType(SdkClientConfiguration config) { - SdkHttpClient httpClient = config.option(CONFIGURED_SYNC_HTTP_CLIENT); - SdkHttpClient.Builder httpClientBuilder = config.option(CONFIGURED_SYNC_HTTP_CLIENT_BUILDER); - if (!(httpClient == null && httpClientBuilder == null)) { - return "e"; + private BusinessMetricFeatureId resolveSyncHttpClientConfigType(SdkClientConfiguration config) { + if (config.option(CONFIGURED_SYNC_HTTP_CLIENT) != null) { + return BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE; } - return "d"; + if (config.option(CONFIGURED_SYNC_HTTP_CLIENT_BUILDER) != null) { + return BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY; + } + return BusinessMetricFeatureId.HTTP_CLIENT_AUTO; } /** @@ -338,13 +340,14 @@ private SdkClientConfiguration finalizeAsyncConfiguration(SdkClientConfiguration .build(); } - private String resolveAsyncHttpClientConfigType(SdkClientConfiguration config) { - SdkAsyncHttpClient httpClient = config.option(CONFIGURED_ASYNC_HTTP_CLIENT); - SdkAsyncHttpClient.Builder httpClientBuilder = config.option(CONFIGURED_ASYNC_HTTP_CLIENT_BUILDER); - if (!(httpClient == null && httpClientBuilder == null)) { - return "e"; + private BusinessMetricFeatureId resolveAsyncHttpClientConfigType(SdkClientConfiguration config) { + if (config.option(CONFIGURED_ASYNC_HTTP_CLIENT) != null) { + return BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE; + } + if (config.option(CONFIGURED_ASYNC_HTTP_CLIENT_BUILDER) != null) { + return BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY; } - return "d"; + return BusinessMetricFeatureId.HTTP_CLIENT_AUTO; } /** @@ -358,7 +361,7 @@ private SdkClientConfiguration finalizeConfiguration(SdkClientConfiguration conf .lazyOption(CLIENT_USER_AGENT, this::resolveClientUserAgent) .lazyOption(COMPRESSION_CONFIGURATION, this::resolveCompressionConfiguration) .lazyOptionIfAbsent(IDENTITY_PROVIDERS, c -> IdentityProviders.builder().build()); - builder.computeOptionIfAbsent(HTTP_CLIENT_CONFIG_TYPE, () -> "d"); + builder.computeOptionIfAbsent(HTTP_CLIENT_CONFIG_TYPE, () -> BusinessMetricFeatureId.HTTP_CLIENT_AUTO); return builder.build(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 70997cf998f4..53dfcc52d6bb 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -36,6 +36,7 @@ import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.useragent.BusinessMetricCollection; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; @@ -201,12 +202,15 @@ public final class SdkClientOption extends ClientOption { *

* Possible values: *

    - *
  • {@code "d"} - Default: HTTP client was auto-detected from the classpath
  • - *
  • {@code "e"} - Explicit: HTTP client was explicitly configured by the user via - * {@code httpClient()} or {@code httpClientBuilder()} methods
  • + *
  • {@link BusinessMetricFeatureId#HTTP_CLIENT_AUTO} - HTTP client was auto-detected from the classpath
  • + *
  • {@link BusinessMetricFeatureId#HTTP_CLIENT_EXPLICIT_INSTANCE} - HTTP client was explicitly provided + * via {@code httpClient()}
  • + *
  • {@link BusinessMetricFeatureId#HTTP_CLIENT_EXPLICIT_FACTORY} - HTTP client factory was explicitly provided + * via {@code httpClientBuilder()}
  • *
*/ - public static final SdkClientOption HTTP_CLIENT_CONFIG_TYPE = new SdkClientOption<>(String.class); + public static final SdkClientOption HTTP_CLIENT_CONFIG_TYPE = + new SdkClientOption<>(BusinessMetricFeatureId.class); /** * Configuration that should be used to build the {@link #SYNC_HTTP_CLIENT} or {@link #ASYNC_HTTP_CLIENT}. diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java index b8952e541998..6748d96603af 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java @@ -57,6 +57,9 @@ public enum BusinessMetricFeatureId { FLEXIBLE_CHECKSUMS_REQ_XXHASH3("AG"), FLEXIBLE_CHECKSUMS_REQ_XXHASH64("AH"), FLEXIBLE_CHECKSUMS_REQ_XXHASH128("AI"), + HTTP_CLIENT_AUTO("AJ"), + HTTP_CLIENT_EXPLICIT_INSTANCE("AK"), + HTTP_CLIENT_EXPLICIT_FACTORY("AL"), DDB_MAPPER("d"), BEARER_SERVICE_ENV_VARS("3"), CREDENTIALS_CODE("e"), diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java index 118f26e16174..cfefe8254cec 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java @@ -82,6 +82,7 @@ import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.signer.NoOpSigner; import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; @@ -389,83 +390,93 @@ public void explicitAsyncHttpClientProvided_ClientIsNotManagedBySdk() { } @Test - public void noHttpClientProvided_httpClientConfigTypeIsDefault() { + public void noHttpClientProvided_httpClientConfigTypeIsAuto() { TestClient client = testClientBuilder().build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); } @Test - public void noAsyncHttpClientProvided_httpClientConfigTypeIsDefault() { + public void noAsyncHttpClientProvided_httpClientConfigTypeIsAuto() { TestAsyncClient client = testAsyncClientBuilder().build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); } @Test - public void explicitSyncHttpClientProvided_httpClientConfigTypeIsExplicit() { + public void explicitSyncHttpClientProvided_httpClientConfigTypeIsExplicitInstance() { TestClient client = testClientBuilder() .httpClient(mock(SdkHttpClient.class)) .build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE); } @Test - public void explicitSyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicit() { + public void explicitSyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicitFactory() { TestClient client = testClientBuilder() .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> mock(SdkHttpClient.class)) .build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY); } @Test - public void explicitAsyncHttpClientProvided_httpClientConfigTypeIsExplicit() { + public void explicitAsyncHttpClientProvided_httpClientConfigTypeIsExplicitInstance() { TestAsyncClient client = testAsyncClientBuilder() .httpClient(mock(SdkAsyncHttpClient.class)) .build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE); } @Test - public void explicitAsyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicit() { + public void explicitAsyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicitFactory() { TestAsyncClient client = testAsyncClientBuilder() .httpClientBuilder((SdkAsyncHttpClient.Builder) serviceDefaults -> mock(SdkAsyncHttpClient.class)) .build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY); } @Test - public void syncHttpClientSetThenCleared_httpClientConfigTypeIsDefault() { + public void syncHttpClientSetThenCleared_httpClientConfigTypeIsAuto() { TestClient client = testClientBuilder() .httpClient(mock(SdkHttpClient.class)) .httpClient((SdkHttpClient) null) .build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); } @Test - public void syncHttpClientBuilderSetThenCleared_httpClientConfigTypeIsDefault() { + public void syncHttpClientBuilderSetThenCleared_httpClientConfigTypeIsAuto() { TestClient client = testClientBuilder() .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> mock(SdkHttpClient.class)) .httpClientBuilder((SdkHttpClient.Builder) null) .build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); } @Test - public void asyncHttpClientSetThenCleared_httpClientConfigTypeIsDefault() { + public void asyncHttpClientSetThenCleared_httpClientConfigTypeIsAuto() { TestAsyncClient client = testAsyncClientBuilder() .httpClient(mock(SdkAsyncHttpClient.class)) .httpClient((SdkAsyncHttpClient) null) .build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); } @Test - public void asyncHttpClientBuilderSetThenCleared_httpClientConfigTypeIsDefault() { + public void asyncHttpClientBuilderSetThenCleared_httpClientConfigTypeIsAuto() { TestAsyncClient client = testAsyncClientBuilder() .httpClientBuilder((SdkAsyncHttpClient.Builder) serviceDefaults -> mock(SdkAsyncHttpClient.class)) .httpClientBuilder((SdkAsyncHttpClient.Builder) null) .build(); - assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); } @Test diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java index 3a7d6a052a59..8d59402f7ee3 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java @@ -20,6 +20,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.matching.RequestPatternBuilder.allRequests; import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.core.useragent.BusinessMetricCollection.METRIC_SEARCH_PATTERN; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; @@ -33,8 +34,11 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.http.AbortableInputStream; import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.protocolrestjsonwithconfig.ProtocolRestJsonWithConfigAsyncClient; import software.amazon.awssdk.services.protocolrestjsonwithconfig.ProtocolRestJsonWithConfigClient; @@ -43,9 +47,9 @@ import software.amazon.awssdk.utils.StringInputStream; /** - * Tests that the HTTP client configuration type metadata (md/hc#d or md/hc#e) is correctly - * included in the User-Agent header based on whether the HTTP client was auto-detected or - * explicitly configured. + * Tests that the HTTP client selection business metric (AJ, AK, or AL) is correctly + * included in the m/ section of the User-Agent header based on how the HTTP client was configured: + * auto-detected (AJ), explicit instance via httpClient() (AK), or explicit factory via httpClientBuilder() (AL). */ public class HttpClientConfigTypeTrackingTest { @@ -74,10 +78,10 @@ public void teardown() { wireMock.stop(); } - // --- Default HTTP client tests (no httpClient() call, auto-detected from classpath) --- + // --- Auto-selected HTTP client tests (no httpClient/httpClientBuilder call) --- @Test - public void syncClient_defaultHttpClient_containsHcDefault() { + public void syncClient_defaultHttpClient_containsAutoMetric() { ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient .builder() .region(Region.US_WEST_2) @@ -85,11 +89,12 @@ public void syncClient_defaultHttpClient_containsHcDefault() { .endpointOverride(URI.create("http://localhost:" + wireMock.port())) .build(); client.allTypes(r -> {}); - assertThat(lastWireMockUserAgent()).contains("md/hc#d"); + assertThat(lastWireMockUserAgent()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); } @Test - public void asyncClient_defaultHttpClient_containsHcDefault() { + public void asyncClient_defaultHttpClient_containsAutoMetric() { ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient .builder() .region(Region.US_WEST_2) @@ -97,13 +102,14 @@ public void asyncClient_defaultHttpClient_containsHcDefault() { .endpointOverride(URI.create("http://localhost:" + wireMock.port())) .build(); client.allTypes(r -> {}).join(); - assertThat(lastWireMockUserAgent()).contains("md/hc#d"); + assertThat(lastWireMockUserAgent()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); } - // --- Explicit HTTP client tests (mock HTTP clients) --- + // --- Explicit HTTP client instance tests (httpClient() call) --- @Test - public void syncClient_explicitHttpClient_containsHcExplicit() { + public void syncClient_explicitHttpClient_containsExplicitInstanceMetric() { ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient .builder() .region(Region.US_WEST_2) @@ -111,11 +117,12 @@ public void syncClient_explicitHttpClient_containsHcExplicit() { .httpClient(mockSyncHttpClient) .build(); client.allTypes(r -> {}); - assertThat(syncUserAgent()).contains("md/hc#e"); + assertThat(syncUserAgent()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); } @Test - public void asyncClient_explicitHttpClient_containsHcExplicit() { + public void asyncClient_explicitHttpClient_containsExplicitInstanceMetric() { ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient .builder() .region(Region.US_WEST_2) @@ -123,13 +130,107 @@ public void asyncClient_explicitHttpClient_containsHcExplicit() { .httpClient(mockAsyncHttpClient) .build(); client.allTypes(r -> {}).join(); - assertThat(asyncUserAgent()).contains("md/hc#e"); + assertThat(asyncUserAgent()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); + } + + // --- Explicit HTTP client factory tests (httpClientBuilder() call) --- + + @Test + public void syncClient_explicitHttpClientBuilder_containsExplicitFactoryMetric() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClientBuilder(new MockSyncHttpClientBuilder(mockSyncHttpClient)) + .build(); + client.allTypes(r -> {}); + assertThat(syncUserAgent()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); + } + + @Test + public void asyncClient_explicitHttpClientBuilder_containsExplicitFactoryMetric() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClientBuilder(new MockAsyncHttpClientBuilder(mockAsyncHttpClient)) + .build(); + client.allTypes(r -> {}).join(); + assertThat(asyncUserAgent()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); + } + + // --- Mutual exclusivity tests --- + + @Test + public void syncClient_defaultHttpClient_doesNotContainExplicitMetrics() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .build(); + client.allTypes(r -> {}); + String ua = lastWireMockUserAgent(); + assertThat(ua).matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); + assertThat(ua).doesNotMatch(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); + assertThat(ua).doesNotMatch(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); + } + + @Test + public void syncClient_explicitInstance_doesNotContainOtherMetrics() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockSyncHttpClient) + .build(); + client.allTypes(r -> {}); + String ua = syncUserAgent(); + assertThat(ua).matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); + assertThat(ua).doesNotMatch(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); + assertThat(ua).doesNotMatch(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); + } + + @Test + public void syncClient_explicitFactory_doesNotContainOtherMetrics() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClientBuilder(new MockSyncHttpClientBuilder(mockSyncHttpClient)) + .build(); + client.allTypes(r -> {}); + String ua = syncUserAgent(); + assertThat(ua).matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); + assertThat(ua).doesNotMatch(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); + assertThat(ua).doesNotMatch(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); } // --- Persistence tests --- @Test - public void syncClient_persistsAcrossRequests() { + public void syncClient_autoMetric_persistsAcrossRequests() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .build(); + client.allTypes(r -> {}); + String firstUserAgent = lastWireMockUserAgent(); + client.allTypes(r -> {}); + String secondUserAgent = lastWireMockUserAgent(); + assertThat(firstUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); + assertThat(secondUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); + } + + @Test + public void syncClient_explicitInstanceMetric_persistsAcrossRequests() { ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient .builder() .region(Region.US_WEST_2) @@ -141,12 +242,14 @@ public void syncClient_persistsAcrossRequests() { mockSyncHttpClient.stubNextResponse(mockResponse()); client.allTypes(r -> {}); String secondUserAgent = syncUserAgent(); - assertThat(firstUserAgent).contains("md/hc#e"); - assertThat(secondUserAgent).contains("md/hc#e"); + assertThat(firstUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); + assertThat(secondUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); } @Test - public void asyncClient_persistsAcrossRequests() { + public void asyncClient_explicitInstanceMetric_persistsAcrossRequests() { ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient .builder() .region(Region.US_WEST_2) @@ -158,8 +261,48 @@ public void asyncClient_persistsAcrossRequests() { mockAsyncHttpClient.stubNextResponse(mockResponse()); client.allTypes(r -> {}).join(); String secondUserAgent = asyncUserAgent(); - assertThat(firstUserAgent).contains("md/hc#e"); - assertThat(secondUserAgent).contains("md/hc#e"); + assertThat(firstUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); + assertThat(secondUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); + } + + @Test + public void syncClient_explicitFactoryMetric_persistsAcrossRequests() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClientBuilder(new MockSyncHttpClientBuilder(mockSyncHttpClient)) + .build(); + client.allTypes(r -> {}); + String firstUserAgent = syncUserAgent(); + mockSyncHttpClient.stubNextResponse(mockResponse()); + client.allTypes(r -> {}); + String secondUserAgent = syncUserAgent(); + assertThat(firstUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); + assertThat(secondUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); + } + + @Test + public void asyncClient_explicitFactoryMetric_persistsAcrossRequests() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClientBuilder(new MockAsyncHttpClientBuilder(mockAsyncHttpClient)) + .build(); + client.allTypes(r -> {}).join(); + String firstUserAgent = asyncUserAgent(); + mockAsyncHttpClient.stubNextResponse(mockResponse()); + client.allTypes(r -> {}).join(); + String secondUserAgent = asyncUserAgent(); + assertThat(firstUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); + assertThat(secondUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY.value())); } // --- Helpers --- @@ -190,4 +333,36 @@ private static HttpExecuteResponse mockResponse() { .responseBody(AbortableInputStream.create(new StringInputStream("{}"))) .build(); } + + /** + * A minimal SdkHttpClient.Builder that returns a pre-built mock sync HTTP client. + */ + private static class MockSyncHttpClientBuilder implements SdkHttpClient.Builder { + private final SdkHttpClient client; + + MockSyncHttpClientBuilder(SdkHttpClient client) { + this.client = client; + } + + @Override + public SdkHttpClient buildWithDefaults(software.amazon.awssdk.utils.AttributeMap serviceDefaults) { + return client; + } + } + + /** + * A minimal SdkAsyncHttpClient.Builder that returns a pre-built mock async HTTP client. + */ + private static class MockAsyncHttpClientBuilder implements SdkAsyncHttpClient.Builder { + private final SdkAsyncHttpClient client; + + MockAsyncHttpClientBuilder(SdkAsyncHttpClient client) { + this.client = client; + } + + @Override + public SdkAsyncHttpClient buildWithDefaults(software.amazon.awssdk.utils.AttributeMap serviceDefaults) { + return client; + } + } } From f997ee115051d06a6d2b1c3be25ee34432316857 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Fri, 10 Apr 2026 14:55:33 -0700 Subject: [PATCH 3/6] Switch to discrete feature IDs --- .../AwsExecutionContextBuilderTest.java | 92 ------------------- 1 file changed, 92 deletions(-) diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java index 0552d7df31db..fa3186b2973d 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java @@ -542,98 +542,6 @@ public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClient_a assertThat(businessMetrics.recordedMetrics()).contains("AK"); } - @Test - public void invokeInterceptorsAndCreateExecutionContext_withRequestBodyAndHcMetadata_addsBoth() throws IOException { - ClientExecutionParams executionParams = clientExecutionParams(); - File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); - testFile.deleteOnExit(); - executionParams.withRequestBody(RequestBody.fromFile(testFile)); - - SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_AUTO) - .build(); - - ExecutionContext executionContext = - AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); - - ExecutionAttributes executionAttributes = executionContext.executionAttributes(); - assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Collections.singletonList(AdditionalMetadata.builder().name("rb").value("f").build()) - ); - BusinessMetricCollection businessMetrics = - executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); - assertThat(businessMetrics.recordedMetrics()).contains("AJ"); - } - - @Test - public void invokeInterceptorsAndCreateExecutionContext_withAsyncRequestBodyAndHcMetadata_addsBoth() throws IOException { - ClientExecutionParams executionParams = clientExecutionParams(); - File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); - testFile.deleteOnExit(); - executionParams.withAsyncRequestBody(AsyncRequestBody.fromFile(testFile)); - - SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE) - .build(); - - ExecutionContext executionContext = - AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); - - ExecutionAttributes executionAttributes = executionContext.executionAttributes(); - assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Collections.singletonList(AdditionalMetadata.builder().name("rb").value("f").build()) - ); - BusinessMetricCollection businessMetrics = - executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); - assertThat(businessMetrics.recordedMetrics()).contains("AK"); - } - - @Test - public void invokeInterceptorsAndCreateExecutionContext_withResponseTransformerAndHcMetadata_addsBoth() throws IOException { - ClientExecutionParams executionParams = clientExecutionParams(); - File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); - testFile.deleteOnExit(); - executionParams.withResponseTransformer(ResponseTransformer.toFile(testFile)); - - SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_AUTO) - .build(); - - ExecutionContext executionContext = - AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); - - ExecutionAttributes executionAttributes = executionContext.executionAttributes(); - assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Collections.singletonList(AdditionalMetadata.builder().name("rt").value("f").build()) - ); - BusinessMetricCollection businessMetrics = - executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); - assertThat(businessMetrics.recordedMetrics()).contains("AJ"); - } - - @Test - public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransformerAndHcMetadata_addsBoth() throws IOException { - ClientExecutionParams executionParams = clientExecutionParams(); - File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); - testFile.deleteOnExit(); - executionParams.withAsyncResponseTransformer(AsyncResponseTransformer.toFile(testFile)); - - SdkClientConfiguration clientConfig = testClientConfiguration() - .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE) - .build(); - - ExecutionContext executionContext = - AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); - - ExecutionAttributes executionAttributes = executionContext.executionAttributes(); - assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( - Collections.singletonList(AdditionalMetadata.builder().name("rt").value("f").build()) - ); - BusinessMetricCollection businessMetrics = - executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); - assertThat(businessMetrics.recordedMetrics()).contains("AK"); - } - private ClientExecutionParams clientExecutionParams() { return clientExecutionParams(sdkRequest); } From 17e5843a965775cadf196a29f370ca9283440e05 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Fri, 10 Apr 2026 15:22:59 -0700 Subject: [PATCH 4/6] Tests update --- .../AwsExecutionContextBuilderTest.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java index fa3186b2973d..75ee9b0f462b 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java @@ -513,7 +513,7 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransfo } @Test - public void invokeInterceptorsAndCreateExecutionContext_withDefaultHttpClient_addsHcMetadata() { + public void invokeInterceptorsAndCreateExecutionContext_withDefaultHttpClient_addsAutoFeatureId() { SdkClientConfiguration clientConfig = testClientConfiguration() .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_AUTO) .build(); @@ -528,7 +528,7 @@ public void invokeInterceptorsAndCreateExecutionContext_withDefaultHttpClient_ad } @Test - public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClient_addsHcMetadata() { + public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClientInstance_addsExplicitInstanceFeatureId() { SdkClientConfiguration clientConfig = testClientConfiguration() .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE) .build(); @@ -542,6 +542,21 @@ public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClient_a assertThat(businessMetrics.recordedMetrics()).contains("AK"); } + @Test + public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClientFactory_addsExplicitFactoryFeatureId() { + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY) + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + assertThat(businessMetrics.recordedMetrics()).contains("AL"); + } + private ClientExecutionParams clientExecutionParams() { return clientExecutionParams(sdkRequest); } From 55618689b54cc8e2f67c102ec53a2c5001c45692 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Fri, 10 Apr 2026 15:26:54 -0700 Subject: [PATCH 5/6] Revert user-agent.md changes --- docs/user-agent.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/user-agent.md b/docs/user-agent.md index 5a3b336c76c8..bb1dfde3a401 100644 --- a/docs/user-agent.md +++ b/docs/user-agent.md @@ -19,6 +19,3 @@ The table below documents additional metadata the SDK may include in the `User-A ||`s`|"Stream". The response transformer adapts the response body to an `InputStream`.| ||`p`|"Publisher". The response transformer adapts the response body to an `SdkPublisher`.| ||`u`|"unknown"| -|`hc`||The HTTP client configuration type. Indicates whether the HTTP client was selected by default or explicitly configured by the user.| -||`d`|"Default". The HTTP client was auto-detected from the classpath.| -||`e`|"Explicit". The HTTP client was explicitly configured by the user via `httpClient()` or `httpClientBuilder()`.| From 557e79b7da97d60ed50ff86087a41337bc16c8aa Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Fri, 10 Apr 2026 16:33:23 -0700 Subject: [PATCH 6/6] Fix test --- .../EnvironmentTokenProviderTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/environmenttokenprovider/EnvironmentTokenProviderTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/environmenttokenprovider/EnvironmentTokenProviderTest.java index c16069a30dd5..870fba92ae0c 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/environmenttokenprovider/EnvironmentTokenProviderTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/environmenttokenprovider/EnvironmentTokenProviderTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.services.environmenttokenprovider; import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.core.useragent.BusinessMetricCollection.METRIC_SEARCH_PATTERN; import java.util.HashMap; import java.util.Map; @@ -135,10 +136,10 @@ private static void verifyRequest(TestCase testCase, SdkHttpFullRequest loggedRe if (testCase.expectBusinessMetricSet) { assertThat(loggedRequest.firstMatchingHeader("User-Agent").get()) - .matches(".*m\\/[A-Za-z0-9,]+" + BusinessMetricFeatureId.BEARER_SERVICE_ENV_VARS); + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.BEARER_SERVICE_ENV_VARS.value())); } else { assertThat(loggedRequest.firstMatchingHeader("User-Agent").get()) - .doesNotMatch(".*m\\/[A-Za-z0-9,]+" + BusinessMetricFeatureId.BEARER_SERVICE_ENV_VARS); + .doesNotMatch(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.BEARER_SERVICE_ENV_VARS.value())); } }