diff --git a/docs/core/metrics.md b/docs/core/metrics.md index e06ab6d10..165c26e1c 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -28,9 +28,7 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar ## Install - Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. - -=== "Maven Java 11+" +=== "Maven" ```xml hl_lines="3-7 16 18 24-27" @@ -75,52 +73,7 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar ``` -=== "Maven Java 1.8" - - ```xml hl_lines="3-7 16 18 24-27" - - ... - - software.amazon.lambda - powertools-metrics - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - - - - - ... - - - ``` - -=== "Gradle Java 11+" +=== "Gradle" ```groovy hl_lines="3 11" plugins { @@ -140,34 +93,14 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar targetCompatibility = 11 ``` -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - ## Getting started Metric has two global settings that will be used across all metrics emitted: -Setting | Description | Environment variable | Constructor parameter -------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- -**Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` -**Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` +| Setting | Description | Environment variable | Constructor parameter | +|----------------------|---------------------------------------------------------------------------------|--------------------------------|-----------------------| +| **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | +| **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` | !!! tip "Use your application or main service as the metric namespace to easily group all metrics" @@ -198,7 +131,7 @@ Setting | Description | Environment variable | Constructor parameter @Override @Metrics(namespace = "ExampleApplication", service = "booking") public Object handleRequest(Object input, Context context) { - ... + // ... } } ``` @@ -224,7 +157,7 @@ You can create metrics using `putMetric`, and manually create dimensions for all public Object handleRequest(Object input, Context context) { metricsLogger.putDimensions(DimensionSet.of("environment", "prod")); metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT); - ... + // ... } } ``` @@ -234,6 +167,35 @@ You can create metrics using `putMetric`, and manually create dimensions for all !!! note "Metrics overflow" CloudWatch EMF supports a max of 100 metrics. Metrics utility will flush all metrics when adding the 100th metric while subsequent metrics will be aggregated into a new EMF object, for your convenience. + +### Adding high-resolution metrics + +You can create [high-resolution metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html#high-resolution-metrics) +passing a `storageResolution` to the `putMetric` method: + +=== "HigResMetricsHandler.java" + + ```java hl_lines="3 13" + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; + import software.amazon.cloudwatchlogs.emf.model.StorageResolution; + + public class MetricsEnabledHandler implements RequestHandler { + + MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); + + @Override + @Metrics(namespace = "ExampleApplication", service = "booking") + public Object handleRequest(Object input, Context context) { + // ... + metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT, StorageResolution.HIGH); + } + } + ``` + +!!! info "When is it useful?" + High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others. + ### Flushing metrics The `@Metrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java index b1a701b8f..e7c410042 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java @@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +import software.amazon.cloudwatchlogs.emf.model.StorageResolution; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.Metrics; @@ -64,6 +65,8 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); }); + metricsLogger().putMetric("CustomMetric3", 1, Unit.COUNT, StorageResolution.HIGH); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() diff --git a/pom.xml b/pom.xml index d96c51d2f..2ca3f0622 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ 3.3.0 3.2.4 5.10.2 - 1.0.6 + 4.1.2 0.6.0 1.6.0 diff --git a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index d9cf575c3..a86e515f7 100644 --- a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -18,9 +18,15 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +import software.amazon.cloudwatchlogs.emf.model.StorageResolution; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.MetricsUtils; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.time.Instant; + public class Function implements RequestHandler { @@ -29,11 +35,17 @@ public class Function implements RequestHandler { @Metrics(captureColdStart = true) public String handleRequest(Input input, Context context) { + Instant currentTimeTruncatedPlusThirty = + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).toInstant(ZoneOffset.UTC).plusSeconds(30); + metricsLogger.setTimestamp(currentTimeTruncatedPlusThirty); + DimensionSet dimensionSet = new DimensionSet(); input.getDimensions().forEach((key, value) -> dimensionSet.addDimension(key, value)); metricsLogger.putDimensions(dimensionSet); - input.getMetrics().forEach((key, value) -> metricsLogger.putMetric(key, value, Unit.COUNT)); + input.getMetrics().forEach((key, value) -> metricsLogger.putMetric(key, value, Unit.COUNT, + input.getHighResolution().equalsIgnoreCase("true") ? StorageResolution.HIGH : + StorageResolution.STANDARD)); return "OK"; } diff --git a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java index 18c4eb747..1328ded77 100644 --- a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java +++ b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java @@ -21,6 +21,8 @@ public class Input { private Map dimensions; + private String highResolution; + public Input() { } @@ -32,6 +34,14 @@ public void setMetrics(Map metrics) { this.metrics = metrics; } + public String getHighResolution() { + return highResolution; + } + + public void setHighResolution(String highResolution) { + this.highResolution = highResolution; + } + public Map getDimensions() { return dimensions; } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java index 80673b995..235255dff 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java @@ -18,6 +18,10 @@ import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.List; import java.util.Map; @@ -66,12 +70,20 @@ public static void tearDown() { @Test public void test_recordMetrics() { // GIVEN + + Instant currentTimeTruncatedToMinutes = + LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).toInstant(ZoneOffset.UTC); + String event1 = - "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"} }"; + "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"false\"}"; + String event2 = + "{ \"metrics\": {\"orders\": 1, \"products\": 8}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"true\"}"; // WHEN InvocationResult invocationResult = invokeFunction(functionName, event1); + invokeFunction(functionName, event2); + // THEN MetricsFetcher metricsFetcher = new MetricsFetcher(); List coldStart = @@ -84,18 +96,35 @@ public void test_recordMetrics() { List orderMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "orders", Collections.singletonMap("Environment", "test")); - assertThat(orderMetrics.get(0)).isEqualTo(1); + assertThat(orderMetrics.get(0)).isEqualTo(2); List productMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "products", Collections.singletonMap("Environment", "test")); - assertThat(productMetrics.get(0)).isEqualTo(4); + + // When searching across a 1 minute time period with a period of 60 we find both metrics and the sum is 12 + + assertThat(productMetrics.get(0)).isEqualTo(12); + orderMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "orders", Collections.singletonMap("Service", service)); - assertThat(orderMetrics.get(0)).isEqualTo(1); + assertThat(orderMetrics.get(0)).isEqualTo(2); productMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, "products", Collections.singletonMap("Service", service)); - assertThat(productMetrics.get(0)).isEqualTo(4); + assertThat(productMetrics.get(0)).isEqualTo(12); + + Instant searchStartTime = currentTimeTruncatedToMinutes.plusSeconds(15); + Instant searchEndTime = currentTimeTruncatedToMinutes.plusSeconds(45); + + List productMetricDataResult = + metricsFetcher.fetchMetrics(searchStartTime, searchEndTime, 1, namespace, + "products", Collections.singletonMap("Environment", "test")); + +// We are searching across the time period the metric was created but with a period of 1 second. Only the high resolution metric will be available at this point + + assertThat(productMetricDataResult.get(0)).isEqualTo(8); + + } } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java index 0402e3cc5..26ae41a00 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java @@ -25,6 +25,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; @@ -35,6 +37,7 @@ import software.amazon.cloudwatchlogs.emf.config.SystemWrapper; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +import software.amazon.cloudwatchlogs.emf.model.StorageResolution; import software.amazon.cloudwatchlogs.emf.model.Unit; class MetricsLoggerTest { @@ -245,6 +248,7 @@ private void testLogger(Consumer> methodToTest) { { metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1")); metricsLogger.putMetric("Metric1", 1, Unit.COUNT); + metricsLogger.putMetric("Metric2", 1, Unit.COUNT, StorageResolution.HIGH); }); assertThat(out.toString()) @@ -263,6 +267,13 @@ private void testLogger(Consumer> methodToTest) { assertThat(aws.get("CloudWatchMetrics")) .asString() .contains("Namespace=GlobalName"); + + ArrayList cloudWatchMetrics = (ArrayList) aws.get("CloudWatchMetrics"); + LinkedHashMap values = + (java.util.LinkedHashMap) cloudWatchMetrics.get(0); + ArrayList metricArray = (ArrayList) values.get("Metrics"); + LinkedHashMap metricValues = (LinkedHashMap) metricArray.get(1); + assertThat(metricValues).containsEntry("StorageResolution", 1); }); } } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java index bc8a6e949..fd406b9cd 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java @@ -26,13 +26,14 @@ public class PowertoolsMetricsTooManyDimensionsHandler implements RequestHandler { @Override - @Metrics + @Metrics(namespace = "ExampleApplication",service = "booking") public Object handleRequest(Object input, Context context) { MetricsLogger metricsLogger = metricsLogger(); - - metricsLogger.setDimensions(IntStream.range(1, 15) - .mapToObj(value -> DimensionSet.of("Dimension" + value, "DimensionValue" + value)) - .toArray(DimensionSet[]::new)); + DimensionSet dimensionSet = new DimensionSet(); + for (int i = 0; i < 35; i++) { + dimensionSet.addDimension("Dimension" + i, "value" + i); + } + metricsLogger.setDimensions(dimensionSet); return null; } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java index 81e10ed22..d27af1fdf 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java @@ -40,6 +40,7 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import software.amazon.cloudwatchlogs.emf.config.SystemWrapper; +import software.amazon.cloudwatchlogs.emf.exception.DimensionSetExceededException; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.MetricsUtils; import software.amazon.lambda.powertools.metrics.ValidationException; @@ -389,9 +390,10 @@ public void exceptionWhenTooManyDimensionsSet() { requestHandler = new PowertoolsMetricsTooManyDimensionsHandler(); - assertThatExceptionOfType(ValidationException.class) + assertThatExceptionOfType(DimensionSetExceededException.class) .isThrownBy(() -> requestHandler.handleRequest("input", context)) - .withMessage("Number of Dimensions must be in range of 0-9. Actual size: 14."); + .withMessage( + "Maximum number of dimensions allowed are 30. Account for default dimensions if not using setDimensions."); } } diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index ee44f7b4d..e959204ad 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -233,6 +233,10 @@ + + + +