Skip to content

Commit 78a3cb6

Browse files
humanzzdagnir
andauthored
Add PropertiesFactory support to EmfMetricLoggingPublisher (#6735)
* Add propertiesSupplier support to EmfMetricLoggingPublisher Enable `EmfMetricLoggingPublisher` to accept an optional `Supplier<Map<String, String>>` for enriching EMF records with custom properties at publish time - Add `propertiesSupplier` field and builder method to `EmfMetricLoggingPublisher.Builder` - Add `propertiesSupplier` field, accessor, and builder setter to `EmfMetricConfiguration`, defaulting to empty map when null - Add `resolveProperties()` to `MetricEmfConverter` which invokes the supplier from config once per convert call, handling null returns and exceptions gracefully - Add `writeCustomProperties()` to `MetricEmfConverter` which writes properties first in the EMF JSON so `_aws`, dimensions, and metrics overwrite any key collisions Closes #6595 * Filter out colliding keys when writing custom properties * Drop blank lines * Drop unused import * Change Supplier to PropertiesFactory functional interface * Update changelog entry --------- Co-authored-by: Dongie Agnir <261310+dagnir@users.noreply.github.com>
1 parent abd1814 commit 78a3cb6

File tree

7 files changed

+390
-7
lines changed

7 files changed

+390
-7
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon CloudWatch EMF Metric Publisher",
4+
"contributor": "humanzz",
5+
"description": "Add `PropertiesFactory` and `propertiesFactory` to `EmfMetricLoggingPublisher.Builder`, enabling users to enrich EMF records with custom key-value properties derived from the metric collection or ambient context, searchable in CloudWatch Logs Insights. See [#6595](https://github.com/aws/aws-sdk-java-v2/issues/6595)."
6+
}

metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ private EmfMetricLoggingPublisher(Builder builder) {
8282
.dimensions(builder.dimensions)
8383
.metricLevel(builder.metricLevel)
8484
.metricCategories(builder.metricCategories)
85+
.propertiesFactory(builder.propertiesFactory)
8586
.build();
8687

8788
this.metricConverter = new MetricEmfConverter(config);
@@ -123,6 +124,7 @@ public static final class Builder {
123124
private Collection<SdkMetric<String>> dimensions;
124125
private Collection<MetricCategory> metricCategories;
125126
private MetricLevel metricLevel;
127+
private PropertiesFactory propertiesFactory;
126128

127129
private Builder() {
128130
}
@@ -217,6 +219,28 @@ public Builder metricLevel(MetricLevel metricLevel) {
217219
}
218220

219221

222+
/**
223+
* Configure a factory for custom properties to include in each EMF record.
224+
* The factory is invoked on each {@link #publish(MetricCollection)} call with the
225+
* {@link MetricCollection} being published, and the returned map entries are written
226+
* as top-level key-value pairs in the EMF JSON output. These appear as searchable
227+
* fields in CloudWatch Logs Insights.
228+
*
229+
* <p>Keys that collide with reserved EMF fields ({@code _aws}), configured
230+
* dimension names, or reported metric names are silently skipped.
231+
*
232+
* <p>If this is not specified, no custom properties are added.
233+
*
234+
* @param propertiesFactory a factory returning a map of property names to values,
235+
* or {@code null} to disable custom properties
236+
* @return this builder
237+
* @see PropertiesFactory
238+
*/
239+
public Builder propertiesFactory(PropertiesFactory propertiesFactory) {
240+
this.propertiesFactory = propertiesFactory;
241+
return this;
242+
}
243+
220244
/**
221245
* Build a {@link EmfMetricLoggingPublisher} using the configuration currently configured on this publisher.
222246
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
import java.util.Map;
19+
import software.amazon.awssdk.annotations.SdkPublicApi;
20+
import software.amazon.awssdk.metrics.MetricCollection;
21+
22+
/**
23+
* A factory for producing custom properties to include in each EMF record.
24+
*
25+
* <p>Implementations receive the {@link MetricCollection} being published, allowing properties
26+
* to be derived from the SDK metrics (e.g. service endpoint, request ID) or from ambient
27+
* context (e.g. Lambda request ID, trace ID).
28+
*
29+
* <p>The returned map entries are written as top-level key-value pairs in the EMF JSON output,
30+
* making them searchable in CloudWatch Logs Insights. Keys that collide with reserved EMF
31+
* fields ({@code _aws}), dimension names, or metric names are silently skipped.
32+
*
33+
* <p>If the factory returns {@code null} or throws an exception, no custom properties are added
34+
* and a warning is logged.
35+
*
36+
* <p>Example using ambient context:
37+
* <pre>{@code
38+
* EmfMetricLoggingPublisher.builder()
39+
* .propertiesFactory(metrics -> Collections.singletonMap("RequestId", requestId))
40+
* .build();
41+
* }</pre>
42+
*
43+
* <p>Example using metric collection values:
44+
* <pre>{@code
45+
* EmfMetricLoggingPublisher.builder()
46+
* .propertiesFactory(metrics -> {
47+
* Map<String, String> props = new HashMap<>();
48+
* metrics.metricValues(CoreMetric.SERVICE_ENDPOINT)
49+
* .stream().findFirst()
50+
* .ifPresent(uri -> props.put("ServiceEndpoint", uri.toString()));
51+
* return props;
52+
* })
53+
* .build();
54+
* }</pre>
55+
*
56+
* @see EmfMetricLoggingPublisher.Builder#propertiesFactory(PropertiesFactory)
57+
*/
58+
@FunctionalInterface
59+
@SdkPublicApi
60+
public interface PropertiesFactory {
61+
62+
/**
63+
* Create a map of custom properties to include in the EMF record for the given metric collection.
64+
*
65+
* @param metricCollection the SDK metric collection being published
66+
* @return a map of property names to string values, or {@code null} for no custom properties
67+
*/
68+
Map<String, String> create(MetricCollection metricCollection);
69+
}

metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import software.amazon.awssdk.metrics.MetricCategory;
2727
import software.amazon.awssdk.metrics.MetricLevel;
2828
import software.amazon.awssdk.metrics.SdkMetric;
29+
import software.amazon.awssdk.metrics.publishers.emf.PropertiesFactory;
2930
import software.amazon.awssdk.utils.Validate;
3031
import software.amazon.awssdk.utils.internal.SystemSettingUtils;
3132

@@ -43,13 +44,17 @@ public final class EmfMetricConfiguration {
4344
private final Set<SdkMetric<String>> dimensions;
4445
private final Collection<MetricCategory> metricCategories;
4546
private final MetricLevel metricLevel;
47+
private final PropertiesFactory propertiesFactory;
4648

4749
private EmfMetricConfiguration(Builder builder) {
4850
this.namespace = builder.namespace == null ? DEFAULT_NAMESPACE : builder.namespace;
4951
this.logGroupName = Validate.paramNotNull(resolveLogGroupName(builder), "logGroupName");
5052
this.dimensions = builder.dimensions == null ? DEFAULT_DIMENSIONS : new HashSet<>(builder.dimensions);
5153
this.metricCategories = builder.metricCategories == null ? DEFAULT_CATEGORIES : new HashSet<>(builder.metricCategories);
5254
this.metricLevel = builder.metricLevel == null ? DEFAULT_METRIC_LEVEL : builder.metricLevel;
55+
this.propertiesFactory = builder.propertiesFactory == null
56+
? mc -> Collections.emptyMap()
57+
: builder.propertiesFactory;
5358
}
5459

5560

@@ -59,6 +64,7 @@ public static class Builder {
5964
private Collection<SdkMetric<String>> dimensions;
6065
private Collection<MetricCategory> metricCategories;
6166
private MetricLevel metricLevel;
67+
private PropertiesFactory propertiesFactory;
6268

6369
public Builder namespace(String namespace) {
6470
this.namespace = namespace;
@@ -85,6 +91,11 @@ public Builder metricLevel(MetricLevel metricLevel) {
8591
return this;
8692
}
8793

94+
public Builder propertiesFactory(PropertiesFactory propertiesFactory) {
95+
this.propertiesFactory = propertiesFactory;
96+
return this;
97+
}
98+
8899
public EmfMetricConfiguration build() {
89100
return new EmfMetricConfiguration(this);
90101
}
@@ -110,6 +121,10 @@ public MetricLevel metricLevel() {
110121
return metricLevel;
111122
}
112123

124+
public PropertiesFactory propertiesFactory() {
125+
return propertiesFactory;
126+
}
127+
113128
private String resolveLogGroupName(Builder builder) {
114129
return builder.logGroupName != null ? builder.logGroupName :
115130
SystemSettingUtils.resolveEnvironmentVariable("AWS_LAMBDA_LOG_GROUP_NAME").orElse(null);

metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import java.time.Clock;
2020
import java.time.Duration;
2121
import java.util.ArrayList;
22+
import java.util.Collections;
2223
import java.util.HashMap;
24+
import java.util.HashSet;
2325
import java.util.LinkedList;
2426
import java.util.List;
2527
import java.util.Map;
@@ -31,6 +33,7 @@
3133
import software.amazon.awssdk.metrics.MetricCollection;
3234
import software.amazon.awssdk.metrics.MetricRecord;
3335
import software.amazon.awssdk.metrics.SdkMetric;
36+
import software.amazon.awssdk.metrics.publishers.emf.PropertiesFactory;
3437
import software.amazon.awssdk.protocols.jsoncore.JsonWriter;
3538
import software.amazon.awssdk.utils.Logger;
3639
import software.amazon.awssdk.utils.MetricValueNormalizer;
@@ -61,17 +64,21 @@ public class MetricEmfConverter {
6164
*/
6265
private static final int MAX_METRIC_NUM = 100;
6366

67+
private static final String AWS_METADATA_KEY = "_aws";
68+
6469
private static final Logger logger = Logger.loggerFor(MetricEmfConverter.class);
6570
private final List<String> dimensions = new ArrayList<>();
6671
private final EmfMetricConfiguration config;
6772
private final boolean metricCategoriesContainsAll;
6873
private final Clock clock;
74+
private final PropertiesFactory propertiesFactory;
6975

7076
@SdkTestInternalApi
7177
public MetricEmfConverter(EmfMetricConfiguration config, Clock clock) {
7278
this.config = config;
7379
this.clock = clock;
7480
this.metricCategoriesContainsAll = config.metricCategories().contains(MetricCategory.ALL);
81+
this.propertiesFactory = config.propertiesFactory();
7582
}
7683

7784
public MetricEmfConverter(EmfMetricConfiguration config) {
@@ -136,7 +143,18 @@ public List<String> convertMetricCollectionToEmf(MetricCollection metricCollecti
136143
}
137144
}
138145

139-
return createEmfStrings(aggregatedMetrics);
146+
Map<String, String> properties = resolveProperties(metricCollection);
147+
return createEmfStrings(aggregatedMetrics, properties);
148+
}
149+
150+
private Map<String, String> resolveProperties(MetricCollection metricCollection) {
151+
try {
152+
Map<String, String> result = propertiesFactory.create(metricCollection);
153+
return result == null ? Collections.emptyMap() : result;
154+
} catch (Exception e) {
155+
logger.warn(() -> "Properties factory threw an exception, publishing without custom properties", e);
156+
return Collections.emptyMap();
157+
}
140158
}
141159

142160
/**
@@ -188,7 +206,8 @@ private void processAndWriteValue(JsonWriter jsonWriter, MetricRecord<?> mRecord
188206
}
189207
}
190208

191-
private List<String> createEmfStrings(Map<SdkMetric<?>, List<MetricRecord<?>>> aggregatedMetrics) {
209+
private List<String> createEmfStrings(Map<SdkMetric<?>, List<MetricRecord<?>>> aggregatedMetrics,
210+
Map<String, String> properties) {
192211
List<String> emfStrings = new ArrayList<>();
193212
Map<SdkMetric<?>, List<MetricRecord<?>>> currentMetricBatch = new HashMap<>();
194213

@@ -204,35 +223,55 @@ private List<String> createEmfStrings(Map<SdkMetric<?>, List<MetricRecord<?>>> a
204223
}
205224

206225
if (currentMetricBatch.size() == MAX_METRIC_NUM) {
207-
emfStrings.add(createEmfString(currentMetricBatch));
226+
emfStrings.add(createEmfString(currentMetricBatch, properties));
208227
currentMetricBatch = new HashMap<>();
209228
}
210229

211230
currentMetricBatch.put(metric, records);
212231
}
213232

214-
emfStrings.add(createEmfString(currentMetricBatch));
233+
emfStrings.add(createEmfString(currentMetricBatch, properties));
215234

216235
return emfStrings;
217236
}
218237

219238

220-
private String createEmfString(Map<SdkMetric<?>, List<MetricRecord<?>>> metrics) {
239+
private String createEmfString(Map<SdkMetric<?>, List<MetricRecord<?>>> metrics,
240+
Map<String, String> properties) {
221241

222242
JsonWriter jsonWriter = JsonWriter.create();
223243
jsonWriter.writeStartObject();
224244

225245
writeAwsObject(jsonWriter, metrics.keySet());
226246
writeMetricValues(jsonWriter, metrics);
247+
writeCustomProperties(jsonWriter, properties, metrics.keySet());
227248

228249
jsonWriter.writeEndObject();
229250

230251
return new String(jsonWriter.getBytes(), StandardCharsets.UTF_8);
231252

232253
}
233254

255+
private void writeCustomProperties(JsonWriter jsonWriter, Map<String, String> properties,
256+
Set<SdkMetric<?>> metrics) {
257+
if (properties.isEmpty()) {
258+
return;
259+
}
260+
Set<String> reservedKeys = new HashSet<>();
261+
reservedKeys.add(AWS_METADATA_KEY);
262+
for (SdkMetric<?> metric : metrics) {
263+
reservedKeys.add(metric.name());
264+
}
265+
for (Map.Entry<String, String> entry : properties.entrySet()) {
266+
if (!reservedKeys.contains(entry.getKey())) {
267+
jsonWriter.writeFieldName(entry.getKey());
268+
jsonWriter.writeValue(entry.getValue());
269+
}
270+
}
271+
}
272+
234273
private void writeAwsObject(JsonWriter jsonWriter, Set<SdkMetric<?>> metricNames) {
235-
jsonWriter.writeFieldName("_aws");
274+
jsonWriter.writeFieldName(AWS_METADATA_KEY);
236275
jsonWriter.writeStartObject();
237276

238277
jsonWriter.writeFieldName("Timestamp");

0 commit comments

Comments
 (0)