Skip to content

Commit 6587c5a

Browse files
committed
feat(metrics): introduce Metrics.flushMetrics
- introduce `Metrics.flushMetrics` as a more powerful version of `flushSingleMetrics` to allow - using defaults by inheriting state e.g. namespace, dimensions and metadata - emitting multiple metrics in one metrics context - refactor `flushSingleMetrics` to use `flushMetrics` - move namespace/service setting from `MetricsFactory` to `EmfMetricsLogger`
1 parent 9200c9c commit 6587c5a

File tree

5 files changed

+90
-53
lines changed

5 files changed

+90
-53
lines changed

powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import com.amazonaws.services.lambda.runtime.Context;
1818
import java.time.Instant;
19+
import java.util.function.Consumer;
1920

2021
import software.amazon.lambda.powertools.metrics.model.DimensionSet;
2122
import software.amazon.lambda.powertools.metrics.model.MetricResolution;
@@ -162,7 +163,15 @@ default void captureColdStartMetric() {
162163
}
163164

164165
/**
165-
* Flush a single metric with custom dimensions. This creates a separate metrics context
166+
* Flush a separate metrics context that inherits the namespace, dimensions and metadata. This creates a separate metrics context
167+
* that doesn't affect the default metrics context.
168+
*
169+
* @param metricsConsumer the consumer to use to edit the metrics instance (e.g. add metrics, override namespace) before flushing
170+
*/
171+
void flushMetrics(Consumer<Metrics> metricsConsumer);
172+
173+
/**
174+
* Flush a single metric with custom namespace and dimensions. This creates a separate metrics context
166175
* that doesn't affect the default metrics context.
167176
*
168177
* @param name the name of the metric
@@ -171,10 +180,17 @@ default void captureColdStartMetric() {
171180
* @param namespace the namespace for the metric
172181
* @param dimensions custom dimensions for this metric (optional)
173182
*/
174-
void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions);
183+
default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions) {
184+
flushMetrics(metrics -> {
185+
metrics.setNamespace(namespace);
186+
metrics.setDefaultDimensions(dimensions);
187+
metrics.addMetric(name, value, unit);
188+
});
189+
190+
}
175191

176192
/**
177-
* Flush a single metric with custom dimensions. This creates a separate metrics context
193+
* Flush a single metric with custom namespace. This creates a separate metrics context
178194
* that doesn't affect the default metrics context.
179195
*
180196
* @param name the name of the metric
@@ -183,6 +199,9 @@ default void captureColdStartMetric() {
183199
* @param namespace the namespace for the metric
184200
*/
185201
default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace) {
186-
flushSingleMetric(name, value, unit, namespace, null);
202+
flushMetrics(metrics -> {
203+
metrics.setNamespace(namespace);
204+
metrics.addMetric(name, value, unit);
205+
});
187206
}
188207
}

powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717
import org.crac.Core;
1818
import org.crac.Resource;
1919
import software.amazon.lambda.powertools.common.internal.ClassPreLoader;
20-
import software.amazon.lambda.powertools.common.internal.LambdaConstants;
21-
import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
22-
import software.amazon.lambda.powertools.metrics.model.DimensionSet;
2320
import software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider;
2421
import software.amazon.lambda.powertools.metrics.provider.MetricsProvider;
2522

@@ -46,18 +43,6 @@ public final class MetricsFactory implements Resource {
4643
public static synchronized Metrics getMetricsInstance() {
4744
if (metrics == null) {
4845
metrics = provider.getMetricsInstance();
49-
50-
// Apply default configuration from environment variables
51-
String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE");
52-
if (envNamespace != null) {
53-
metrics.setNamespace(envNamespace);
54-
}
55-
56-
// Only set Service dimension if it's not the default undefined value
57-
String serviceName = LambdaHandlerProcessor.serviceName();
58-
if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) {
59-
metrics.setDefaultDimensions(DimensionSet.of("Service", serviceName));
60-
}
6146
}
6247

6348
return metrics;

powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.LinkedHashMap;
2323
import java.util.Map;
2424
import java.util.concurrent.atomic.AtomicBoolean;
25+
import java.util.function.Consumer;
2526

2627
import org.slf4j.Logger;
2728
import org.slf4j.LoggerFactory;
@@ -33,6 +34,8 @@
3334
import software.amazon.cloudwatchlogs.emf.model.MetricsContext;
3435
import software.amazon.cloudwatchlogs.emf.model.StorageResolution;
3536
import software.amazon.cloudwatchlogs.emf.model.Unit;
37+
import software.amazon.lambda.powertools.common.internal.LambdaConstants;
38+
import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
3639
import software.amazon.lambda.powertools.metrics.Metrics;
3740
import software.amazon.lambda.powertools.metrics.model.MetricResolution;
3841
import software.amazon.lambda.powertools.metrics.model.MetricUnit;
@@ -50,15 +53,30 @@ public class EmfMetricsLogger implements Metrics {
5053

5154
private final software.amazon.cloudwatchlogs.emf.logger.MetricsLogger emfLogger;
5255
private final EnvironmentProvider environmentProvider;
53-
private AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false);
56+
private final MetricsContext metricsContext;
57+
private final AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false);
5458
private String namespace;
5559
private Map<String, String> defaultDimensions = new HashMap<>();
60+
private final Map<String, Object> metadata = new HashMap<>();
5661
private final AtomicBoolean hasMetrics = new AtomicBoolean(false);
5762

5863
public EmfMetricsLogger(EnvironmentProvider environmentProvider, MetricsContext metricsContext) {
5964
this.emfLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(environmentProvider,
6065
metricsContext);
6166
this.environmentProvider = environmentProvider;
67+
this.metricsContext = metricsContext;
68+
69+
// Apply default configuration from environment variables
70+
String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE");
71+
if (envNamespace != null) {
72+
setNamespace(envNamespace);
73+
}
74+
75+
// Only set Service dimension if it's not the default undefined value
76+
String serviceName = LambdaHandlerProcessor.serviceName();
77+
if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) {
78+
setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of("Service", serviceName));
79+
}
6280
}
6381

6482
@Override
@@ -92,6 +110,7 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi
92110
@Override
93111
public void addMetadata(String key, Object value) {
94112
emfLogger.putMetadata(key, value);
113+
metadata.put(key, value);
95114
}
96115

97116
@Override
@@ -221,43 +240,22 @@ public void captureColdStartMetric(software.amazon.lambda.powertools.metrics.mod
221240
}
222241

223242
@Override
224-
public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace,
225-
software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) {
243+
public void flushMetrics(Consumer<Metrics> metricsConsumer) {
226244
if (isMetricsDisabled()) {
227245
LOGGER.debug("Metrics are disabled, skipping single metric flush");
228246
return;
229247
}
230-
231-
Validator.validateNamespace(namespace);
232-
233-
// Create a new logger for this single metric
234-
software.amazon.cloudwatchlogs.emf.logger.MetricsLogger singleMetricLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(
235-
environmentProvider);
236-
237-
try {
238-
singleMetricLogger.setNamespace(namespace);
239-
} catch (Exception e) {
240-
LOGGER.error("Namespace cannot be set for single metric due to an error in EMF", e);
248+
// Create a new instance, inheriting namespace/dimensions state
249+
EmfMetricsLogger metrics = new EmfMetricsLogger(environmentProvider, metricsContext);
250+
if (namespace != null) {
251+
metrics.setNamespace(this.namespace);
241252
}
253+
defaultDimensions.forEach(metrics::addDimension);
254+
metadata.forEach(metrics::addMetadata);
242255

243-
// Add the metric
244-
singleMetricLogger.putMetric(name, value, convertUnit(unit));
245-
246-
// Set dimensions if provided
247-
if (dimensions != null) {
248-
DimensionSet emfDimensionSet = new DimensionSet();
249-
dimensions.getDimensions().forEach((key, val) -> {
250-
try {
251-
emfDimensionSet.addDimension(key, val);
252-
} catch (Exception e) {
253-
// Ignore dimension errors
254-
}
255-
});
256-
singleMetricLogger.setDimensions(emfDimensionSet);
257-
}
256+
metricsConsumer.accept(metrics);
258257

259-
// Flush the metric
260-
singleMetricLogger.flush();
258+
metrics.flush();
261259
}
262260

263261
private boolean isMetricsDisabled() {

powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ void shouldAddDimensionSet() throws Exception {
246246
@Test
247247
void shouldThrowExceptionWhenDimensionSetIsNull() {
248248
// When/Then
249-
assertThatThrownBy(() -> metrics.addDimension((DimensionSet) null))
249+
assertThatThrownBy(() -> metrics.addDimension(null))
250250
.isInstanceOf(IllegalArgumentException.class)
251251
.hasMessage("DimensionSet cannot be null");
252252
}
@@ -304,7 +304,7 @@ void shouldGetDefaultDimensions() {
304304
@Test
305305
void shouldThrowExceptionWhenDefaultDimensionSetIsNull() {
306306
// When/Then
307-
assertThatThrownBy(() -> metrics.setDefaultDimensions((DimensionSet) null))
307+
assertThatThrownBy(() -> metrics.setDefaultDimensions(null))
308308
.isInstanceOf(IllegalArgumentException.class)
309309
.hasMessage("DimensionSet cannot be null");
310310
}
@@ -346,7 +346,7 @@ void shouldLogWarningOnEmptyMetrics() throws Exception {
346346

347347
// Then
348348
// Read the log file and check for the warning
349-
String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
349+
String logContent = Files.readString(logFile.toPath(), StandardCharsets.UTF_8);
350350
assertThat(logContent).contains("No metrics were emitted");
351351
// No EMF output should be generated
352352
assertThat(outputStreamCaptor.toString().trim()).isEmpty();
@@ -446,6 +446,35 @@ void shouldReuseNamespaceForColdStartMetric() throws Exception {
446446
.isEqualTo(customNamespace);
447447
}
448448

449+
@Test
450+
void shouldFlushMetrics() throws Exception {
451+
// Given
452+
metrics.setNamespace("MainNamespace");
453+
metrics.setDefaultDimensions(DimensionSet.of("CustomDim", "CustomValue"));
454+
metrics.addMetadata("CustomMetadata", "MetadataValue");
455+
456+
// When
457+
metrics.flushMetrics(m -> {
458+
m.addMetric("metric-one", 200, MetricUnit.COUNT);
459+
m.addMetric("metric-two", 100, MetricUnit.COUNT);
460+
});
461+
462+
// Then
463+
String emfOutput = outputStreamCaptor.toString().trim();
464+
JsonNode rootNode = objectMapper.readTree(emfOutput);
465+
466+
assertThat(rootNode.has("metric-one")).isTrue();
467+
assertThat(rootNode.get("metric-one").asDouble()).isEqualTo(200.0);
468+
assertThat(rootNode.has("metric-two")).isTrue();
469+
assertThat(rootNode.get("metric-two").asDouble()).isEqualTo(100);
470+
assertThat(rootNode.has("CustomDim")).isTrue();
471+
assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue");
472+
assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText())
473+
.isEqualTo("MainNamespace");
474+
assertThat(rootNode.get("_aws").has("CustomMetadata")).isTrue();
475+
assertThat(rootNode.get("_aws").get("CustomMetadata").asText()).isEqualTo("MetadataValue");
476+
}
477+
449478
@Test
450479
void shouldFlushSingleMetric() throws Exception {
451480
// Given

powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.time.Instant;
44
import java.util.Collections;
5+
import java.util.function.Consumer;
56

67
import com.amazonaws.services.lambda.runtime.Context;
78

@@ -77,6 +78,11 @@ public void captureColdStartMetric(DimensionSet dimensions) {
7778
// Test placeholder
7879
}
7980

81+
@Override
82+
public void flushMetrics(Consumer<Metrics> metricsConsumer) {
83+
// Test placeholder
84+
}
85+
8086
@Override
8187
public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace,
8288
DimensionSet dimensions) {

0 commit comments

Comments
 (0)