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..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 @@ -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,58 @@ 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())); } + } + + private static void putHttpClientConfigTypeMetadata(ExecutionAttributes executionAttributes, + SdkClientConfiguration clientConfig) { + BusinessMetricFeatureId httpClientConfigType = clientConfig.option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE); + if (httpClientConfigType == null) { + return; + } + BusinessMetricCollection businessMetrics = executionAttributes.getAttribute( + SdkInternalExecutionAttribute.BUSINESS_METRICS); + if (businessMetrics != null) { + businessMetrics.addMetric(httpClientConfigType.value()); + } + } - executionAttributes.putAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA, userAgentMetadata); + 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..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 @@ -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; @@ -510,6 +512,51 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransfo ); } + @Test + public void invokeInterceptorsAndCreateExecutionContext_withDefaultHttpClient_addsAutoFeatureId() { + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_AUTO) + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + assertThat(businessMetrics.recordedMetrics()).contains("AJ"); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClientInstance_addsExplicitInstanceFeatureId() { + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE) + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + 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); } 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..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 @@ -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; @@ -96,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; @@ -310,34 +312,57 @@ 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 BusinessMetricFeatureId resolveSyncHttpClientConfigType(SdkClientConfiguration config) { + if (config.option(CONFIGURED_SYNC_HTTP_CLIENT) != null) { + return BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE; + } + if (config.option(CONFIGURED_SYNC_HTTP_CLIENT_BUILDER) != null) { + return BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY; + } + return BusinessMetricFeatureId.HTTP_CLIENT_AUTO; + } + /** * 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 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 BusinessMetricFeatureId.HTTP_CLIENT_AUTO; + } + /** * 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, () -> BusinessMetricFeatureId.HTTP_CLIENT_AUTO); + 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..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; @@ -196,6 +197,21 @@ 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: + *

    + *
  • {@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<>(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 973fc5a92ce5..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 @@ -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; @@ -81,14 +82,13 @@ 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; 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 +389,96 @@ public void explicitAsyncHttpClientProvided_ClientIsNotManagedBySdk() { verify(defaultAsyncHttpClientFactory, never()).buildWithDefaults(any()); } + @Test + public void noHttpClientProvided_httpClientConfigTypeIsAuto() { + TestClient client = testClientBuilder().build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); + } + + @Test + public void noAsyncHttpClientProvided_httpClientConfigTypeIsAuto() { + TestAsyncClient client = testAsyncClientBuilder().build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); + } + + @Test + public void explicitSyncHttpClientProvided_httpClientConfigTypeIsExplicitInstance() { + TestClient client = testClientBuilder() + .httpClient(mock(SdkHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE); + } + + @Test + public void explicitSyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicitFactory() { + TestClient client = testClientBuilder() + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> mock(SdkHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY); + } + + @Test + public void explicitAsyncHttpClientProvided_httpClientConfigTypeIsExplicitInstance() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClient(mock(SdkAsyncHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE); + } + + @Test + public void explicitAsyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicitFactory() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClientBuilder((SdkAsyncHttpClient.Builder) serviceDefaults -> mock(SdkAsyncHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_FACTORY); + } + + @Test + public void syncHttpClientSetThenCleared_httpClientConfigTypeIsAuto() { + TestClient client = testClientBuilder() + .httpClient(mock(SdkHttpClient.class)) + .httpClient((SdkHttpClient) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); + } + + @Test + 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(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); + } + + @Test + public void asyncHttpClientSetThenCleared_httpClientConfigTypeIsAuto() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClient(mock(SdkAsyncHttpClient.class)) + .httpClient((SdkAsyncHttpClient) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)) + .isEqualTo(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); + } + + @Test + 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(BusinessMetricFeatureId.HTTP_CLIENT_AUTO); + } + @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/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())); } } 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..8d59402f7ee3 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java @@ -0,0 +1,368 @@ +/* + * 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 static software.amazon.awssdk.core.useragent.BusinessMetricCollection.METRIC_SEARCH_PATTERN; + +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.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; +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 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 { + + 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(); + } + + // --- Auto-selected HTTP client tests (no httpClient/httpClientBuilder call) --- + + @Test + public void syncClient_defaultHttpClient_containsAutoMetric() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .build(); + client.allTypes(r -> {}); + assertThat(lastWireMockUserAgent()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); + } + + @Test + public void asyncClient_defaultHttpClient_containsAutoMetric() { + 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()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_AUTO.value())); + } + + // --- Explicit HTTP client instance tests (httpClient() call) --- + + @Test + public void syncClient_explicitHttpClient_containsExplicitInstanceMetric() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockSyncHttpClient) + .build(); + client.allTypes(r -> {}); + assertThat(syncUserAgent()) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); + } + + @Test + public void asyncClient_explicitHttpClient_containsExplicitInstanceMetric() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockAsyncHttpClient) + .build(); + client.allTypes(r -> {}).join(); + 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_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) + .credentialsProvider(CREDENTIALS) + .httpClient(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_INSTANCE.value())); + assertThat(secondUserAgent) + .matches(METRIC_SEARCH_PATTERN.apply(BusinessMetricFeatureId.HTTP_CLIENT_EXPLICIT_INSTANCE.value())); + } + + @Test + public void asyncClient_explicitInstanceMetric_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) + .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 --- + + 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(); + } + + /** + * 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; + } + } +}