Skip to content

feat: Metrics utility #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Sep 22, 2020
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
max-parallel: 4
matrix:
# test against latest update of each major Java version, as well as specific updates of LTS versions:
java: [8, 8.0.192, 9.0.x, 10, 11.0.x, 11.0.3, 12, 13 ]
java: [8, 8.0.192, 11.0.x, 11.0.3, 12, 13 ]
name: Java ${{ matrix.java }}
env:
OS: ${{ matrix.os }}
Expand Down
154 changes: 154 additions & 0 deletions docs/content/core/metrics.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
title: Metrics
description: Core utility
---

Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF).

These metrics can be visualized through [Amazon CloudWatch Console](https://console.aws.amazon.com/cloudwatch/).

**Key features**

* Aggregate up to 100 metrics using a single CloudWatch EMF object (large JSON blob)
* Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc)
* Metrics are created asynchronously by the CloudWatch service, no custom stacks needed
* Context manager to create a one off metric with a different dimension

## Initialization

Set `POWERTOOLS_SERVICE_NAME` and `POWERTOOLS_METRICS_NAMESPACE` env vars as a start - Here is an example using AWS Serverless Application Model (SAM)

```yaml:title=template.yaml
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
...
Runtime: java8
Environment:
Variables:
POWERTOOLS_SERVICE_NAME: payment # highlight-line
POWERTOOLS_METRICS_NAMESPACE: ServerlessAirline # highlight-line
```

We recommend you use your application or main service as a metric namespace.
You can explicitly set a namespace name an annotation variable `namespace` param or via `POWERTOOLS_METRICS_NAMESPACE` env var.

This sets **namespace** key that will be used for all metrics.
You can also pass a service name via `service` param or `POWERTOOLS_SERVICE_NAME` env var. This will create a dimension with the service name.

```java:title=Handler.java
package example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
import software.amazon.cloudwatchlogs.emf.model.Unit;
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
import software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger;

public class PowertoolsMetricsEnabledHandler implements RequestHandler<Object, Object> {

MetricsLogger metricsLogger = PowertoolsMetricsLogger.metricsLogger();

@Override
@PowertoolsMetrics(namespace = "ExampleApplication", service = "booking")
public Object handleRequest(Object input, Context context) {
...
}
}
```

You can initialize Metrics anywhere in your code as many times as you need - It'll keep track of your aggregate metrics in memory.

## Creating metrics

You can create metrics using `putMetric`, and manually create dimensions for all your aggregate metrics using `add_dimension`.

```java:title=app.py
public class PowertoolsMetricsEnabledHandler implements RequestHandler<Object, Object> {

MetricsLogger metricsLogger = PowertoolsMetricsLogger.metricsLogger();

@Override
@PowertoolsMetrics(namespace = "ExampleApplication", service = "booking")
public Object handleRequest(Object input, Context context) {
# highlight-start
metricsLogger.putDimensions(DimensionSet.of("environment", "prod"));
metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT);
# highlight-end
...
}
}
```

The `Unit` enum facilitate finding a supported metric unit by CloudWatch.

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.

## Creating a metric with a different dimension

CloudWatch EMF uses the same dimensions across all your metrics. Use `single_metric` if you have a metric that should have different dimensions.

<Note type="info">
Generally, this would be an edge case since you <a href="https://aws.amazon.com/cloudwatch/pricing/">pay for unique metric</a>. Keep the following formula in mind:
<br/><br/>
<strong>unique metric = (metric_name + dimension_name + dimension_value)</strong>
</Note><br/>

```java:title=Handler.java
withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> {
metric.setDimensions(DimensionSet.of("AnotherService", "CustomService"));
});
```

## Adding metadata

You can use `putMetadata` for advanced use cases, where you want to metadata as part of the serialized metrics object.

<Note type="info">
<strong>This will not be available during metrics visualization</strong> - Use <strong>dimensions</strong> for this purpose
</Note><br/>

```javv:title=Handler.java
@PowertoolsMetrics(namespace = "ServerlessAirline", service = "payment")
public APIGatewayProxyResponseEvent handleRequest(Object input, Context context) {
metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT);
metricsLogger().putMetadata("booking_id", "1234567890"); # highlight-line

...
```

This will be available in CloudWatch Logs to ease operations on high cardinal data.

The `@PowertoolsMetrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, if no metrics are provided no exception will be raised.

If metrics are provided, and any of the following criteria are not met, `ValidationException` exception will be raised:

* Minimum of 1 dimension
* Maximum of 9 dimensions

If you want to ensure that at least one metric is emitted, you can pass `raiseOnEmptyMetrics = true` to the **@PowertoolsMetrics** annotation:

```java:title=Handler.java
@PowertoolsMetrics(raiseOnEmptyMetrics = true)
public Object handleRequest(Object input, Context context) {
...
```

## Capturing cold start metric

You can capture cold start metrics automatically with `@PowertoolsMetrics` via the `captureColdStart` variable.

```java:title=Handler.java
@PowertoolsMetrics(captureColdStart = true)
public Object handleRequest(Object input, Context context) {
...
```

If it's a cold start invocation, this feature will:

* Create a separate EMF blob solely containing a metric named `ColdStart`
* Add `FunctionName` and `Service` dimensions

This has the advantage of keeping cold start metric separate from your application metrics.
3 changes: 2 additions & 1 deletion docs/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ module.exports = {
],
'Core utilities': [
'core/logging',
'core/tracing'
'core/tracing',
'core/metrics'
],
'Utilities': [
'utilities/sqs_large_message_handling'
Expand Down
9 changes: 9 additions & 0 deletions example/HelloWorldFunction/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
<artifactId>powertools-logging</artifactId>
<version>0.2.0-beta</version>
</dependency>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-metrics</artifactId>
<version>0.2.0-beta</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
Expand Down Expand Up @@ -76,6 +81,10 @@
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-logging</artifactId>
</aspectLibrary>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-metrics</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
Expand Down
29 changes: 21 additions & 8 deletions example/HelloWorldFunction/src/main/java/helloworld/App.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package helloworld;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
Expand All @@ -8,19 +16,16 @@
import com.amazonaws.xray.entities.Entity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import software.amazon.cloudwatchlogs.emf.model.DimensionSet;
import software.amazon.cloudwatchlogs.emf.model.Unit;
import software.amazon.lambda.powertools.logging.PowertoolsLogger;
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
import software.amazon.lambda.powertools.tracing.PowerTracer;
import software.amazon.lambda.powertools.tracing.PowertoolsTracing;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.withSingleMetric;
import static software.amazon.lambda.powertools.tracing.PowerTracer.putMetadata;
import static software.amazon.lambda.powertools.tracing.PowerTracer.withEntitySubsegment;

Expand All @@ -33,12 +38,20 @@ public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatew

@PowertoolsLogging(logEvent = true, samplingRate = 0.7)
@PowertoolsTracing(captureError = false, captureResponse = false)
@PowertoolsMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
Map<String, String> headers = new HashMap<>();

headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");

metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT);

withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> {
metric.setDimensions(DimensionSet.of("AnotherService", "CustomService"));
metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1"));
});

PowertoolsLogger.appendKey("test", "willBeLogged");

APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package helloworld;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.lambda.powertools.logging.PowertoolsLogging;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;

public class AppStream implements RequestStreamHandler {
private static final ObjectMapper mapper = new ObjectMapper();

@Override
@PowertoolsLogging(logEvent = true)
@PowertoolsMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true)
public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException {
Map map = mapper.readValue(input, Map.class);

Expand Down
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<module>powertools-logging</module>
<module>powertools-tracing</module>
<module>powertools-sqs</module>
<module>powertools-metrics</module>
</modules>

<scm>
Expand Down Expand Up @@ -67,6 +68,7 @@
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
<maven-gpg-plugin.version>1.6</maven-gpg-plugin.version>
<junit-jupiter.version>5.7.0</junit-jupiter.version>
<aws-embedded-metrics.version>1.0.0</aws-embedded-metrics.version>
</properties>

<distributionManagement>
Expand Down Expand Up @@ -143,6 +145,11 @@
<artifactId>aws-xray-recorder-sdk-aws-sdk-v2-instrumentor</artifactId>
<version>${aws.xray.recorder.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.cloudwatchlogs</groupId>
<artifactId>aws-embedded-metrics</artifactId>
<version>${aws-embedded-metrics.version}</version>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down Expand Up @@ -175,6 +182,12 @@
<version>3.5.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.5.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@

import java.io.InputStream;
import java.io.OutputStream;
import java.util.Optional;

import static java.util.Optional.empty;
import static java.util.Optional.of;

public final class LambdaHandlerProcessor {
private static String SERVICE_NAME = null != System.getenv("POWERTOOLS_SERVICE_NAME")
Expand Down Expand Up @@ -48,12 +52,27 @@ public static boolean placedOnStreamHandler(final ProceedingJoinPoint pjp) {
&& pjp.getArgs()[2] instanceof Context;
}

public static Optional<Context> extractContext(final ProceedingJoinPoint pjp) {

if (isHandlerMethod(pjp)) {
if (placedOnRequestHandler(pjp)) {
return of((Context) pjp.getArgs()[1]);
}

if (placedOnStreamHandler(pjp)) {
return of((Context) pjp.getArgs()[2]);
}
}

return empty();
}

public static String serviceName() {
return SERVICE_NAME;
}

public static Boolean isColdStart() {
return IS_COLD_START;
public static boolean isColdStart() {
return IS_COLD_START == null;
}

public static void coldStartDone() {
Expand Down
Loading