Skip to content

Commit a22bf2d

Browse files
feat: Metrics utility (#91)
1 parent 8ea74f8 commit a22bf2d

File tree

25 files changed

+1072
-39
lines changed

25 files changed

+1072
-39
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
max-parallel: 4
2828
matrix:
2929
# test against latest update of each major Java version, as well as specific updates of LTS versions:
30-
java: [8, 8.0.192, 9.0.x, 10, 11.0.x, 11.0.3, 12, 13 ]
30+
java: [8, 8.0.192, 11.0.x, 11.0.3, 12, 13 ]
3131
name: Java ${{ matrix.java }}
3232
env:
3333
OS: ${{ matrix.os }}

docs/content/core/metrics.mdx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
---
2+
title: Metrics
3+
description: Core utility
4+
---
5+
6+
Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF).
7+
8+
These metrics can be visualized through [Amazon CloudWatch Console](https://console.aws.amazon.com/cloudwatch/).
9+
10+
**Key features**
11+
12+
* Aggregate up to 100 metrics using a single CloudWatch EMF object (large JSON blob)
13+
* Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc)
14+
* Metrics are created asynchronously by the CloudWatch service, no custom stacks needed
15+
* Context manager to create a one off metric with a different dimension
16+
17+
## Initialization
18+
19+
Set `POWERTOOLS_SERVICE_NAME` and `POWERTOOLS_METRICS_NAMESPACE` env vars as a start - Here is an example using AWS Serverless Application Model (SAM)
20+
21+
```yaml:title=template.yaml
22+
Resources:
23+
HelloWorldFunction:
24+
Type: AWS::Serverless::Function
25+
Properties:
26+
...
27+
Runtime: java8
28+
Environment:
29+
Variables:
30+
POWERTOOLS_SERVICE_NAME: payment # highlight-line
31+
POWERTOOLS_METRICS_NAMESPACE: ServerlessAirline # highlight-line
32+
```
33+
34+
We recommend you use your application or main service as a metric namespace.
35+
You can explicitly set a namespace name an annotation variable `namespace` param or via `POWERTOOLS_METRICS_NAMESPACE` env var.
36+
37+
This sets **namespace** key that will be used for all metrics.
38+
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.
39+
40+
```java:title=Handler.java
41+
package example;
42+
43+
import com.amazonaws.services.lambda.runtime.Context;
44+
import com.amazonaws.services.lambda.runtime.RequestHandler;
45+
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
46+
import software.amazon.cloudwatchlogs.emf.model.Unit;
47+
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
48+
import software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger;
49+
50+
public class PowertoolsMetricsEnabledHandler implements RequestHandler<Object, Object> {
51+
52+
MetricsLogger metricsLogger = PowertoolsMetricsLogger.metricsLogger();
53+
54+
@Override
55+
@PowertoolsMetrics(namespace = "ExampleApplication", service = "booking")
56+
public Object handleRequest(Object input, Context context) {
57+
...
58+
}
59+
}
60+
```
61+
62+
You can initialize Metrics anywhere in your code as many times as you need - It'll keep track of your aggregate metrics in memory.
63+
64+
## Creating metrics
65+
66+
You can create metrics using `putMetric`, and manually create dimensions for all your aggregate metrics using `add_dimension`.
67+
68+
```java:title=app.py
69+
public class PowertoolsMetricsEnabledHandler implements RequestHandler<Object, Object> {
70+
71+
MetricsLogger metricsLogger = PowertoolsMetricsLogger.metricsLogger();
72+
73+
@Override
74+
@PowertoolsMetrics(namespace = "ExampleApplication", service = "booking")
75+
public Object handleRequest(Object input, Context context) {
76+
# highlight-start
77+
metricsLogger.putDimensions(DimensionSet.of("environment", "prod"));
78+
metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT);
79+
# highlight-end
80+
...
81+
}
82+
}
83+
```
84+
85+
The `Unit` enum facilitate finding a supported metric unit by CloudWatch.
86+
87+
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.
88+
89+
## Creating a metric with a different dimension
90+
91+
CloudWatch EMF uses the same dimensions across all your metrics. Use `single_metric` if you have a metric that should have different dimensions.
92+
93+
<Note type="info">
94+
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:
95+
<br/><br/>
96+
<strong>unique metric = (metric_name + dimension_name + dimension_value)</strong>
97+
</Note><br/>
98+
99+
```java:title=Handler.java
100+
withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> {
101+
metric.setDimensions(DimensionSet.of("AnotherService", "CustomService"));
102+
});
103+
```
104+
105+
## Adding metadata
106+
107+
You can use `putMetadata` for advanced use cases, where you want to metadata as part of the serialized metrics object.
108+
109+
<Note type="info">
110+
<strong>This will not be available during metrics visualization</strong> - Use <strong>dimensions</strong> for this purpose
111+
</Note><br/>
112+
113+
```javv:title=Handler.java
114+
@PowertoolsMetrics(namespace = "ServerlessAirline", service = "payment")
115+
public APIGatewayProxyResponseEvent handleRequest(Object input, Context context) {
116+
metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT);
117+
metricsLogger().putMetadata("booking_id", "1234567890"); # highlight-line
118+
119+
...
120+
```
121+
122+
This will be available in CloudWatch Logs to ease operations on high cardinal data.
123+
124+
The `@PowertoolsMetrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, if no metrics are provided no exception will be raised.
125+
126+
If metrics are provided, and any of the following criteria are not met, `ValidationException` exception will be raised:
127+
128+
* Minimum of 1 dimension
129+
* Maximum of 9 dimensions
130+
131+
If you want to ensure that at least one metric is emitted, you can pass `raiseOnEmptyMetrics = true` to the **@PowertoolsMetrics** annotation:
132+
133+
```java:title=Handler.java
134+
@PowertoolsMetrics(raiseOnEmptyMetrics = true)
135+
public Object handleRequest(Object input, Context context) {
136+
...
137+
```
138+
139+
## Capturing cold start metric
140+
141+
You can capture cold start metrics automatically with `@PowertoolsMetrics` via the `captureColdStart` variable.
142+
143+
```java:title=Handler.java
144+
@PowertoolsMetrics(captureColdStart = true)
145+
public Object handleRequest(Object input, Context context) {
146+
...
147+
```
148+
149+
If it's a cold start invocation, this feature will:
150+
151+
* Create a separate EMF blob solely containing a metric named `ColdStart`
152+
* Add `FunctionName` and `Service` dimensions
153+
154+
This has the advantage of keeping cold start metric separate from your application metrics.

docs/gatsby-config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ module.exports = {
2424
],
2525
'Core utilities': [
2626
'core/logging',
27-
'core/tracing'
27+
'core/tracing',
28+
'core/metrics'
2829
],
2930
'Utilities': [
3031
'utilities/sqs_large_message_handling'

example/HelloWorldFunction/pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
<artifactId>powertools-logging</artifactId>
2424
<version>0.2.0-beta</version>
2525
</dependency>
26+
<dependency>
27+
<groupId>software.amazon.lambda</groupId>
28+
<artifactId>powertools-metrics</artifactId>
29+
<version>0.2.0-beta</version>
30+
</dependency>
2631
<dependency>
2732
<groupId>com.amazonaws</groupId>
2833
<artifactId>aws-lambda-java-core</artifactId>
@@ -76,6 +81,10 @@
7681
<groupId>software.amazon.lambda</groupId>
7782
<artifactId>powertools-logging</artifactId>
7883
</aspectLibrary>
84+
<aspectLibrary>
85+
<groupId>software.amazon.lambda</groupId>
86+
<artifactId>powertools-metrics</artifactId>
87+
</aspectLibrary>
7988
</aspectLibraries>
8089
</configuration>
8190
<executions>

example/HelloWorldFunction/src/main/java/helloworld/App.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package helloworld;
22

3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.net.URL;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
import java.util.stream.Collectors;
10+
311
import com.amazonaws.services.lambda.runtime.Context;
412
import com.amazonaws.services.lambda.runtime.RequestHandler;
513
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
@@ -8,19 +16,16 @@
816
import com.amazonaws.xray.entities.Entity;
917
import org.apache.logging.log4j.LogManager;
1018
import org.apache.logging.log4j.Logger;
19+
import software.amazon.cloudwatchlogs.emf.model.DimensionSet;
20+
import software.amazon.cloudwatchlogs.emf.model.Unit;
1121
import software.amazon.lambda.powertools.logging.PowertoolsLogger;
1222
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
23+
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
1324
import software.amazon.lambda.powertools.tracing.PowerTracer;
1425
import software.amazon.lambda.powertools.tracing.PowertoolsTracing;
1526

16-
import java.io.BufferedReader;
17-
import java.io.IOException;
18-
import java.io.InputStreamReader;
19-
import java.net.URL;
20-
import java.util.HashMap;
21-
import java.util.Map;
22-
import java.util.stream.Collectors;
23-
27+
import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
28+
import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.withSingleMetric;
2429
import static software.amazon.lambda.powertools.tracing.PowerTracer.putMetadata;
2530
import static software.amazon.lambda.powertools.tracing.PowerTracer.withEntitySubsegment;
2631

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

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

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

48+
metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT);
49+
50+
withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> {
51+
metric.setDimensions(DimensionSet.of("AnotherService", "CustomService"));
52+
metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1"));
53+
});
54+
4255
PowertoolsLogger.appendKey("test", "willBeLogged");
4356

4457
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()

example/HelloWorldFunction/src/main/java/helloworld/AppStream.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
package helloworld;
22

3-
import com.amazonaws.services.lambda.runtime.Context;
4-
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
5-
import com.fasterxml.jackson.databind.ObjectMapper;
6-
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
7-
83
import java.io.IOException;
94
import java.io.InputStream;
105
import java.io.OutputStream;
116
import java.util.Map;
127

8+
import com.amazonaws.services.lambda.runtime.Context;
9+
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
12+
import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
13+
1314
public class AppStream implements RequestStreamHandler {
1415
private static final ObjectMapper mapper = new ObjectMapper();
1516

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

pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<module>powertools-logging</module>
3232
<module>powertools-tracing</module>
3333
<module>powertools-sqs</module>
34+
<module>powertools-metrics</module>
3435
</modules>
3536

3637
<scm>
@@ -67,6 +68,7 @@
6768
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
6869
<maven-gpg-plugin.version>1.6</maven-gpg-plugin.version>
6970
<junit-jupiter.version>5.7.0</junit-jupiter.version>
71+
<aws-embedded-metrics.version>1.0.0</aws-embedded-metrics.version>
7072
</properties>
7173

7274
<distributionManagement>
@@ -143,6 +145,11 @@
143145
<artifactId>aws-xray-recorder-sdk-aws-sdk-v2-instrumentor</artifactId>
144146
<version>${aws.xray.recorder.version}</version>
145147
</dependency>
148+
<dependency>
149+
<groupId>software.amazon.cloudwatchlogs</groupId>
150+
<artifactId>aws-embedded-metrics</artifactId>
151+
<version>${aws-embedded-metrics.version}</version>
152+
</dependency>
146153

147154
<!-- Test dependencies -->
148155
<dependency>
@@ -175,6 +182,12 @@
175182
<version>3.5.11</version>
176183
<scope>test</scope>
177184
</dependency>
185+
<dependency>
186+
<groupId>org.mockito</groupId>
187+
<artifactId>mockito-inline</artifactId>
188+
<version>3.5.10</version>
189+
<scope>test</scope>
190+
</dependency>
178191
<dependency>
179192
<groupId>org.aspectj</groupId>
180193
<artifactId>aspectjweaver</artifactId>

powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020

2121
import java.io.InputStream;
2222
import java.io.OutputStream;
23+
import java.util.Optional;
24+
25+
import static java.util.Optional.empty;
26+
import static java.util.Optional.of;
2327

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

55+
public static Optional<Context> extractContext(final ProceedingJoinPoint pjp) {
56+
57+
if (isHandlerMethod(pjp)) {
58+
if (placedOnRequestHandler(pjp)) {
59+
return of((Context) pjp.getArgs()[1]);
60+
}
61+
62+
if (placedOnStreamHandler(pjp)) {
63+
return of((Context) pjp.getArgs()[2]);
64+
}
65+
}
66+
67+
return empty();
68+
}
69+
5170
public static String serviceName() {
5271
return SERVICE_NAME;
5372
}
5473

55-
public static Boolean isColdStart() {
56-
return IS_COLD_START;
74+
public static boolean isColdStart() {
75+
return IS_COLD_START == null;
5776
}
5877

5978
public static void coldStartDone() {

0 commit comments

Comments
 (0)