Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds the user agent appId metadata tag #5636

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-0277feb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Adds an option to set 'appId' metadata to the client builder or to system settings and config files. This metadata string value will be added to the user agent string as `app/somevalue`"
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ public final class ProfileProperty {
*/
public static final String ENDPOINT_URL = "endpoint_url";

/**
* Configure an optional identification value to be appended to the user agent header.
* The value should be less than 50 characters in length and is null by default.
*/
public static final String SDK_UA_APP_ID = "sdk_ua_app_id";

private ProfileProperty() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,13 @@ public enum SdkSystemSetting implements SystemSetting {
* Defines a file path from which partition metadata should be loaded. If this isn't specified, the partition
* metadata deployed with the SDK client will be used instead.
*/
AWS_PARTITIONS_FILE("aws.partitionsFile", null)
AWS_PARTITIONS_FILE("aws.partitionsFile", null),

/**
* Configure an optional identification value to be appended to the user agent header.
* The value should be less than 50 characters in length and is null by default.
*/
AWS_SDK_UA_APP_ID("sdk.ua.appId", null)

;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.core.SdkPlugin;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.endpoints.EndpointProvider;
import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme;
import software.amazon.awssdk.utils.builder.SdkBuilder;
Expand Down Expand Up @@ -95,4 +96,24 @@ default B addPlugin(SdkPlugin plugin) {
default List<SdkPlugin> plugins() {
throw new UnsupportedOperationException();
}

/**
* Configure an optional identification value to be appended to the user agent header.
* The value should be less than 50 characters in length and is null by default.
* <p>
* Users can additionally supply the appId value through environment and JVM settings, and
* it will be resolved using the following order of precedence (highest first):
* <ol>
* <li>This client builder configuration </li>
* <li>The {@code AWS_SDK_UA_APP_ID} environment variable</li>
* <li>The {@code sdk.ua.appId} JVM system property</li>
* <li>The {@code sdk_ua_app_id} setting in the profile file for the active profile</li>
* </ol>
* <p>
* This configuration option supersedes {@link SdkAdvancedClientOption#USER_AGENT_PREFIX} and
* {@link SdkAdvancedClientOption#USER_AGENT_SUFFIX} and should be used instead of those options.
*/
default B appId(String appId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add this to ClientOverrideConfiguration instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY;
import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE;
import static software.amazon.awssdk.core.client.config.SdkClientOption.SYNC_HTTP_CLIENT;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.APP_ID;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.INTERNAL_METADATA_MARKER;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO;
Expand Down Expand Up @@ -92,6 +93,7 @@
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy;
import software.amazon.awssdk.core.internal.useragent.SdkClientUserAgentProperties;
import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder;
import software.amazon.awssdk.core.internal.useragent.AppIdResolver;
davidh44 marked this conversation as resolved.
Show resolved Hide resolved
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.core.retry.RetryPolicy;
import software.amazon.awssdk.core.util.SystemUserAgent;
Expand Down Expand Up @@ -149,7 +151,7 @@ public abstract class SdkDefaultClientBuilder<B extends SdkClientBuilder<B, C>,
private final SdkHttpClient.Builder defaultHttpClientBuilder;
private final SdkAsyncHttpClient.Builder defaultAsyncHttpClientBuilder;
private final List<SdkPlugin> plugins = new ArrayList<>();

private String appId;
cenedhryn marked this conversation as resolved.
Show resolved Hide resolved


protected SdkDefaultClientBuilder() {
Expand Down Expand Up @@ -422,10 +424,18 @@ private String resolveClientUserAgent(LazyValueSource config) {
clientProperties.putProperty(HTTP, SdkHttpUtils.urlEncode(clientName(resolvedClientType,
config.get(SYNC_HTTP_CLIENT),
config.get(ASYNC_HTTP_CLIENT))));

clientProperties.putProperty(APP_ID, appId().orElseGet(() -> resolveAppId(config)));
return SdkUserAgentBuilder.buildClientUserAgentString(SystemUserAgent.getOrCreate(), clientProperties);
}

private String resolveAppId(LazyValueSource config) {
Optional<String> appIdFromConfig = AppIdResolver.create()
.profileFile(config.get(PROFILE_FILE_SUPPLIER))
.profileName(config.get(PROFILE_NAME))
.resolve();
return appIdFromConfig.orElse(null);
}

private static String clientName(ClientType clientType, SdkHttpClient syncHttpClient, SdkAsyncHttpClient asyncHttpClient) {
if (clientType == SYNC) {
return syncHttpClient == null ? "null" : syncHttpClient.clientName();
Expand All @@ -446,7 +456,11 @@ private RetryStrategy resolveRetryStrategy(LazyValueSource config) {
.resolve();
return SdkDefaultRetryStrategy.forRetryMode(retryMode);
}


public Optional<String> appId() {
return Optional.ofNullable(appId);
}

/**
* Finalize which sync HTTP client will be used for the created client.
*/
Expand Down Expand Up @@ -644,6 +658,12 @@ public final List<SdkPlugin> plugins() {
return Collections.unmodifiableList(plugins);
}

@Override
public final B appId(String appId) {
this.appId = appId;
return thisBuilder();
}

/**
* Return "this" for method chaining.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.core.internal.useragent;

import java.util.Optional;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.utils.OptionalUtils;

@SdkInternalApi
public final class AppIdResolver {

private Supplier<ProfileFile> profileFile;
private String profileName;
Comment on lines +30 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they're not final


private AppIdResolver() {
}

public static AppIdResolver create() {
return new AppIdResolver();
}

public AppIdResolver profileFile(Supplier<ProfileFile> profileFile) {
this.profileFile = profileFile;
return this;
}

public AppIdResolver profileName(String profileName) {
this.profileName = profileName;
return this;
}

public Optional<String> resolve() {
return OptionalUtils.firstPresent(fromSystemSettings(),
() -> fromProfileFile(profileFile, profileName));
}

private Optional<String> fromSystemSettings() {
return SdkSystemSetting.AWS_SDK_UA_APP_ID.getStringValue();
}

private Optional<String> fromProfileFile(Supplier<ProfileFile> profileFile, String profileName) {
profileFile = profileFile != null ? profileFile : ProfileFile::defaultProfileFile;
profileName = profileName != null ? profileName : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow();
return profileFile.get()
.profile(profileName)
.flatMap(p -> p.property(ProfileProperty.SDK_UA_APP_ID));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.core.internal.useragent;

import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.APP_ID;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.CONFIG_METADATA;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.ENV_METADATA;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP;
Expand All @@ -33,7 +34,9 @@

import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.core.internal.http.pipeline.stages.ApplyUserAgentStage;
davidh44 marked this conversation as resolved.
Show resolved Hide resolved
import software.amazon.awssdk.core.util.SystemUserAgent;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.StringUtils;

/**
Expand All @@ -43,6 +46,8 @@
@SdkProtectedApi
public final class SdkUserAgentBuilder {

private static final Logger log = Logger.loggerFor(SdkUserAgentBuilder.class);

private SdkUserAgentBuilder() {
}

Expand Down Expand Up @@ -77,6 +82,12 @@ public static String buildClientUserAgentString(SystemUserAgent systemValues,
appendFieldAndSpace(uaString, CONFIG_METADATA, uaPair(RETRY_MODE, retryMode));
}

String appId = userAgentProperties.getProperty(APP_ID);
if (!StringUtils.isEmpty(appId)) {
checkLengthAndWarn(appId);
appendFieldAndSpace(uaString, APP_ID, appId);
}

removeFinalWhitespace(uaString);
return uaString.toString();
}
Expand Down Expand Up @@ -124,4 +135,12 @@ private static void appendAdditionalJvmMetadata(StringBuilder builder, SystemUse
appendNonEmptyField(builder, METADATA, lang);
}
}

private static void checkLengthAndWarn(String appId) {
if (appId.length() > 50) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double check, is this only logged once?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's only logged once.

log.warn(() -> String.format("The configured appId '%s' is longer than the recommended maximum length of 50. "
+ "This could result in not being able to transmit and log the whole user agent string, "
+ "including the complete value of this string.", appId));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public final class UserAgentConstant {
public static final String FRAMEWORK_METADATA = "lib";
public static final String METADATA = "md";
public static final String INTERNAL_METADATA_MARKER = "internal";
public static final String APP_ID = "app";

//Separators used in SDK user agent
public static final String SLASH = "/";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.core.internal.useragent;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
import software.amazon.awssdk.utils.Pair;
import software.amazon.awssdk.utils.StringInputStream;
import software.amazon.awssdk.utils.StringUtils;

class AppIdResolutionTest {

private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper();
private static final String PROFILE = "test";

@AfterEach
public void cleanup() {
ENVIRONMENT_VARIABLE_HELPER.reset();
System.clearProperty(SdkSystemSetting.AWS_SDK_UA_APP_ID.property());
}

@ParameterizedTest(name = "{index} - {0}")
@MethodSource("inputValues")
void resolveAppIdFromEnvironment(String description, String systemProperty, String envVar,
ProfileFile profileFile, String expected) {

setUpSystemSettings(systemProperty, envVar);

AppIdResolver resolver = AppIdResolver.create().profileName(PROFILE);
if (profileFile != null) {
resolver.profileFile(() -> profileFile);
}

if (expected != null) {
assertThat(resolver.resolve()).isNotEmpty().contains(expected);
} else {
assertThat(resolver.resolve()).isEmpty();
}
}

private static Stream<Arguments> inputValues() {
ProfileFile emptyProfile = configFile("profile test", Pair.of("foo", "bar"));

Function<String, ProfileFile> testProfileConfig =
s -> configFile("profile test", Pair.of(ProfileProperty.SDK_UA_APP_ID, s));

return Stream.of(
Arguments.of("Without input, resolved value is null", null, null, null, null),
Arguments.of("Setting system property only gives result", "SystemPropertyAppId", null, null, "SystemPropertyAppId"),
Arguments.of("Setting env var only gives result", null, "EnvVarAppId", null, "EnvVarAppId"),
Arguments.of("System property takes precedence over env var", "SystemPropertyAppId", "EnvVarAppId", null,
"SystemPropertyAppId"),
Arguments.of("Setting profile file only gives result", null, null, testProfileConfig.apply("profileAppId"),
"profileAppId"),
Arguments.of("When profile file exists but has no input, resolved value is null", null, null, emptyProfile, null),
Arguments.of("System property takes precedence over profile file", "SystemPropertyAppId", null,
testProfileConfig.apply("profileAppId"), "SystemPropertyAppId"),
Arguments.of("Env var takes precedence over profile file", null, "EnvVarAppId",
testProfileConfig.apply("profileAppId"), "EnvVarAppId"),
Arguments.of("System prop var takes precedence over profile file", null, "EnvVarAppId",
testProfileConfig.apply("profileAppId"), "EnvVarAppId")
);
}

private static void setUpSystemSettings(String systemProperty, String envVar) {
if (!StringUtils.isEmpty(systemProperty)) {
System.setProperty(SdkSystemSetting.AWS_SDK_UA_APP_ID.property(), systemProperty);
}
if (!StringUtils.isEmpty(envVar)) {
ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_SDK_UA_APP_ID.environmentVariable(), envVar);
}
}

private static ProfileFile configFile(String name, Pair<?, ?>... pairs) {
String values = Arrays.stream(pairs)
.map(pair -> String.format("%s=%s", pair.left(), pair.right()))
.collect(Collectors.joining(System.lineSeparator()));
String contents = String.format("[%s]\n%s", name, values);

return configFile(contents);
}

private static ProfileFile configFile(String credentialFile) {
return ProfileFile.builder()
.content(new StringInputStream(credentialFile))
.type(ProfileFile.Type.CONFIGURATION)
.build();
}
}
Loading
Loading