Skip to content

Commit 9a31343

Browse files
authored
Feature: EmfMetricLoggingPublisher (#5792)
* EmfMetricPublisher class added (#5752) * EmfMetricPublisher class with basic unit test added * Edge cases handled * javadoc for class added * Java doc for methods added * minor unit test changes * Change from using jacksonCore directly to using jsonWriter * EmfMetricConfiguration class added * MetricEmfConverter class added * new module checklist done * checkStyle fixed * minor build problem fixed * internal package added * fixed the issue passing raw objects * added java clock to unit test * added unit test for publish method * minor changes * config class to builder pattern * config class to builder pattern * minor change * End to end test passed * added valid case in WARN_LOG allowlist * unit tests adjusted * Change logGroupName to required field * move MetricValueNormalizer class to utils package * converter implementation changed * schemaConformTest added * SchemaConformTest adjusted * minor change * emf metric publisher performance test (#5775) * Benchmark test for emfMetricPublisher added * input name changed * add emfBenchmarkTest to BenchmarkRunner * enabled METRIC_BENCHMARKS * checkStyle fixed * Changed the classname to EmfMetricLoggingPublisher (#5783) * change class name and artifactId * test-coverage pom changed * javadoc fixed * Cloudwatch and Logging benchmark test added / Changed logGroupName config (#5790) * more benchmark test added/changed logGroupName config * teardown method overrided * logGroupName access method changed * Snapshot version changed * LogGroupName in lambda unit test added * suppression added for system * changelog added * changeLog modified * minor fix * snapshot version fixed * Snapshot version changed
1 parent 74f7529 commit 9a31343

File tree

28 files changed

+1542
-14
lines changed

28 files changed

+1542
-14
lines changed

.brazil.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"aws-xml-protocol": { "packageName": "AwsJavaSdk-Core-AwsXmlProtocol" },
1717
"smithy-rpcv2-protocol": { "packageName": "AwsJavaSdk-Core-SmithyRpcV2Protocol" },
1818
"cloudwatch-metric-publisher": { "packageName": "AwsJavaSdk-MetricPublisher-CloudWatch" },
19+
"emf-metric-logging-publisher": { "packageName": "AwsJavaSdk-MetricPublisher-Emf" },
1920
"codegen": { "packageName": "AwsJavaSdk-Codegen" },
2021
"dynamodb-enhanced": { "packageName": "AwsJavaSdk-DynamoDb-Enhanced" },
2122
"http-client-spi": { "packageName": "AwsJavaSdk-HttpClient" },
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Emf Metric Logging Publisher",
4+
"contributor": "",
5+
"description": "Added a new EmfMetricLoggingPublisher class that transforms SdkMetricCollection to emf format string and logs it, which will be automatically collected by cloudwatch."
6+
}

aws-sdk-java/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,6 +1773,11 @@ Amazon AutoScaling, etc).</description>
17731773
<artifactId>cloudwatch-metric-publisher</artifactId>
17741774
<version>${awsjavasdk.version}</version>
17751775
</dependency>
1776+
<dependency>
1777+
<groupId>software.amazon.awssdk</groupId>
1778+
<artifactId>emf-metric-logging-publisher</artifactId>
1779+
<version>${awsjavasdk.version}</version>
1780+
</dependency>
17761781
<dependency>
17771782
<groupId>software.amazon.awssdk</groupId>
17781783
<artifactId>launchwizard</artifactId>

bom/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@
212212
<artifactId>cloudwatch-metric-publisher</artifactId>
213213
<version>${awsjavasdk.version}</version>
214214
</dependency>
215+
<dependency>
216+
<groupId>software.amazon.awssdk</groupId>
217+
<artifactId>emf-metric-logging-publisher</artifactId>
218+
<version>${awsjavasdk.version}</version>
219+
</dependency>
215220
<dependency>
216221
<groupId>software.amazon.awssdk</groupId>
217222
<artifactId>s3-transfer-manager</artifactId>

metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/MetricCollectionAggregator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import software.amazon.awssdk.services.cloudwatch.model.MetricDatum;
3434
import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest;
3535
import software.amazon.awssdk.services.cloudwatch.model.StatisticSet;
36+
import software.amazon.awssdk.utils.MetricValueNormalizer;
3637

3738
/**
3839
* Aggregates {@link MetricCollection}s by: (1) the minute in which they occurred, and (2) the dimensions in the collection

metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/TimeBucketedMetrics.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import software.amazon.awssdk.metrics.SdkMetric;
3737
import software.amazon.awssdk.services.cloudwatch.model.Dimension;
3838
import software.amazon.awssdk.services.cloudwatch.model.StandardUnit;
39+
import software.amazon.awssdk.utils.MetricValueNormalizer;
3940

4041
/**
4142
* "Buckets" metrics by the minute in which they were collected. This allows all metric data for a given 1-minute period to be
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License").
6+
~ You may not use this file except in compliance with the License.
7+
~ A copy of the License is located at
8+
~
9+
~ http://aws.amazon.com/apache2.0
10+
~
11+
~ or in the "license" file accompanying this file. This file is distributed
12+
~ on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13+
~ express or implied. See the License for the specific language governing
14+
~ permissions and limitations under the License.
15+
-->
16+
17+
<project xmlns="http://maven.apache.org/POM/4.0.0"
18+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
19+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
20+
<modelVersion>4.0.0</modelVersion>
21+
<parent>
22+
<groupId>software.amazon.awssdk</groupId>
23+
<artifactId>metric-publishers</artifactId>
24+
<version>2.30.3-SNAPSHOT</version>
25+
</parent>
26+
27+
<artifactId>emf-metric-logging-publisher</artifactId>
28+
<name>AWS Java SDK :: Metric Publishers :: Emf</name>
29+
<packaging>jar</packaging>
30+
31+
<build>
32+
<plugins>
33+
<plugin>
34+
<groupId>org.apache.maven.plugins</groupId>
35+
<artifactId>maven-jar-plugin</artifactId>
36+
<configuration>
37+
<archive>
38+
<manifestEntries>
39+
<Automatic-Module-Name>software.amazon.awssdk.metrics.publishers.emf</Automatic-Module-Name>
40+
</manifestEntries>
41+
</archive>
42+
</configuration>
43+
</plugin>
44+
</plugins>
45+
</build>
46+
47+
<dependencies>
48+
<dependency>
49+
<groupId>software.amazon.awssdk</groupId>
50+
<artifactId>annotations</artifactId>
51+
<version>${awsjavasdk.version}</version>
52+
</dependency>
53+
<dependency>
54+
<groupId>software.amazon.awssdk</groupId>
55+
<artifactId>http-client-spi</artifactId>
56+
<version>${awsjavasdk.version}</version>
57+
</dependency>
58+
<dependency>
59+
<groupId>software.amazon.awssdk</groupId>
60+
<artifactId>json-utils</artifactId>
61+
<version>${awsjavasdk.version}</version>
62+
<scope>compile</scope>
63+
</dependency>
64+
<dependency>
65+
<groupId>software.amazon.awssdk</groupId>
66+
<artifactId>sdk-core</artifactId>
67+
<version>${awsjavasdk.version}</version>
68+
<scope>compile</scope>
69+
</dependency>
70+
<dependency>
71+
<groupId>com.networknt</groupId>
72+
<artifactId>json-schema-validator</artifactId>
73+
<version>${json-schema-validator.version}</version>
74+
<scope>test</scope>
75+
</dependency>
76+
</dependencies>
77+
78+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.metrics.publishers.emf;
17+
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.Collection;
22+
import java.util.List;
23+
import software.amazon.awssdk.annotations.Immutable;
24+
import software.amazon.awssdk.annotations.SdkPublicApi;
25+
import software.amazon.awssdk.annotations.ThreadSafe;
26+
import software.amazon.awssdk.core.metrics.CoreMetric;
27+
import software.amazon.awssdk.metrics.MetricCategory;
28+
import software.amazon.awssdk.metrics.MetricCollection;
29+
import software.amazon.awssdk.metrics.MetricLevel;
30+
import software.amazon.awssdk.metrics.MetricPublisher;
31+
import software.amazon.awssdk.metrics.SdkMetric;
32+
import software.amazon.awssdk.metrics.publishers.emf.internal.EmfMetricConfiguration;
33+
import software.amazon.awssdk.metrics.publishers.emf.internal.MetricEmfConverter;
34+
import software.amazon.awssdk.utils.Logger;
35+
36+
/**
37+
* A metric publisher implementation that converts metrics into CloudWatch Embedded Metric Format (EMF).
38+
* EMF allows metrics to be published through CloudWatch Logs using a structured JSON format, which
39+
* CloudWatch automatically extracts and processes into metrics.
40+
*
41+
* <p>
42+
* This publisher is particularly well-suited for serverless environments like AWS Lambda and container
43+
* environments like Amazon ECS that have built-in integration with CloudWatch Logs. Using EMF eliminates
44+
* the need for separate metric publishing infrastructure as metrics are automatically extracted from
45+
* log entries.
46+
* </p>
47+
*
48+
* <p>
49+
* The EMF publisher converts metric collections into JSON-formatted log entries that conform to the
50+
* CloudWatch EMF specification. The logGroupName field is required for EMF to work.
51+
* CloudWatch automatically processes these logs to generate corresponding metrics that can be used for
52+
* monitoring and alerting.
53+
* </p>
54+
*
55+
* @snippet
56+
* // Create a EmfMetricLoggingPublisher using a custom namespace.
57+
* MetricPublisher emfMetricLoggingPublisher = EmfMetricLoggingPublisher.builder()
58+
* .logGroupName("myLogGroupName")
59+
* .namespace("myApplication")
60+
* .build();
61+
*
62+
* @see MetricPublisher The base interface for metric publishers
63+
* @see MetricCollection For the collection of metrics to be published
64+
* @see EmfMetricConfiguration For configuration options
65+
* @see MetricEmfConverter For the conversion logic
66+
*
67+
*/
68+
69+
@ThreadSafe
70+
@Immutable
71+
@SdkPublicApi
72+
public final class EmfMetricLoggingPublisher implements MetricPublisher {
73+
74+
private static final Logger logger = Logger.loggerFor(EmfMetricLoggingPublisher.class);
75+
private final MetricEmfConverter metricConverter;
76+
77+
78+
private EmfMetricLoggingPublisher(Builder builder) {
79+
EmfMetricConfiguration config = new EmfMetricConfiguration.Builder()
80+
.namespace(builder.namespace)
81+
.logGroupName(builder.logGroupName)
82+
.dimensions(builder.dimensions)
83+
.metricLevel(builder.metricLevel)
84+
.metricCategories(builder.metricCategories)
85+
.build();
86+
87+
this.metricConverter = new MetricEmfConverter(config);
88+
}
89+
90+
91+
public static Builder builder() {
92+
return new Builder();
93+
}
94+
95+
96+
@Override
97+
public void publish(MetricCollection metricCollection) {
98+
if (metricCollection == null) {
99+
logger.warn(() -> "Null metric collection passed to the publisher");
100+
return;
101+
}
102+
try {
103+
List<String> emfStrings = metricConverter.convertMetricCollectionToEmf(metricCollection);
104+
for (String emfString : emfStrings) {
105+
logger.info(() -> emfString);
106+
}
107+
} catch (Exception e) {
108+
logger.error(() -> "Failed to log metrics in EMF format", e);
109+
}
110+
}
111+
112+
/**
113+
* Closes this metric publisher. This implementation is empty as the EMF metric logging publisher
114+
* does not maintain any resources that require explicit cleanup.
115+
*/
116+
@Override
117+
public void close() {
118+
}
119+
120+
public static final class Builder {
121+
private String namespace;
122+
private String logGroupName;
123+
private Collection<SdkMetric<String>> dimensions;
124+
private Collection<MetricCategory> metricCategories;
125+
private MetricLevel metricLevel;
126+
127+
private Builder() {
128+
}
129+
130+
/**
131+
* Configure the namespace that will be put into the emf log to this publisher.
132+
*
133+
* <p>If this is not specified, {@code AwsSdk/JavaSdk2} will be used.
134+
*/
135+
public Builder namespace(String namespace) {
136+
this.namespace = namespace;
137+
return this;
138+
}
139+
140+
/**
141+
* Configure the {@link SdkMetric} that are used to define the Dimension Set Array that will be put into the emf log to
142+
* this
143+
* publisher.
144+
*
145+
* <p>If this is not specified, {@link CoreMetric#SERVICE_ID} and {@link CoreMetric#OPERATION_NAME} will be used.
146+
*/
147+
public Builder dimensions(Collection<SdkMetric<String>> dimensions) {
148+
this.dimensions = new ArrayList<>(dimensions);
149+
return this;
150+
}
151+
152+
/**
153+
* @see #dimensions(SdkMetric[])
154+
*/
155+
@SafeVarargs
156+
public final Builder dimensions(SdkMetric<String>... dimensions) {
157+
return dimensions(Arrays.asList(dimensions));
158+
}
159+
160+
161+
/**
162+
* Configure the {@link MetricCategory}s that should be uploaded to CloudWatch.
163+
*
164+
* <p>If this is not specified, {@link MetricCategory#ALL} is used.
165+
*
166+
* <p>All {@link SdkMetric}s are associated with at least one {@code MetricCategory}. This setting determines which
167+
* category of metrics uploaded to CloudWatch. Any metrics {@link #publish(MetricCollection)}ed that do not fall under
168+
* these configured categories are ignored.
169+
*
170+
* <p>Note: If there are {@link #dimensions(Collection)} configured that do not fall under these {@code MetricCategory}
171+
* values, the dimensions will NOT be ignored. In other words, the metric category configuration only affects which
172+
* metrics are uploaded to CloudWatch, not which values can be used for {@code dimensions}.
173+
*/
174+
public Builder metricCategories(Collection<MetricCategory> metricCategories) {
175+
this.metricCategories = new ArrayList<>(metricCategories);
176+
return this;
177+
}
178+
179+
/**
180+
* @see #metricCategories(Collection)
181+
*/
182+
public Builder metricCategories(MetricCategory... metricCategories) {
183+
return metricCategories(Arrays.asList(metricCategories));
184+
}
185+
186+
/**
187+
* Configure the LogGroupName key that will be put into the emf log to this publisher. This is required when using
188+
* the CloudWatch agent to send embedded metric format logs that tells the agent which log
189+
* group to use.
190+
*
191+
* <p> If this is not specified, for AWS lambda environments, {@code AWS_LAMBDA_LOG_GROUP_NAME}
192+
* is used.
193+
* This field is required and must not be null or empty for non-lambda environments.
194+
* @throws NullPointerException if non-lambda environment and logGroupName is null
195+
*/
196+
public Builder logGroupName(String logGroupName) {
197+
this.logGroupName = logGroupName;
198+
return this;
199+
}
200+
201+
/**
202+
* Configure the {@link MetricLevel} that should be uploaded to CloudWatch.
203+
*
204+
* <p>If this is not specified, {@link MetricLevel#INFO} is used.
205+
*
206+
* <p>All {@link SdkMetric}s are associated with one {@code MetricLevel}. This setting determines which level of metrics
207+
* uploaded to CloudWatch. Any metrics {@link #publish(MetricCollection)}ed that do not fall under these configured
208+
* categories are ignored.
209+
*
210+
* <p>Note: If there are {@link #dimensions(Collection)} configured that do not fall under this {@code MetricLevel}
211+
* values, the dimensions will NOT be ignored. In other words, the metric category configuration only affects which
212+
* metrics are uploaded to CloudWatch, not which values can be used for {@code dimensions}.
213+
*/
214+
public Builder metricLevel(MetricLevel metricLevel) {
215+
this.metricLevel = metricLevel;
216+
return this;
217+
}
218+
219+
220+
/**
221+
* Build a {@link EmfMetricLoggingPublisher} using the configuration currently configured on this publisher.
222+
*/
223+
public EmfMetricLoggingPublisher build() {
224+
return new EmfMetricLoggingPublisher(this);
225+
}
226+
227+
}
228+
}

0 commit comments

Comments
 (0)