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 all 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 @@ -95,4 +95,5 @@ default B addPlugin(SdkPlugin plugin) {
default List<SdkPlugin> plugins() {
throw new UnsupportedOperationException();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
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.client.config.SdkClientOption.USER_AGENT_APP_ID;
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 @@ -90,6 +92,7 @@
import software.amazon.awssdk.core.internal.http.pipeline.stages.CompressRequestStage;
import software.amazon.awssdk.core.internal.interceptor.HttpChecksumValidationInterceptor;
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy;
import software.amazon.awssdk.core.internal.useragent.AppIdResolver;
import software.amazon.awssdk.core.internal.useragent.SdkClientUserAgentProperties;
import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder;
import software.amazon.awssdk.core.retry.RetryMode;
Expand Down Expand Up @@ -151,7 +154,6 @@ public abstract class SdkDefaultClientBuilder<B extends SdkClientBuilder<B, C>,
private final List<SdkPlugin> plugins = new ArrayList<>();



protected SdkDefaultClientBuilder() {
this(DEFAULT_HTTP_CLIENT_BUILDER, DEFAULT_ASYNC_HTTP_CLIENT_BUILDER);
}
Expand Down Expand Up @@ -413,7 +415,7 @@ private String resolveClientUserAgent(LazyValueSource config) {
SdkClientUserAgentProperties clientProperties = new SdkClientUserAgentProperties();

ClientType clientType = config.get(CLIENT_TYPE);
ClientType resolvedClientType = clientType == null ? ClientType.UNKNOWN : config.get(CLIENT_TYPE);
ClientType resolvedClientType = clientType == null ? ClientType.UNKNOWN : clientType;

clientProperties.putProperty(RETRY_MODE, StringUtils.lowerCase(resolveRetryMode(config.get(RETRY_POLICY),
config.get(RETRY_STRATEGY))));
Expand All @@ -422,10 +424,20 @@ private String resolveClientUserAgent(LazyValueSource config) {
clientProperties.putProperty(HTTP, SdkHttpUtils.urlEncode(clientName(resolvedClientType,
config.get(SYNC_HTTP_CLIENT),
config.get(ASYNC_HTTP_CLIENT))));

String appId = config.get(USER_AGENT_APP_ID);
String resolvedAppId = appId == null ? resolveAppId(config) : appId;
clientProperties.putProperty(APP_ID, resolvedAppId);
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 +458,7 @@ private RetryStrategy resolveRetryStrategy(LazyValueSource config) {
.resolve();
return SdkDefaultRetryStrategy.forRetryMode(retryMode);
}

/**
* Finalize which sync HTTP client will be used for the created client.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY;
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.USER_AGENT_APP_ID;
import static software.amazon.awssdk.utils.ScheduledExecutorUtils.unmanagedScheduledExecutor;
import static software.amazon.awssdk.utils.ScheduledExecutorUtils.unwrapUnmanagedScheduledExecutor;

Expand Down Expand Up @@ -120,6 +121,7 @@ public final class ClientOverrideConfiguration
options.add(CONFIGURED_RETRY_STRATEGY);
options.add(CONFIGURED_RETRY_CONFIGURATOR);
options.add(CONFIGURED_RETRY_MODE);
options.add(USER_AGENT_APP_ID);
CLIENT_OVERRIDE_OPTIONS = Collections.unmodifiableSet(options);

Set<ClientOption<?>> resolvedOptions = new HashSet<>();
Expand Down Expand Up @@ -381,6 +383,14 @@ public Optional<CompressionConfiguration> compressionConfiguration() {
return Optional.ofNullable(compressionConfig);
}

/**
* An optional user specified identification value to be appended to the user agent header.
* For more information, see {@link SdkClientOption#USER_AGENT_APP_ID}.
*/
public Optional<String> appId() {
return Optional.ofNullable(config.option(USER_AGENT_APP_ID));
}

@Override
public String toString() {
return ToString.builder("ClientOverrideConfiguration")
Expand All @@ -395,6 +405,7 @@ public String toString() {
.add("profileName", defaultProfileName().orElse(null))
.add("scheduledExecutorService", scheduledExecutorService().orElse(null))
.add("compressionConfiguration", compressionConfiguration().orElse(null))
.add("appId", appId().orElse(null))
.build();
}

Expand Down Expand Up @@ -757,6 +768,16 @@ default Builder compressionConfiguration(Consumer<CompressionConfiguration.Build
}

CompressionConfiguration compressionConfiguration();

/**
* Sets the appId for this client. See {@link SdkClientOption#USER_AGENT_APP_ID}.
*/
Builder appId(String appId);

/**
* The appId for this client. See {@link SdkClientOption#USER_AGENT_APP_ID}.
*/
String appId();
}

/**
Expand Down Expand Up @@ -1089,6 +1110,17 @@ public CompressionConfiguration compressionConfiguration() {
return config.option(CONFIGURED_COMPRESSION_CONFIGURATION);
}

@Override
public String appId() {
return config.option(USER_AGENT_APP_ID);
}

@Override
public Builder appId(String appId) {
config.option(USER_AGENT_APP_ID, appId);
return this;
}

@Override
public ClientOverrideConfiguration build() {
return new ClientOverrideConfiguration(config.build(), resolvedConfig.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,24 @@ public final class SdkClientOption<T> extends ClientOption<T> {
public static final SdkClientOption<CompressionConfiguration> COMPRESSION_CONFIGURATION =
new SdkClientOption<>(CompressionConfiguration.class);

/**
* 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 option 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.
*/
public static final SdkClientOption<String> USER_AGENT_APP_ID = new SdkClientOption<>(String.class);

/**
* Option to specify a reference to the SDK client in use.
*/
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 @@ -34,6 +35,7 @@
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.core.util.SystemUserAgent;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.StringUtils;

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

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

private SdkUserAgentBuilder() {
}

Expand Down Expand Up @@ -77,6 +81,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 +134,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
Expand Up @@ -382,7 +382,7 @@ public void explicitAsyncHttpClientProvided_ClientIsNotManagedBySdk() {
public void clientBuilderFieldsHaveBeanEquivalents() throws Exception {
// Mutating properties might not have bean equivalents. This is probably fine, since very few customers require
// bean-equivalent methods and it's not clear what they'd expect them to be named anyway. Ignore these methods for now.
Set<String> NON_BEAN_EQUIVALENT_METHODS = ImmutableSet.of("addPlugin", "plugins", "putAuthScheme");
Set<String> NON_BEAN_EQUIVALENT_METHODS = ImmutableSet.of("addPlugin", "plugins", "putAuthScheme", "appId");
SdkClientBuilder<TestClientBuilder, TestClient> builder = testClientBuilder();

BeanInfo beanInfo = Introspector.getBeanInfo(builder.getClass());
Expand Down
Loading
Loading