Skip to content

Commit 7179713

Browse files
authored
chore: Add powertools specific user-agent-suffix to the AWS SDK v2 clients (#1306)
1 parent 6900b72 commit 7179713

File tree

13 files changed

+304
-12
lines changed

13 files changed

+304
-12
lines changed

powertools-core/pom.xml

+11
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@
6464
<groupId>org.aspectj</groupId>
6565
<artifactId>aspectjrt</artifactId>
6666
</dependency>
67+
<dependency>
68+
<groupId>org.apache.logging.log4j</groupId>
69+
<artifactId>log4j-slf4j2-impl</artifactId>
70+
<version>${log4j.version}</version>
71+
</dependency>
6772

6873
<!-- Test dependencies -->
6974
<dependency>
@@ -104,6 +109,12 @@
104109
</dependencies>
105110

106111
<build>
112+
<resources>
113+
<resource>
114+
<directory>src/main/resources-filtered</directory>
115+
<filtering>true</filtering>
116+
</resource>
117+
</resources>
107118
<plugins>
108119
<plugin>
109120
<groupId>org.apache.maven.plugins</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2023 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*
13+
*/
14+
15+
package software.amazon.lambda.powertools.core.internal;
16+
17+
import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;
18+
19+
import java.io.FileInputStream;
20+
import java.io.IOException;
21+
import java.net.URL;
22+
import java.util.Properties;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
27+
/**
28+
* Can be used to create a string that can server as a User-Agent suffix in requests made with the AWS SDK clients
29+
*/
30+
public class UserAgentConfigurator {
31+
32+
public static final String NA = "NA";
33+
public static final String VERSION_KEY = "powertools.version";
34+
public static final String PT_FEATURE_VARIABLE = "${PT_FEATURE}";
35+
public static final String PT_EXEC_ENV_VARIABLE = "${PT_EXEC_ENV}";
36+
public static final String VERSION_PROPERTIES_FILENAME = "version.properties";
37+
public static final String AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV";
38+
private static final Logger LOG = LoggerFactory.getLogger(UserAgentConfigurator.class);
39+
private static final String NO_OP = "no-op";
40+
private static String ptVersion = getProjectVersion();
41+
private static String userAgentPattern = "PT/" + PT_FEATURE_VARIABLE + "/" + ptVersion + " PTEnv/"
42+
+ PT_EXEC_ENV_VARIABLE;
43+
44+
private UserAgentConfigurator() {
45+
throw new IllegalStateException("Utility class. Not meant to be instantiated");
46+
}
47+
48+
/**
49+
* Retrieves the project version from the version.properties file
50+
*
51+
* @return the project version
52+
*/
53+
static String getProjectVersion() {
54+
return getVersionFromProperties(VERSION_PROPERTIES_FILENAME, VERSION_KEY);
55+
}
56+
57+
58+
/**
59+
* Retrieves the project version from a properties file.
60+
* The file should be in the resources folder.
61+
* The version is retrieved from the property with the given key.
62+
*
63+
* @param propertyFileName the name of the properties file
64+
* @param versionKey the key of the property that contains the version
65+
* @return the version of the project as configured in the given properties file
66+
*/
67+
static String getVersionFromProperties(String propertyFileName, String versionKey) {
68+
69+
URL propertiesFileURI = Thread.currentThread().getContextClassLoader().getResource(propertyFileName);
70+
if (propertiesFileURI != null) {
71+
try (FileInputStream fis = new FileInputStream(propertiesFileURI.getPath())) {
72+
Properties properties = new Properties();
73+
properties.load(fis);
74+
String version = properties.getProperty(versionKey);
75+
if (version != null && !version.isEmpty()) {
76+
return version;
77+
}
78+
} catch (IOException e) {
79+
LOG.warn("Unable to read {} file. Using default version.", propertyFileName);
80+
LOG.debug("Exception:", e);
81+
}
82+
}
83+
return NA;
84+
}
85+
86+
/**
87+
* Retrieves the user agent string for the Powertools for AWS Lambda.
88+
* It follows the pattern PT/{PT_FEATURE}/{PT_VERSION} PTEnv/{PT_EXEC_ENV}
89+
* The version of the project is automatically retrieved.
90+
* The PT_EXEC_ENV is automatically retrieved from the AWS_EXECUTION_ENV environment variable.
91+
* If it AWS_EXECUTION_ENV is not set, PT_EXEC_ENV defaults to "NA"
92+
*
93+
* @param ptFeature a custom feature to be added to the user agent string (e.g. idempotency).
94+
* If null or empty, the default PT_FEATURE is used.
95+
* The default PT_FEATURE is "no-op".
96+
* @return the user agent string
97+
*/
98+
public static String getUserAgent(String ptFeature) {
99+
100+
String awsExecutionEnv = getenv(AWS_EXECUTION_ENV);
101+
String ptExecEnv = awsExecutionEnv != null ? awsExecutionEnv : NA;
102+
String userAgent = userAgentPattern.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv);
103+
104+
if (ptFeature == null || ptFeature.isEmpty()) {
105+
ptFeature = NO_OP;
106+
}
107+
return userAgent
108+
.replace(PT_FEATURE_VARIABLE, ptFeature)
109+
.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv);
110+
}
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# The filtered properties can have variables that are filled in by system properties or project properties.
2+
# See https://maven.apache.org/plugins/maven-resources-plugin/examples/filter.html
3+
#
4+
# The values are replaced before copying the resources to the main output directory. Therefore, as soon as the build phase is completed,
5+
# the values should have been replaced if the properties are available and if 'filtering' is set to true in the pom.xml
6+
#
7+
#
8+
# The ${project.version} is retrieved from the respective pom.xml property
9+
powertools.version=${project.version}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2023 Amazon.com, Inc. or its affiliates.
3+
* Licensed under the Apache License, Version 2.0 (the
4+
* "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*
13+
*/
14+
15+
package software.amazon.lambda.powertools.core.internal;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.mockito.Mockito.mockStatic;
19+
import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv;
20+
import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.AWS_EXECUTION_ENV;
21+
import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.VERSION_KEY;
22+
import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.VERSION_PROPERTIES_FILENAME;
23+
import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.getVersionFromProperties;
24+
25+
import java.io.File;
26+
import java.util.Objects;
27+
import java.util.regex.Pattern;
28+
import org.junit.jupiter.api.Test;
29+
import org.mockito.MockedStatic;
30+
31+
class UserAgentConfiguratorTest {
32+
33+
private static final String SEM_VER_PATTERN = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
34+
private static final String VERSION = UserAgentConfigurator.getProjectVersion();
35+
36+
37+
@Test
38+
void testGetVersion() {
39+
40+
assertThat(VERSION)
41+
.isNotNull()
42+
.isNotEmpty();
43+
assertThat(Pattern.matches(SEM_VER_PATTERN, VERSION)).isTrue();
44+
}
45+
46+
@Test
47+
void testGetVersionFromProperties_WrongKey() {
48+
String version = getVersionFromProperties(VERSION_PROPERTIES_FILENAME, "some invalid key");
49+
50+
assertThat(version)
51+
.isNotNull()
52+
.isEqualTo("NA");
53+
}
54+
55+
@Test
56+
void testGetVersionFromProperties_FileNotExist() {
57+
String version = getVersionFromProperties("some file", VERSION_KEY);
58+
59+
assertThat(version)
60+
.isNotNull()
61+
.isEqualTo("NA");
62+
}
63+
64+
@Test
65+
void testGetVersionFromProperties_InvalidFile() {
66+
File f = new File(Objects.requireNonNull(Thread.currentThread().getContextClassLoader()
67+
.getResource("unreadable.properties")).getPath());
68+
f.setReadable(false);
69+
70+
String version = getVersionFromProperties("unreadable.properties", VERSION_KEY);
71+
72+
assertThat(version).isEqualTo("NA");
73+
}
74+
75+
@Test
76+
void testGetVersionFromProperties_EmptyVersion() {
77+
String version = getVersionFromProperties("test.properties", VERSION_KEY);
78+
79+
assertThat(version).isEqualTo("NA");
80+
}
81+
82+
@Test
83+
void testGetUserAgent() {
84+
String userAgent = UserAgentConfigurator.getUserAgent("test-feature");
85+
86+
assertThat(userAgent)
87+
.isNotNull()
88+
.isEqualTo("PT/test-feature/" + VERSION + " PTEnv/NA");
89+
90+
}
91+
92+
@Test
93+
void testGetUserAgent_NoFeature() {
94+
String userAgent = UserAgentConfigurator.getUserAgent("");
95+
96+
assertThat(userAgent)
97+
.isNotNull()
98+
.isEqualTo("PT/no-op/" + VERSION + " PTEnv/NA");
99+
}
100+
101+
@Test
102+
void testGetUserAgent_NullFeature() {
103+
String userAgent = UserAgentConfigurator.getUserAgent(null);
104+
105+
assertThat(userAgent)
106+
.isNotNull()
107+
.isEqualTo("PT/no-op/" + VERSION + " PTEnv/NA");
108+
}
109+
110+
@Test
111+
void testGetUserAgent_SetAWSExecutionEnv() {
112+
try (MockedStatic<SystemWrapper> mockedSystemWrapper = mockStatic(SystemWrapper.class)) {
113+
mockedSystemWrapper.when(() -> getenv(AWS_EXECUTION_ENV)).thenReturn("AWS_Lambda_java8");
114+
String userAgent = UserAgentConfigurator.getUserAgent("test-feature");
115+
116+
assertThat(userAgent)
117+
.isNotNull()
118+
.isEqualTo("PT/test-feature/" + VERSION + " PTEnv/AWS_Lambda_java8");
119+
}
120+
}
121+
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
powertools.version=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This is intentionally left empty
2+
# It used during testing and is set to un-readable to fulfil the test purposes.

powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java

+15-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414

1515
package software.amazon.lambda.powertools.idempotency.persistence;
1616

17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
20+
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
21+
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
22+
import software.amazon.awssdk.regions.Region;
23+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
24+
import software.amazon.awssdk.utils.StringUtils;
25+
import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator;
26+
import software.amazon.lambda.powertools.idempotency.Constants;
27+
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
28+
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
1729
import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_REGION_ENV;
1830
import static software.amazon.lambda.powertools.core.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV;
1931
import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS;
@@ -25,22 +37,14 @@
2537
import java.util.OptionalLong;
2638
import java.util.stream.Collectors;
2739
import java.util.stream.Stream;
28-
import org.slf4j.Logger;
29-
import org.slf4j.LoggerFactory;
30-
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
31-
import software.amazon.awssdk.regions.Region;
32-
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
40+
3341
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3442
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
3543
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
3644
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
3745
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
3846
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
3947
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
40-
import software.amazon.awssdk.utils.StringUtils;
41-
import software.amazon.lambda.powertools.idempotency.Constants;
42-
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
43-
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
4448

4549
/**
4650
* DynamoDB version of the {@link PersistenceStore}. Will store idempotency data in DynamoDB.<br>
@@ -49,6 +53,7 @@
4953
public class DynamoDBPersistenceStore extends BasePersistenceStore implements PersistenceStore {
5054

5155
private static final Logger LOG = LoggerFactory.getLogger(DynamoDBPersistenceStore.class);
56+
public static final String IDEMPOTENCY = "idempotency";
5257

5358
private final String tableName;
5459
private final String keyAttr;
@@ -92,6 +97,7 @@ private DynamoDBPersistenceStore(String tableName,
9297
if (idempotencyDisabledEnv == null || "false".equalsIgnoreCase(idempotencyDisabledEnv)) {
9398
this.dynamoDbClient = DynamoDbClient.builder()
9499
.httpClient(UrlConnectionHttpClient.builder().build())
100+
.overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(IDEMPOTENCY)).build())
95101
.region(Region.of(System.getenv(AWS_REGION_ENV)))
96102
.build();
97103
} else {

powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java

+5
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
import java.util.HashMap;
1818
import java.util.Map;
1919
import software.amazon.awssdk.core.SdkSystemSetting;
20+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
21+
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
2022
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
2123
import software.amazon.awssdk.regions.Region;
2224
import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient;
2325
import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest;
2426
import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse;
2527
import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest;
28+
import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator;
2629
import software.amazon.lambda.powertools.parameters.cache.CacheManager;
2730
import software.amazon.lambda.powertools.parameters.transform.TransformationManager;
2831

@@ -153,6 +156,8 @@ public AppConfigProvider build() {
153156
client = AppConfigDataClient.builder()
154157
.httpClientBuilder(UrlConnectionHttpClient.builder())
155158
.region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())))
159+
.overrideConfiguration(ClientOverrideConfiguration.builder()
160+
.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(PARAMETERS)).build())
156161
.build();
157162
}
158163

powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
*/
3232
@NotThreadSafe
3333
public abstract class BaseProvider implements ParamProvider {
34+
public static final String PARAMETERS = "parameters";
3435

3536
protected final CacheManager cacheManager;
3637
private TransformationManager transformationManager;

powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/DynamoDbProvider.java

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import java.util.Map;
1919
import java.util.stream.Collectors;
2020
import software.amazon.awssdk.core.SdkSystemSetting;
21+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
22+
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
2123
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
2224
import software.amazon.awssdk.regions.Region;
2325
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@@ -26,6 +28,7 @@
2628
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
2729
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
2830
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
31+
import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator;
2932
import software.amazon.lambda.powertools.parameters.cache.CacheManager;
3033
import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException;
3134
import software.amazon.lambda.powertools.parameters.transform.TransformationManager;
@@ -132,6 +135,7 @@ private static DynamoDbClient createClient() {
132135
return DynamoDbClient.builder()
133136
.httpClientBuilder(UrlConnectionHttpClient.builder())
134137
.region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())))
138+
.overrideConfiguration(ClientOverrideConfiguration.builder().putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, UserAgentConfigurator.getUserAgent(PARAMETERS)).build())
135139
.build();
136140
}
137141

0 commit comments

Comments
 (0)