Skip to content
Merged
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
64 changes: 48 additions & 16 deletions docs/core/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ For most use-cases, we recommend using Environment variables and only overwrite
Type: AWS::Serverless::Function
Properties:
...
Runtime: java8
Runtime: java11
Environment:
Variables:
POWERTOOLS_SERVICE_NAME: payment
Expand Down Expand Up @@ -558,9 +558,24 @@ If you would like to suppress metrics output during your unit tests, you can use

When unit testing your code, you can run assertions against the output generated by the `Metrics` Singleton. For the `EmfMetricsLogger`, you can assert the generated JSON blob following the [CloudWatch EMF specification](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html) against your expected output.

Make sure to set a test metrics namespace and service name to run assertions against metrics. For example, by setting the following environment variables in your tests:

```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<environmentVariables>
<POWERTOOLS_SERVICE_NAME>TestService</POWERTOOLS_SERVICE_NAME>
<POWERTOOLS_METRICS_NAMESPACE>TestNamespace</POWERTOOLS_METRICS_NAMESPACE>
</environmentVariables>
</configuration>
</plugin>
```

Consider the following example where we redirect the standard output to a custom `PrintStream`. We use the Jackson library to parse the EMF output into a `JsonNode` and run assertions against that.

```java hl_lines="23 28 33 50-55"
```java hl_lines="35 40 56-72"
import static org.assertj.core.api.Assertions.assertThat;

import java.io.ByteArrayOutputStream;
Expand All @@ -571,56 +586,73 @@ import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import software.amazon.lambda.powertools.metrics.model.MetricUnit;
import software.amazon.lambda.powertools.metrics.testutils.TestContext;

@ExtendWith(MockitoExtension.class)
class MetricsTestExample {

@Mock
Context lambdaContext;

private final PrintStream standardOut = System.out;
private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
private ByteArrayOutputStream outputStreamCaptor;
private final ObjectMapper objectMapper = new ObjectMapper();

@BeforeEach
void setUp() {
outputStreamCaptor = new ByteArrayOutputStream();
System.setOut(new PrintStream(outputStreamCaptor));
}

@AfterEach
void tearDown() {
void tearDown() throws Exception {
System.setOut(standardOut);
}

@Test
void shouldCaptureMetricsFromAnnotatedHandler() throws Exception {
// Given
RequestHandler<Map<String, Object>, String> handler = new HandlerWithMetricsAnnotation();
Context context = new TestContext();
Map<String, Object> input = new HashMap<>();

// When
handler.handleRequest(input, context);
handler.handleRequest(input, lambdaContext);

// Then
String emfOutput = outputStreamCaptor.toString().trim();
JsonNode rootNode = objectMapper.readTree(emfOutput);

assertThat(rootNode.has("test-metric")).isTrue();
assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100.0);
assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText())
.isEqualTo("CustomNamespace");
assertThat(rootNode.has("Service")).isTrue();
assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService");
String[] jsonLines = emfOutput.split("\n");

// First JSON object should be the cold start metric
JsonNode coldStartNode = objectMapper.readTree(jsonLines[0]);
assertThat(coldStartNode.has("ColdStart")).isTrue();
assertThat(coldStartNode.get("ColdStart").asDouble()).isEqualTo(1.0);
assertThat(coldStartNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText())
.isEqualTo("TestNamespace");
assertThat(coldStartNode.has("Service")).isTrue();
assertThat(coldStartNode.get("Service").asText()).isEqualTo("TestService");

// Second JSON object should be the regular metric
JsonNode regularNode = objectMapper.readTree(jsonLines[1]);
assertThat(regularNode.has("test-metric")).isTrue();
assertThat(regularNode.get("test-metric").asDouble()).isEqualTo(100.0);
assertThat(regularNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText())
.isEqualTo("TestNamespace");
assertThat(regularNode.has("Service")).isTrue();
assertThat(regularNode.get("Service").asText()).isEqualTo("TestService");
}

static class HandlerWithMetricsAnnotation implements RequestHandler<Map<String, Object>, String> {
@Override
@FlushMetrics(namespace = "CustomNamespace", service = "CustomService")
@FlushMetrics(captureColdStart = true)
public String handleRequest(Map<String, Object> input, Context context) {
Metrics metrics = MetricsFactory.getMetricsInstance();
metrics.addMetric("test-metric", 100, MetricUnit.COUNT);
Expand Down
4 changes: 2 additions & 2 deletions docs/core/tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Before your use this utility, your AWS Lambda function [must have permissions](h
Type: AWS::Serverless::Function
Properties:
...
Runtime: java8
Runtime: java11

Tracing: Active
Environment:
Expand Down Expand Up @@ -191,7 +191,7 @@ different supported `captureMode` to record response, exception or both.
Type: AWS::Serverless::Function
Properties:
...
Runtime: java8
Runtime: java11

Tracing: Active
Environment:
Expand Down
19 changes: 19 additions & 0 deletions powertools-metrics/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-common</artifactId>
Expand Down Expand Up @@ -135,6 +140,13 @@
</profile>
<profile>
<id>generate-graalvm-files</id>
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-subclass</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
Expand All @@ -154,6 +166,13 @@
</profile>
<profile>
<id>graalvm-native</id>
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-subclass</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@
emfDimensionSet.addDimension(key, val);
// Update our local copy of default dimensions
defaultDimensions.put(key, val);
} catch (Exception e) {
// Ignore dimension errors
}

Check failure on line 86 in powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java

View workflow job for this annotation

GitHub Actions / pmd_analyse

Avoid empty catch blocks

Empty Catch Block finds instances where an exception is caught, but nothing is done. In most circumstances, this swallows an exception which should either be acted on or reported. EmptyCatchBlock (Priority: 1, Ruleset: Error Prone) https://docs.pmd-code.org/snapshot/pmd_rules_java_errorprone.html#emptycatchblock
});

emfLogger.putDimensions(emfDimensionSet);
Expand All @@ -105,9 +105,9 @@
dimensions.forEach((key, value) -> {
try {
emfDimensionSet.addDimension(key, value);
} catch (Exception e) {
// Ignore dimension errors
}

Check failure on line 110 in powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java

View workflow job for this annotation

GitHub Actions / pmd_analyse

Avoid empty catch blocks

Empty Catch Block finds instances where an exception is caught, but nothing is done. In most circumstances, this swallows an exception which should either be acted on or reported. EmptyCatchBlock (Priority: 1, Ruleset: Error Prone) https://docs.pmd-code.org/snapshot/pmd_rules_java_errorprone.html#emptycatchblock
});
emfLogger.setDimensions(emfDimensionSet);
// Store a copy of the default dimensions
Expand Down Expand Up @@ -196,15 +196,15 @@
dimensions.getDimensions().forEach((key, val) -> {
try {
emfDimensionSet.addDimension(key, val);
} catch (Exception e) {
// Ignore dimension errors
}

Check failure on line 201 in powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java

View workflow job for this annotation

GitHub Actions / pmd_analyse

Avoid empty catch blocks

Empty Catch Block finds instances where an exception is caught, but nothing is done. In most circumstances, this swallows an exception which should either be acted on or reported. EmptyCatchBlock (Priority: 1, Ruleset: Error Prone) https://docs.pmd-code.org/snapshot/pmd_rules_java_errorprone.html#emptycatchblock
});
coldStartLogger.setDimensions(emfDimensionSet);
}

// Add request ID from context if available
if (context != null) {
if (context != null && context.getAwsRequestId() != null) {
coldStartLogger.putProperty(REQUEST_ID_PROPERTY, context.getAwsRequestId());
}

Expand Down Expand Up @@ -249,9 +249,9 @@
dimensions.getDimensions().forEach((key, val) -> {
try {
emfDimensionSet.addDimension(key, val);
} catch (Exception e) {
// Ignore dimension errors
}

Check failure on line 254 in powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java

View workflow job for this annotation

GitHub Actions / pmd_analyse

Avoid empty catch blocks

Empty Catch Block finds instances where an exception is caught, but nothing is done. In most circumstances, this swallows an exception which should either be acted on or reported. EmptyCatchBlock (Priority: 1, Ruleset: Error Prone) https://docs.pmd-code.org/snapshot/pmd_rules_java_errorprone.html#emptycatchblock
});
singleMetricLogger.setDimensions(emfDimensionSet);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetr
}

Metrics metricsInstance = MetricsFactory.getMetricsInstance();
metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId());
// This can be null e.g. during unit tests when mocking the Lambda context
if (extractedContext.getAwsRequestId() != null) {
metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId());
}

// Only capture cold start metrics if enabled on annotation
if (metrics.captureColdStart()) {
Expand All @@ -133,8 +136,9 @@ private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetr
}

// Add function name
coldStartDimensions.addDimension("FunctionName",
funcName != null ? funcName : extractedContext.getFunctionName());
if (funcName != null) {
coldStartDimensions.addDimension("FunctionName", funcName);
}

metricsInstance.captureColdStartMetric(extractedContext, coldStartDimensions);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package software.amazon.lambda.powertools.metrics;

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

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.SetEnvironmentVariable;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
import software.amazon.lambda.powertools.metrics.model.MetricUnit;

@ExtendWith(MockitoExtension.class)
class RequestHandlerTest {

// For developer convenience, no exceptions should be thrown when using a plain Lambda Context mock
@Mock
Context lambdaContext;

private static final PrintStream STDOUT = System.out;
private ByteArrayOutputStream outputStreamCaptor;
private final ObjectMapper objectMapper = new ObjectMapper();

@BeforeEach
void setUp() throws Exception {
outputStreamCaptor = new ByteArrayOutputStream();
System.setOut(new PrintStream(outputStreamCaptor));

// Reset LambdaHandlerProcessor's SERVICE_NAME
Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName");
resetServiceName.setAccessible(true);
resetServiceName.invoke(null);

// Reset IS_COLD_START
java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START");
coldStartField.setAccessible(true);
coldStartField.set(null, null);
}

@AfterEach
void tearDown() throws Exception {
System.setOut(STDOUT);

// Reset the singleton state between tests
java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics");
field.setAccessible(true);
field.set(null, null);

field = MetricsFactory.class.getDeclaredField("provider");
field.setAccessible(true);
field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider());
}

@Test
@SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "TestNamespace")
@SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "TestService")
void shouldCaptureMetricsFromAnnotatedHandler() throws Exception {
// Given
RequestHandler<Map<String, Object>, String> handler = new HandlerWithMetricsAnnotation();
Map<String, Object> input = new HashMap<>();

// When
handler.handleRequest(input, lambdaContext);

// Then
String emfOutput = outputStreamCaptor.toString().trim();
String[] jsonLines = emfOutput.split("\n");

// First JSON object should be the cold start metric
JsonNode coldStartNode = objectMapper.readTree(jsonLines[0]);
assertThat(coldStartNode.has("ColdStart")).isTrue();
assertThat(coldStartNode.get("ColdStart").asDouble()).isEqualTo(1.0);
assertThat(coldStartNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText())
.isEqualTo("TestNamespace");
assertThat(coldStartNode.has("Service")).isTrue();
assertThat(coldStartNode.get("Service").asText()).isEqualTo("TestService");

// Second JSON object should be the regular metric
JsonNode regularNode = objectMapper.readTree(jsonLines[1]);
assertThat(regularNode.has("test-metric")).isTrue();
assertThat(regularNode.get("test-metric").asDouble()).isEqualTo(100.0);
assertThat(regularNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText())
.isEqualTo("TestNamespace");
assertThat(regularNode.has("Service")).isTrue();
assertThat(regularNode.get("Service").asText()).isEqualTo("TestService");
}

@SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true")
@Test
void shouldNotCaptureMetricsWhenDisabled() {
// Given
RequestHandler<Map<String, Object>, String> handler = new HandlerWithMetricsAnnotation();
Map<String, Object> input = new HashMap<>();

// When
handler.handleRequest(input, lambdaContext);

// Then
String emfOutput = outputStreamCaptor.toString().trim();
assertThat(emfOutput).isEmpty();
}

static class HandlerWithMetricsAnnotation implements RequestHandler<Map<String, Object>, String> {
@Override
@FlushMetrics(captureColdStart = true)
public String handleRequest(Map<String, Object> input, Context context) {
Metrics metrics = MetricsFactory.getMetricsInstance();
metrics.addMetric("test-metric", 100, MetricUnit.COUNT);
return "OK";
}
}
}
Loading