Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import com.amazonaws.services.lambda.runtime.Context;
import java.time.Instant;
import java.util.function.Consumer;

import software.amazon.lambda.powertools.metrics.model.DimensionSet;
import software.amazon.lambda.powertools.metrics.model.MetricResolution;
Expand Down Expand Up @@ -162,7 +163,15 @@ default void captureColdStartMetric() {
}

/**
* Flush a single metric with custom dimensions. This creates a separate metrics context
* Flush a separate metrics context that inherits the namespace, dimensions and metadata. This creates a separate metrics context
* that doesn't affect the default metrics context.
*
* @param metricsConsumer the consumer to use to edit the metrics instance (e.g. add metrics, override namespace) before flushing
*/
void flushMetrics(Consumer<Metrics> metricsConsumer);

/**
* Flush a single metric with custom namespace and dimensions. This creates a separate metrics context
* that doesn't affect the default metrics context.
*
* @param name the name of the metric
Expand All @@ -171,10 +180,17 @@ default void captureColdStartMetric() {
* @param namespace the namespace for the metric
* @param dimensions custom dimensions for this metric (optional)
*/
void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions);
default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions) {
flushMetrics(metrics -> {
metrics.setNamespace(namespace);
metrics.setDefaultDimensions(dimensions);
metrics.addMetric(name, value, unit);
});

}

/**
* Flush a single metric with custom dimensions. This creates a separate metrics context
* Flush a single metric with custom namespace. This creates a separate metrics context
* that doesn't affect the default metrics context.
*
* @param name the name of the metric
Expand All @@ -183,6 +199,9 @@ default void captureColdStartMetric() {
* @param namespace the namespace for the metric
*/
default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace) {
flushSingleMetric(name, value, unit, namespace, null);
flushMetrics(metrics -> {
metrics.setNamespace(namespace);
metrics.addMetric(name, value, unit);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -50,9 +51,10 @@ public class EmfMetricsLogger implements Metrics {

private final software.amazon.cloudwatchlogs.emf.logger.MetricsLogger emfLogger;
private final EnvironmentProvider environmentProvider;
private AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false);
private final AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false);
private String namespace;
private Map<String, String> defaultDimensions = new HashMap<>();
private final Map<String, Object> metadata = new HashMap<>();
private final AtomicBoolean hasMetrics = new AtomicBoolean(false);

public EmfMetricsLogger(EnvironmentProvider environmentProvider, MetricsContext metricsContext) {
Expand All @@ -79,8 +81,6 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi
dimensionSet.getDimensions().forEach((key, val) -> {
try {
emfDimensionSet.addDimension(key, val);
// Update our local copy of default dimensions
defaultDimensions.put(key, val);
} catch (Exception e) {
// Ignore dimension errors
}
Expand All @@ -92,6 +92,7 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi
@Override
public void addMetadata(String key, Object value) {
emfLogger.putMetadata(key, value);
metadata.put(key, value);
}

@Override
Expand Down Expand Up @@ -221,43 +222,24 @@ public void captureColdStartMetric(software.amazon.lambda.powertools.metrics.mod
}

@Override
public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace,
software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) {
public void flushMetrics(Consumer<Metrics> metricsConsumer) {
if (isMetricsDisabled()) {
LOGGER.debug("Metrics are disabled, skipping single metric flush");
return;
}

Validator.validateNamespace(namespace);

// Create a new logger for this single metric
software.amazon.cloudwatchlogs.emf.logger.MetricsLogger singleMetricLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(
environmentProvider);

try {
singleMetricLogger.setNamespace(namespace);
} catch (Exception e) {
LOGGER.error("Namespace cannot be set for single metric due to an error in EMF", e);
// Create a new instance, inheriting namespace/dimensions state
EmfMetricsLogger metrics = new EmfMetricsLogger(environmentProvider, new MetricsContext());
if (namespace != null) {
metrics.setNamespace(this.namespace);
}

// Add the metric
singleMetricLogger.putMetric(name, value, convertUnit(unit));

// Set dimensions if provided
if (dimensions != null) {
DimensionSet emfDimensionSet = new DimensionSet();
dimensions.getDimensions().forEach((key, val) -> {
try {
emfDimensionSet.addDimension(key, val);
} catch (Exception e) {
// Ignore dimension errors
}
});
singleMetricLogger.setDimensions(emfDimensionSet);
if (!defaultDimensions.isEmpty()) {
metrics.setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of(defaultDimensions));
}
metadata.forEach(metrics::addMetadata);

metricsConsumer.accept(metrics);

// Flush the metric
singleMetricLogger.flush();
metrics.flush();
}

private boolean isMetricsDisabled() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ void shouldAddDimensionSet() throws Exception {
@Test
void shouldThrowExceptionWhenDimensionSetIsNull() {
// When/Then
assertThatThrownBy(() -> metrics.addDimension((DimensionSet) null))
assertThatThrownBy(() -> metrics.addDimension(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("DimensionSet cannot be null");
}
Expand Down Expand Up @@ -304,7 +304,7 @@ void shouldGetDefaultDimensions() {
@Test
void shouldThrowExceptionWhenDefaultDimensionSetIsNull() {
// When/Then
assertThatThrownBy(() -> metrics.setDefaultDimensions((DimensionSet) null))
assertThatThrownBy(() -> metrics.setDefaultDimensions(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("DimensionSet cannot be null");
}
Expand Down Expand Up @@ -346,7 +346,7 @@ void shouldLogWarningOnEmptyMetrics() throws Exception {

// Then
// Read the log file and check for the warning
String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
String logContent = Files.readString(logFile.toPath(), StandardCharsets.UTF_8);
assertThat(logContent).contains("No metrics were emitted");
// No EMF output should be generated
assertThat(outputStreamCaptor.toString().trim()).isEmpty();
Expand Down Expand Up @@ -446,6 +446,37 @@ void shouldReuseNamespaceForColdStartMetric() throws Exception {
.isEqualTo(customNamespace);
}

@Test
void shouldFlushMetrics() throws Exception {
// Given
metrics.setNamespace("MainNamespace");
metrics.setDefaultDimensions(DimensionSet.of("CustomDim", "CustomValue"));
metrics.addDimension(DimensionSet.of("CustomDim2", "CustomValue2"));
metrics.addMetadata("CustomMetadata", "MetadataValue");

// When
metrics.flushMetrics(m -> {
m.addMetric("metric-one", 200, MetricUnit.COUNT);
m.addMetric("metric-two", 100, MetricUnit.COUNT);
});

// Then
String emfOutput = outputStreamCaptor.toString().trim();
JsonNode rootNode = objectMapper.readTree(emfOutput);

assertThat(rootNode.has("metric-one")).isTrue();
assertThat(rootNode.get("metric-one").asDouble()).isEqualTo(200.0);
assertThat(rootNode.has("metric-two")).isTrue();
assertThat(rootNode.get("metric-two").asDouble()).isEqualTo(100);
assertThat(rootNode.has("CustomDim")).isTrue();
assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue");
assertThat(rootNode.get("CustomDim2")).isNull();
assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText())
.isEqualTo("MainNamespace");
assertThat(rootNode.get("_aws").has("CustomMetadata")).isTrue();
assertThat(rootNode.get("_aws").get("CustomMetadata").asText()).isEqualTo("MetadataValue");
}

@Test
void shouldFlushSingleMetric() throws Exception {
// Given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.time.Instant;
import java.util.Collections;
import java.util.function.Consumer;

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

Expand Down Expand Up @@ -77,6 +78,11 @@ public void captureColdStartMetric(DimensionSet dimensions) {
// Test placeholder
}

@Override
public void flushMetrics(Consumer<Metrics> metricsConsumer) {
// Test placeholder
}

@Override
public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace,
DimensionSet dimensions) {
Expand Down