Skip to content

Commit c3120b5

Browse files
author
Pankaj Agrawal
committed
feat: Raise error on empty metrics and validations
1 parent 99e1190 commit c3120b5

File tree

11 files changed

+239
-7
lines changed

11 files changed

+239
-7
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
6969
<maven-gpg-plugin.version>1.6</maven-gpg-plugin.version>
7070
<junit-jupiter.version>5.6.2</junit-jupiter.version>
71-
<aws-embedded-metrics.version>1.0.0</aws-embedded-metrics.version>
71+
<aws-embedded-metrics.version>0.1.0-beta</aws-embedded-metrics.version>
7272
</properties>
7373

7474
<distributionManagement>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package software.amazon.cloudwatchlogs.emf.model;
2+
3+
import java.lang.reflect.Field;
4+
5+
import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
6+
7+
public final class MetricsLoggerHelper {
8+
private MetricsLoggerHelper() {
9+
}
10+
11+
public static boolean hasNoMetrics() {
12+
return metricsContext().getRootNode().getAws().isEmpty();
13+
}
14+
15+
public static long dimensionsCount() {
16+
return metricsContext().getDimensions().size();
17+
}
18+
19+
private static MetricsContext metricsContext() {
20+
try {
21+
Field f = metricsLogger().getClass().getDeclaredField("context");
22+
f.setAccessible(true);
23+
return (MetricsContext) f.get(metricsLogger());
24+
} catch (NoSuchFieldException | IllegalAccessException e) {
25+
throw new RuntimeException(e);
26+
}
27+
}
28+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
* </br>This will create a metric with the key {@code "ColdStart"} and the unit type {@code COUNT}.
2727
* </p>
2828
*
29+
* <p>To raise exception if no metrics are emitted, use {@code @PowertoolsMetrics(raiseOnEmptyMetrics = true)}.
30+
* </br>This will create a create a exception of type {@link ValidationException}. By default its value is set to false.
31+
* </p>
32+
*
2933
* <p>By default the service name associated with metrics created will be
3034
* "service_undefined". This can be overridden with the environment variable {@code POWERTOOLS_SERVICE_NAME}
3135
* or the annotation variable {@code @PowertoolsMetrics(service = "Service Name")}.
@@ -42,4 +46,5 @@
4246
String namespace() default "";
4347
String service() default "";
4448
boolean captureColdStart() default false;
49+
boolean raiseOnEmptyMetrics() default false;
4550
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package software.amazon.lambda.powertools.metrics;
22

3+
import java.util.function.Consumer;
4+
35
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
46
import software.amazon.cloudwatchlogs.emf.model.Unit;
57

6-
import java.util.function.Consumer;
7-
88
/**
99
* A class used to retrieve the instance of the {@code MetricsLogger} used by
1010
* {@code PowertoolsMetrics}.
@@ -14,6 +14,9 @@
1414
public final class PowertoolsMetricsLogger {
1515
private static final MetricsLogger metricsLogger = new MetricsLogger();
1616

17+
private PowertoolsMetricsLogger() {
18+
}
19+
1720
/**
1821
* The instance of the {@code MetricsLogger} used by {@code PowertoolsMetrics}.
1922
*
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package software.amazon.lambda.powertools.metrics;
2+
3+
public class ValidationException extends RuntimeException {
4+
5+
public ValidationException(String message) {
6+
super(message);
7+
}
8+
}

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package software.amazon.lambda.powertools.metrics.internal;
22

3+
import java.lang.reflect.Field;
34
import java.util.Optional;
45

56
import com.amazonaws.services.lambda.runtime.Context;
@@ -9,9 +10,13 @@
910
import org.aspectj.lang.annotation.Pointcut;
1011
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
1112
import software.amazon.cloudwatchlogs.emf.model.DimensionSet;
13+
import software.amazon.cloudwatchlogs.emf.model.MetricsContext;
1214
import software.amazon.cloudwatchlogs.emf.model.Unit;
1315
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
16+
import software.amazon.lambda.powertools.metrics.ValidationException;
1417

18+
import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.dimensionsCount;
19+
import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.hasNoMetrics;
1520
import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone;
1621
import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.extractContext;
1722
import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart;
@@ -47,11 +52,19 @@ public Object around(ProceedingJoinPoint pjp,
4752

4853
coldStartSingleMetricIfApplicable(pjp, powertoolsMetrics);
4954

50-
Object proceed = pjp.proceed(proceedArgs);
55+
try {
56+
Object proceed = pjp.proceed(proceedArgs);
5157

52-
coldStartDone();
53-
logger.flush();
54-
return proceed;
58+
coldStartDone();
59+
60+
validateBeforeFlushingMetrics(powertoolsMetrics);
61+
62+
logger.flush();
63+
return proceed;
64+
65+
} finally {
66+
refreshMetricsContext();
67+
}
5568
}
5669

5770
return pjp.proceed(proceedArgs);
@@ -74,11 +87,32 @@ && isColdStart()) {
7487
}
7588
}
7689

90+
private void validateBeforeFlushingMetrics(PowertoolsMetrics powertoolsMetrics) {
91+
if (powertoolsMetrics.raiseOnEmptyMetrics() && hasNoMetrics()) {
92+
throw new ValidationException("No metrics captured, at least one metrics must be emitted");
93+
}
94+
95+
if (dimensionsCount() == 0 || dimensionsCount() > 10) {
96+
throw new ValidationException(String.format("Number of Dimensions must be in range of 1-10." +
97+
" Actual size: %d.", dimensionsCount()));
98+
}
99+
}
100+
77101
private String namespace(PowertoolsMetrics powertoolsMetrics) {
78102
return !"".equals(powertoolsMetrics.namespace()) ? powertoolsMetrics.namespace() : NAMESPACE;
79103
}
80104

81105
private String service(PowertoolsMetrics powertoolsMetrics) {
82106
return !"".equals(powertoolsMetrics.service()) ? powertoolsMetrics.service() : serviceName();
83107
}
108+
109+
private static void refreshMetricsContext() {
110+
try {
111+
Field f = metricsLogger().getClass().getDeclaredField("context");
112+
f.setAccessible(true);
113+
f.set(metricsLogger(), new MetricsContext());
114+
} catch (NoSuchFieldException | IllegalAccessException e) {
115+
throw new RuntimeException(e);
116+
}
117+
}
84118
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package software.amazon.lambda.powertools.metrics.handlers;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import com.amazonaws.services.lambda.runtime.RequestHandler;
5+
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
6+
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
7+
8+
import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
9+
10+
public class PowertoolsMetricsExceptionWhenNoMetricsHandler implements RequestHandler<Object, Object> {
11+
12+
@Override
13+
@PowertoolsMetrics(namespace = "ExampleApplication", service = "booking", raiseOnEmptyMetrics = true)
14+
public Object handleRequest(Object input, Context context) {
15+
MetricsLogger metricsLogger = metricsLogger();
16+
metricsLogger.putMetadata("MetaData", "MetaDataValue");
17+
18+
return null;
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package software.amazon.lambda.powertools.metrics.handlers;
2+
3+
import java.util.function.IntConsumer;
4+
import java.util.stream.IntStream;
5+
6+
import com.amazonaws.services.lambda.runtime.Context;
7+
import com.amazonaws.services.lambda.runtime.RequestHandler;
8+
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
9+
import software.amazon.cloudwatchlogs.emf.model.DimensionSet;
10+
import software.amazon.cloudwatchlogs.emf.model.Unit;
11+
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
12+
13+
import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
14+
15+
public class PowertoolsMetricsNoDimensionsHandler implements RequestHandler<Object, Object> {
16+
17+
@Override
18+
@PowertoolsMetrics(namespace = "ExampleApplication", service = "booking", captureColdStart = true)
19+
public Object handleRequest(Object input, Context context) {
20+
MetricsLogger metricsLogger = metricsLogger();
21+
metricsLogger.setDimensions();
22+
23+
return null;
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package software.amazon.lambda.powertools.metrics.handlers;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import com.amazonaws.services.lambda.runtime.RequestHandler;
5+
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
6+
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
7+
8+
import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
9+
10+
public class PowertoolsMetricsNoExceptionWhenNoMetricsHandler implements RequestHandler<Object, Object> {
11+
12+
@Override
13+
@PowertoolsMetrics(namespace = "ExampleApplication", service = "booking")
14+
public Object handleRequest(Object input, Context context) {
15+
MetricsLogger metricsLogger = metricsLogger();
16+
metricsLogger.putMetadata("MetaData", "MetaDataValue");
17+
18+
return null;
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package software.amazon.lambda.powertools.metrics.handlers;
2+
3+
import java.util.List;
4+
import java.util.stream.Collectors;
5+
import java.util.stream.IntStream;
6+
7+
import com.amazonaws.services.lambda.runtime.Context;
8+
import com.amazonaws.services.lambda.runtime.RequestHandler;
9+
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
10+
import software.amazon.cloudwatchlogs.emf.model.DimensionSet;
11+
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
12+
13+
import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
14+
15+
public class PowertoolsMetricsTooManyDimensionsHandler implements RequestHandler<Object, Object> {
16+
17+
@Override
18+
@PowertoolsMetrics
19+
public Object handleRequest(Object input, Context context) {
20+
MetricsLogger metricsLogger = metricsLogger();
21+
22+
metricsLogger.setDimensions(IntStream.range(1, 15)
23+
.mapToObj(value -> DimensionSet.of("Dimension" + value, "DimensionValue" + value))
24+
.toArray(DimensionSet[]::new));
25+
26+
return null;
27+
}
28+
}

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@
1919
import org.mockito.MockedStatic;
2020
import software.amazon.cloudwatchlogs.emf.config.SystemWrapper;
2121
import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor;
22+
import software.amazon.lambda.powertools.metrics.ValidationException;
2223
import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsColdStartEnabledHandler;
2324
import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledHandler;
2425
import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledStreamHandler;
26+
import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsExceptionWhenNoMetricsHandler;
27+
import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsNoDimensionsHandler;
28+
import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsNoExceptionWhenNoMetricsHandler;
29+
import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsTooManyDimensionsHandler;
2530

2631
import static java.util.Collections.emptyMap;
2732
import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField;
2833
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2935
import static org.mockito.Mockito.mockStatic;
3036
import static org.mockito.Mockito.when;
3137
import static org.mockito.MockitoAnnotations.openMocks;
@@ -171,6 +177,61 @@ public void metricsWithStreamHandler() throws IOException {
171177
}
172178
}
173179

180+
@Test
181+
public void exceptionWhenNoMetricsEmitted() {
182+
requestHandler = new PowertoolsMetricsExceptionWhenNoMetricsHandler();
183+
try (MockedStatic<SystemWrapper> mocked = mockStatic(SystemWrapper.class)) {
184+
mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
185+
186+
assertThatExceptionOfType(ValidationException.class)
187+
.isThrownBy(() -> requestHandler.handleRequest("input", context))
188+
.withMessage("No metrics captured, at least one metrics must be emitted");
189+
}
190+
}
191+
192+
@Test
193+
public void noExceptionWhenNoMetricsEmitted() {
194+
requestHandler = new PowertoolsMetricsNoExceptionWhenNoMetricsHandler();
195+
196+
try (MockedStatic<SystemWrapper> mocked = mockStatic(SystemWrapper.class)) {
197+
mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
198+
requestHandler.handleRequest("input", context);
199+
200+
assertThat(out.toString())
201+
.satisfies(s -> {
202+
Map<String, Object> logAsJson = readAsJson(s);
203+
204+
assertThat(logAsJson)
205+
.containsEntry("service", "booking")
206+
.doesNotContainKey("_aws");
207+
});
208+
}
209+
}
210+
211+
@Test
212+
public void exceptionWhenNoDimensionsSet() {
213+
requestHandler = new PowertoolsMetricsNoDimensionsHandler();
214+
try (MockedStatic<SystemWrapper> mocked = mockStatic(SystemWrapper.class)) {
215+
mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
216+
217+
assertThatExceptionOfType(ValidationException.class)
218+
.isThrownBy(() -> requestHandler.handleRequest("input", context))
219+
.withMessage("Number of Dimensions must be in range of 1-10. Actual size: 0.");
220+
}
221+
}
222+
223+
@Test
224+
public void exceptionWhenTooManyDimensionsSet() {
225+
requestHandler = new PowertoolsMetricsTooManyDimensionsHandler();
226+
227+
try (MockedStatic<SystemWrapper> mocked = mockStatic(SystemWrapper.class)) {
228+
mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
229+
230+
assertThatExceptionOfType(ValidationException.class)
231+
.isThrownBy(() -> requestHandler.handleRequest("input", context))
232+
.withMessage("Number of Dimensions must be in range of 1-10. Actual size: 14.");
233+
}
234+
}
174235

175236
private void setupContext() {
176237
when(context.getFunctionName()).thenReturn("testFunction");

0 commit comments

Comments
 (0)