Skip to content

Commit 37de57d

Browse files
committed
Add namespace validation. Refacator into own Validator class. Require namespace to be set or raise an exception otherwise.
1 parent cc74f3f commit 37de57d

File tree

11 files changed

+312
-182
lines changed

11 files changed

+312
-182
lines changed

powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,8 @@
4848
* or the annotation variable {@code @Metrics(service = "Service Name")}.
4949
* If both are specified then the value of the annotation variable will be used.</p>
5050
*
51-
* <p>By default the namespace associated with metrics created will be "aws-embedded-metrics".
52-
* This can be overridden with the environment variable {@code POWERTOOLS_METRICS_NAMESPACE}
53-
* or the annotation variable {@code @Metrics(namespace = "Namespace")}.
51+
* <p>A namespace must be specified for metrics. This can be set with the environment variable {@code POWERTOOLS_METRICS_NAMESPACE}
52+
* or the annotation variable {@code @Metrics(namespace = "Namespace")}. If not specified, an IllegalStateException will be thrown.
5453
* If both are specified then the value of the annotation variable will be used.</p>
5554
*
5655
* <p>You can specify a custom function name with {@code @Metrics(functionName = "MyFunction")}.

powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,13 @@ public software.amazon.lambda.powertools.metrics.model.DimensionSet getDefaultDi
119119

120120
@Override
121121
public void setNamespace(String namespace) {
122+
Validator.validateNamespace(namespace);
123+
122124
this.namespace = namespace;
123125
try {
124126
emfLogger.setNamespace(namespace);
125127
} catch (Exception e) {
126-
// Ignore namespace errors
128+
LOGGER.error("Namespace cannot be set due to an error in EMF", e);
127129
}
128130
}
129131

@@ -140,6 +142,8 @@ public void clearDefaultDimensions() {
140142

141143
@Override
142144
public void flush() {
145+
Validator.validateNamespace(namespace);
146+
143147
if (!hasMetrics.get()) {
144148
if (raiseOnEmptyMetrics) {
145149
throw new IllegalStateException("No metrics were emitted");
@@ -154,15 +158,14 @@ public void flush() {
154158
public void captureColdStartMetric(Context context,
155159
software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) {
156160
if (isColdStart()) {
161+
Validator.validateNamespace(namespace);
162+
157163
software.amazon.cloudwatchlogs.emf.logger.MetricsLogger coldStartLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger();
158164

159-
// Set namespace if available
160-
if (namespace != null) {
161-
try {
162-
coldStartLogger.setNamespace(namespace);
163-
} catch (Exception e) {
164-
// Ignore namespace errors
165-
}
165+
try {
166+
coldStartLogger.setNamespace(namespace);
167+
} catch (Exception e) {
168+
LOGGER.error("Namespace cannot be set for cold start metrics due to an error in EMF", e);
166169
}
167170

168171
coldStartLogger.putMetric(COLD_START_METRIC, 1, Unit.COUNT);
@@ -200,15 +203,16 @@ public void captureColdStartMetric(software.amazon.lambda.powertools.metrics.mod
200203
@Override
201204
public void pushSingleMetric(String name, double value, MetricUnit unit, String namespace,
202205
software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) {
206+
Validator.validateNamespace(namespace);
207+
203208
// Create a new logger for this single metric
204209
software.amazon.cloudwatchlogs.emf.logger.MetricsLogger singleMetricLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(
205210
environmentProvider);
206211

207-
// Set namespace (now mandatory)
208212
try {
209213
singleMetricLogger.setNamespace(namespace);
210214
} catch (Exception e) {
211-
// Ignore namespace errors
215+
LOGGER.error("Namespace cannot be set for single metric due to an error in EMF", e);
212216
}
213217

214218
// Add the metric
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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.metrics.internal;
16+
17+
import org.apache.commons.lang3.StringUtils;
18+
19+
/**
20+
* Utility class for validating metrics-related parameters.
21+
*/
22+
public class Validator {
23+
private static final int MAX_DIMENSION_NAME_LENGTH = 250;
24+
private static final int MAX_DIMENSION_VALUE_LENGTH = 1024;
25+
private static final int MAX_NAMESPACE_LENGTH = 255;
26+
private static final String NAMESPACE_REGEX = "^[a-zA-Z0-9._#/]+$";
27+
28+
private Validator() {
29+
// Private constructor to prevent instantiation
30+
}
31+
32+
/**
33+
* Validates that a namespace is properly specified.
34+
*
35+
* @param namespace The namespace to validate
36+
* @throws IllegalArgumentException if the namespace is invalid
37+
*/
38+
public static void validateNamespace(String namespace) {
39+
if (namespace == null || namespace.trim().isEmpty()) {
40+
throw new IllegalArgumentException("Namespace must be specified before flushing metrics");
41+
}
42+
43+
if (namespace.length() > MAX_NAMESPACE_LENGTH) {
44+
throw new IllegalArgumentException(
45+
"Namespace exceeds maximum length of " + MAX_NAMESPACE_LENGTH + ": " + namespace);
46+
}
47+
48+
if (!namespace.matches(NAMESPACE_REGEX)) {
49+
throw new IllegalArgumentException("Namespace contains invalid characters: " + namespace);
50+
}
51+
}
52+
53+
/**
54+
* Validates a dimension key-value pair.
55+
*
56+
* @param key The dimension key to validate
57+
* @param value The dimension value to validate
58+
* @throws IllegalArgumentException if the key or value is invalid
59+
*/
60+
public static void validateDimension(String key, String value) {
61+
if (key == null || key.trim().isEmpty()) {
62+
throw new IllegalArgumentException("Dimension key cannot be null or empty");
63+
}
64+
65+
if (value == null || value.trim().isEmpty()) {
66+
throw new IllegalArgumentException("Dimension value cannot be null or empty");
67+
}
68+
69+
if (StringUtils.containsWhitespace(key)) {
70+
throw new IllegalArgumentException("Dimension key cannot contain whitespaces: " + key);
71+
}
72+
73+
if (StringUtils.containsWhitespace(value)) {
74+
throw new IllegalArgumentException("Dimension value cannot contain whitespaces: " + value);
75+
}
76+
77+
if (key.startsWith(":")) {
78+
throw new IllegalArgumentException("Dimension key cannot start with colon: " + key);
79+
}
80+
81+
if (key.length() > MAX_DIMENSION_NAME_LENGTH) {
82+
throw new IllegalArgumentException(
83+
"Dimension name exceeds maximum length of " + MAX_DIMENSION_NAME_LENGTH + ": " + key);
84+
}
85+
86+
if (value.length() > MAX_DIMENSION_VALUE_LENGTH) {
87+
throw new IllegalArgumentException(
88+
"Dimension value exceeds maximum length of " + MAX_DIMENSION_VALUE_LENGTH + ": " + value);
89+
}
90+
91+
if (!StringUtils.isAsciiPrintable(key)) {
92+
throw new IllegalArgumentException("Dimension name has invalid characters: " + key);
93+
}
94+
95+
if (!StringUtils.isAsciiPrintable(value)) {
96+
throw new IllegalArgumentException("Dimension value has invalid characters: " + value);
97+
}
98+
}
99+
}

powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,13 @@
1818
import java.util.Map;
1919
import java.util.Set;
2020

21-
import org.apache.commons.lang3.StringUtils;
21+
import software.amazon.lambda.powertools.metrics.internal.Validator;
2222

2323
/**
2424
* Represents a set of dimensions for CloudWatch metrics
2525
*/
2626
public class DimensionSet {
2727
private static final int MAX_DIMENSION_SET_SIZE = 30;
28-
private static final int MAX_DIMENSION_NAME_LENGTH = 250;
29-
private static final int MAX_DIMENSION_VALUE_LENGTH = 1024;
3028

3129
private final Map<String, String> dimensions = new LinkedHashMap<>();
3230

@@ -190,42 +188,6 @@ public Map<String, String> getDimensions() {
190188
}
191189

192190
private void validateDimension(String key, String value) {
193-
if (key == null || key.trim().isEmpty()) {
194-
throw new IllegalArgumentException("Dimension key cannot be null or empty");
195-
}
196-
197-
if (value == null || value.trim().isEmpty()) {
198-
throw new IllegalArgumentException("Dimension value cannot be null or empty");
199-
}
200-
201-
if (StringUtils.containsWhitespace(key)) {
202-
throw new IllegalArgumentException("Dimension key cannot contain whitespaces: " + key);
203-
}
204-
205-
if (StringUtils.containsWhitespace(value)) {
206-
throw new IllegalArgumentException("Dimension value cannot contain whitespaces: " + value);
207-
}
208-
209-
if (key.startsWith(":")) {
210-
throw new IllegalArgumentException("Dimension key cannot start with colon: " + key);
211-
}
212-
213-
if (key.length() > MAX_DIMENSION_NAME_LENGTH) {
214-
throw new IllegalArgumentException(
215-
"Dimension name exceeds maximum length of " + MAX_DIMENSION_NAME_LENGTH + ": " + key);
216-
}
217-
218-
if (value.length() > MAX_DIMENSION_VALUE_LENGTH) {
219-
throw new IllegalArgumentException(
220-
"Dimension value exceeds maximum length of " + MAX_DIMENSION_VALUE_LENGTH + ": " + value);
221-
}
222-
223-
if (!StringUtils.isAsciiPrintable(key)) {
224-
throw new IllegalArgumentException("Dimension name has invalid characters: " + key);
225-
}
226-
227-
if (!StringUtils.isAsciiPrintable(value)) {
228-
throw new IllegalArgumentException("Dimension value has invalid characters: " + value);
229-
}
191+
Validator.validateDimension(key, value);
230192
}
231193
}

powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ void environmentVariablesShouldBeUsedWhenNoOverrides() throws Exception {
161161
@Test
162162
void shouldUseDefaultsWhenNoConfiguration() throws Exception {
163163
// Given
164+
MetricsLoggerBuilder.builder()
165+
.withNamespace("TestNamespace")
166+
.build();
167+
164168
RequestHandler<Map<String, Object>, String> handler = new HandlerWithDefaultMetricsAnnotation();
165169
Context context = new TestContext();
166170
Map<String, Object> input = new HashMap<>();
@@ -174,7 +178,7 @@ void shouldUseDefaultsWhenNoConfiguration() throws Exception {
174178

175179
// Default values should be used
176180
assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText())
177-
.isEqualTo("aws-embedded-metrics");
181+
.isEqualTo("TestNamespace");
178182
assertThat(rootNode.has("Service")).isTrue();
179183
assertThat(rootNode.get("Service").asText()).isEqualTo("service_undefined");
180184
}

powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ void shouldBuildWithCustomService() throws Exception {
8181
// When
8282
MetricsLogger metricsLogger = MetricsLoggerBuilder.builder()
8383
.withService("CustomService")
84+
.withNamespace("TestNamespace")
8485
.build();
8586

8687
metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT);
@@ -99,6 +100,7 @@ void shouldBuildWithRaiseOnEmptyMetrics() {
99100
// When
100101
MetricsLogger metricsLogger = MetricsLoggerBuilder.builder()
101102
.withRaiseOnEmptyMetrics(true)
103+
.withNamespace("TestNamespace")
102104
.build();
103105

104106
// Then
@@ -113,6 +115,7 @@ void shouldBuildWithDefaultDimension() throws Exception {
113115
// When
114116
MetricsLogger metricsLogger = MetricsLoggerBuilder.builder()
115117
.withDefaultDimension("Environment", "Test")
118+
.withNamespace("TestNamespace")
116119
.build();
117120

118121
metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT);
@@ -131,6 +134,7 @@ void shouldBuildWithMultipleDefaultDimensions() throws Exception {
131134
// When
132135
MetricsLogger metricsLogger = MetricsLoggerBuilder.builder()
133136
.withDefaultDimensions(DimensionSet.of("Environment", "Test", "Region", "us-west-2"))
137+
.withNamespace("TestNamespace")
134138
.build();
135139

136140
metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT);
@@ -166,6 +170,7 @@ void shouldOverrideServiceWithDefaultDimensions() throws Exception {
166170
MetricsLogger metricsLogger = MetricsLoggerBuilder.builder()
167171
.withService("OriginalService")
168172
.withDefaultDimensions(DimensionSet.of("Service", "OverriddenService"))
173+
.withNamespace("TestNamespace")
169174
.build();
170175

171176
metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT);

powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ void shouldUseNamespaceFromEnvironmentVariable() throws Exception {
113113
void shouldUseServiceNameFromEnvironmentVariable() throws Exception {
114114
// When
115115
MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger();
116+
metricsLogger.setNamespace("TestNamespace");
116117
metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT);
117118
metricsLogger.flush();
118119

powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ void setUp() throws Exception {
6565
coldStartField.set(null, null);
6666

6767
metricsLogger = MetricsLoggerFactory.getMetricsLogger();
68+
metricsLogger.setNamespace("TestNamespace");
6869
System.setOut(new PrintStream(outputStreamCaptor));
6970
}
7071

powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public String handleRequest(Map<String, Object> input, Context context) {
252252

253253
static class HandlerWithColdStartMetricsAnnotation implements RequestHandler<Map<String, Object>, String> {
254254
@Override
255-
@Metrics(captureColdStart = true)
255+
@Metrics(captureColdStart = true, namespace = "TestNamespace")
256256
public String handleRequest(Map<String, Object> input, Context context) {
257257
MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger();
258258
metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT);
@@ -262,7 +262,7 @@ public String handleRequest(Map<String, Object> input, Context context) {
262262

263263
static class HandlerWithCustomFunctionName implements RequestHandler<Map<String, Object>, String> {
264264
@Override
265-
@Metrics(captureColdStart = true, functionName = "CustomFunction")
265+
@Metrics(captureColdStart = true, functionName = "CustomFunction", namespace = "TestNamespace")
266266
public String handleRequest(Map<String, Object> input, Context context) {
267267
MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger();
268268
metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT);
@@ -272,7 +272,7 @@ public String handleRequest(Map<String, Object> input, Context context) {
272272

273273
static class HandlerWithServiceNameAndColdStart implements RequestHandler<Map<String, Object>, String> {
274274
@Override
275-
@Metrics(service = "CustomService", captureColdStart = true)
275+
@Metrics(service = "CustomService", captureColdStart = true, namespace = "TestNamespace")
276276
public String handleRequest(Map<String, Object> input, Context context) {
277277
MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger();
278278
metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT);

0 commit comments

Comments
 (0)