Skip to content

Commit fe5aef6

Browse files
humanzzphipag
andauthored
feat(metrics): introduce Metrics.flushMetrics (#2154)
* 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` * 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` * address metrics context issue * fix addDimension wrongly adding to defaultDimensions * introduce Metrics.addProperty and use it instead of addMetadata * use flushMetrics in captureColdStartMetric * use putProperty for addMetadata and consolidate duplicate logic * update docs and javadoc * Update docs/core/metrics.md Co-authored-by: Philipp Page <philipp.page@yahoo.de> --------- Co-authored-by: Philipp Page <philipp.page@yahoo.de>
1 parent e2707c5 commit fe5aef6

File tree

7 files changed

+140
-118
lines changed

7 files changed

+140
-118
lines changed

docs/core/metrics.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -462,9 +462,9 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics
462462
Overwriting the default dimensions will also overwrite the default `Service` dimension. If you wish to keep `Service` in your default dimensions, you need to add it manually.
463463
<!-- prettier-ignore-end -->
464464

465-
### Creating a single metric with different configuration
465+
### Creating metrics with different configuration
466466

467-
You can create a single metric with its own namespace and dimensions using `flushSingleMetric`:
467+
You can create metrics with different configurations e.g. different namespace and/or dimensions using `flushMetrics()`:
468468

469469
=== "App.java"
470470

@@ -480,13 +480,17 @@ You can create a single metric with its own namespace and dimensions using `flus
480480
@Override
481481
@FlushMetrics(namespace = "ServerlessAirline", service = "payment")
482482
public Object handleRequest(Object input, Context context) {
483-
metrics.flushSingleMetric(
484-
"CustomMetric",
485-
1,
486-
MetricUnit.COUNT,
487-
"CustomNamespace",
488-
DimensionSet.of("CustomDimension", "value") // Dimensions are optional
489-
);
483+
metrics.flushMetrics((customMetrics) -> {
484+
customMetrics.addMetric("CustomMetric", 1, MetricUnit.COUNT);
485+
// To optionally set a different namespace
486+
customMetrics.setNamespace("CustomNamespace");
487+
// To optionally set different default dimensions
488+
customMetrics.setDefaultDimensions(DimensionSet.of("CustomDefaultDimension", "value"));
489+
// To optionally append additional dimensions to default dimensions
490+
customMetrics.addDimension(DimensionSet.of("CustomDimension", "value"));
491+
// To optionally add metadata
492+
customMetrics.addMetadata("CustomMetadata", "value"));
493+
});
490494
}
491495
}
492496
```
@@ -516,7 +520,7 @@ The following example shows how to configure a custom `Metrics` Singleton using
516520

517521
public class App implements RequestHandler<Object, Object> {
518522
// Create and configure a Metrics singleton without annotation
519-
private static final Metrics customMetrics = MetricsBuilder.builder()
523+
private static final Metrics metrics = MetricsBuilder.builder()
520524
.withNamespace("ServerlessAirline")
521525
.withRaiseOnEmptyMetrics(true)
522526
.withService("payment")
@@ -527,11 +531,11 @@ The following example shows how to configure a custom `Metrics` Singleton using
527531
// You can manually capture the cold start metric
528532
// Lambda context is an optional argument if not available in your environment
529533
// Dimensions are also optional.
530-
customMetrics.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment"));
534+
metrics.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment"));
531535

532536
// Add metrics to the custom metrics singleton
533-
customMetrics.addMetric("CustomMetric", 1, MetricUnit.COUNT);
534-
customMetrics.flush();
537+
metrics.addMetric("CustomMetric", 1, MetricUnit.COUNT);
538+
metrics.flush();
535539
}
536540
}
537541
```

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, default 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, set or add custom dimensions) 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/internal/EmfMetricsLogger.java

Lines changed: 23 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414

1515
package software.amazon.lambda.powertools.metrics.internal;
1616

17-
import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId;
1817
import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart;
1918

2019
import java.time.Instant;
2120
import java.util.HashMap;
2221
import java.util.LinkedHashMap;
2322
import java.util.Map;
2423
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.function.Consumer;
2525

2626
import org.slf4j.Logger;
2727
import org.slf4j.LoggerFactory;
@@ -43,16 +43,15 @@
4343
*/
4444
public class EmfMetricsLogger implements Metrics {
4545
private static final Logger LOGGER = LoggerFactory.getLogger(EmfMetricsLogger.class);
46-
private static final String TRACE_ID_PROPERTY = "xray_trace_id";
47-
private static final String REQUEST_ID_PROPERTY = "function_request_id";
4846
private static final String COLD_START_METRIC = "ColdStart";
4947
private static final String METRICS_DISABLED_ENV_VAR = "POWERTOOLS_METRICS_DISABLED";
5048

5149
private final software.amazon.cloudwatchlogs.emf.logger.MetricsLogger emfLogger;
5250
private final EnvironmentProvider environmentProvider;
53-
private AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false);
51+
private final AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false);
5452
private String namespace;
5553
private Map<String, String> defaultDimensions = new HashMap<>();
54+
private final Map<String, Object> properties = new HashMap<>();
5655
private final AtomicBoolean hasMetrics = new AtomicBoolean(false);
5756

5857
public EmfMetricsLogger(EnvironmentProvider environmentProvider, MetricsContext metricsContext) {
@@ -79,8 +78,6 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi
7978
dimensionSet.getDimensions().forEach((key, val) -> {
8079
try {
8180
emfDimensionSet.addDimension(key, val);
82-
// Update our local copy of default dimensions
83-
defaultDimensions.put(key, val);
8481
} catch (Exception e) {
8582
// Ignore dimension errors
8683
}
@@ -91,7 +88,8 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi
9188

9289
@Override
9390
public void addMetadata(String key, Object value) {
94-
emfLogger.putMetadata(key, value);
91+
emfLogger.putProperty(key, value);
92+
properties.put(key, value);
9593
}
9694

9795
@Override
@@ -173,45 +171,13 @@ public void flush() {
173171
public void captureColdStartMetric(Context context,
174172
software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) {
175173
if (isColdStart()) {
176-
if (isMetricsDisabled()) {
177-
LOGGER.debug("Metrics are disabled, skipping cold start metric capture");
178-
return;
179-
}
180-
181-
Validator.validateNamespace(namespace);
182-
183-
software.amazon.cloudwatchlogs.emf.logger.MetricsLogger coldStartLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger();
184-
185-
try {
186-
coldStartLogger.setNamespace(namespace);
187-
} catch (Exception e) {
188-
LOGGER.error("Namespace cannot be set for cold start metrics due to an error in EMF", e);
189-
}
190-
191-
coldStartLogger.putMetric(COLD_START_METRIC, 1, Unit.COUNT);
192-
193-
// Set dimensions if provided
194-
if (dimensions != null) {
195-
DimensionSet emfDimensionSet = new DimensionSet();
196-
dimensions.getDimensions().forEach((key, val) -> {
197-
try {
198-
emfDimensionSet.addDimension(key, val);
199-
} catch (Exception e) {
200-
// Ignore dimension errors
201-
}
202-
});
203-
coldStartLogger.setDimensions(emfDimensionSet);
204-
}
205-
206-
// Add request ID from context if available
207-
if (context != null && context.getAwsRequestId() != null) {
208-
coldStartLogger.putProperty(REQUEST_ID_PROPERTY, context.getAwsRequestId());
209-
}
210-
211-
// Add trace ID using the standard logic
212-
getXrayTraceId().ifPresent(traceId -> coldStartLogger.putProperty(TRACE_ID_PROPERTY, traceId));
213-
214-
coldStartLogger.flush();
174+
flushMetrics(metrics -> {
175+
MetricsUtils.addRequestIdAndXrayTraceIdIfAvailable(context, metrics);
176+
if (dimensions != null) {
177+
metrics.setDefaultDimensions(dimensions);
178+
}
179+
metrics.addMetric(COLD_START_METRIC, 1, MetricUnit.COUNT);
180+
});
215181
}
216182
}
217183

@@ -221,43 +187,24 @@ public void captureColdStartMetric(software.amazon.lambda.powertools.metrics.mod
221187
}
222188

223189
@Override
224-
public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace,
225-
software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) {
190+
public void flushMetrics(Consumer<Metrics> metricsConsumer) {
226191
if (isMetricsDisabled()) {
227192
LOGGER.debug("Metrics are disabled, skipping single metric flush");
228193
return;
229194
}
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);
195+
// Create a new instance, inheriting namespace, default dimensions, and metadata
196+
EmfMetricsLogger metrics = new EmfMetricsLogger(environmentProvider, new MetricsContext());
197+
if (namespace != null) {
198+
metrics.setNamespace(this.namespace);
241199
}
242-
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);
200+
if (!defaultDimensions.isEmpty()) {
201+
metrics.setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of(defaultDimensions));
257202
}
203+
properties.forEach(metrics::addMetadata);
204+
205+
metricsConsumer.accept(metrics);
258206

259-
// Flush the metric
260-
singleMetricLogger.flush();
207+
metrics.flush();
261208
}
262209

263210
private boolean isMetricsDisabled() {

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

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434

3535
@Aspect
3636
public class LambdaMetricsAspect {
37-
public static final String TRACE_ID_PROPERTY = "xray_trace_id";
38-
public static final String REQUEST_ID_PROPERTY = "function_request_id";
3937
private static final String SERVICE_DIMENSION = "Service";
4038
private static final String FUNCTION_NAME_ENV_VAR = "POWERTOOLS_METRICS_FUNCTION_NAME";
4139

@@ -90,11 +88,10 @@ public Object around(ProceedingJoinPoint pjp,
9088

9189
metricsInstance.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics());
9290

93-
// Add trace ID metadata if available
94-
LambdaHandlerProcessor.getXrayTraceId()
95-
.ifPresent(traceId -> metricsInstance.addMetadata(TRACE_ID_PROPERTY, traceId));
91+
Context extractedContext = extractContext(pjp);
92+
MetricsUtils.addRequestIdAndXrayTraceIdIfAvailable(extractedContext, metricsInstance);
9693

97-
captureColdStartMetricIfEnabled(extractContext(pjp), metrics);
94+
captureColdStartMetricIfEnabled(extractedContext, metrics);
9895

9996
try {
10097
return pjp.proceed(proceedArgs);
@@ -107,40 +104,36 @@ public Object around(ProceedingJoinPoint pjp,
107104
return pjp.proceed(proceedArgs);
108105
}
109106

110-
private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetrics metrics) {
107+
private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetrics flushMetrics) {
111108
if (extractedContext == null) {
112109
return;
113110
}
114111

115-
Metrics metricsInstance = MetricsFactory.getMetricsInstance();
116-
// This can be null e.g. during unit tests when mocking the Lambda context
117-
if (extractedContext.getAwsRequestId() != null) {
118-
metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId());
119-
}
112+
Metrics metrics = MetricsFactory.getMetricsInstance();
120113

121114
// Only capture cold start metrics if enabled on annotation
122-
if (metrics.captureColdStart()) {
115+
if (flushMetrics.captureColdStart()) {
123116
// Get function name from annotation or context
124-
String funcName = functionName(metrics, extractedContext);
117+
String funcName = functionName(flushMetrics, extractedContext);
125118

126-
DimensionSet coldStartDimensions = new DimensionSet();
119+
DimensionSet dimensionSet = new DimensionSet();
127120

128121
// Get service name from metrics instance default dimensions or fallback
129-
String serviceName = metricsInstance.getDefaultDimensions().getDimensions().getOrDefault(
122+
String serviceName = metrics.getDefaultDimensions().getDimensions().getOrDefault(
130123
SERVICE_DIMENSION,
131-
serviceNameWithFallback(metrics));
124+
serviceNameWithFallback(flushMetrics));
132125

133126
// Only add service if it is not undefined
134127
if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) {
135-
coldStartDimensions.addDimension(SERVICE_DIMENSION, serviceName);
128+
dimensionSet.addDimension(SERVICE_DIMENSION, serviceName);
136129
}
137130

138131
// Add function name
139132
if (funcName != null) {
140-
coldStartDimensions.addDimension("FunctionName", funcName);
133+
dimensionSet.addDimension("FunctionName", funcName);
141134
}
142135

143-
metricsInstance.captureColdStartMetric(extractedContext, coldStartDimensions);
136+
metrics.captureColdStartMetric(extractedContext, dimensionSet);
144137
}
145138
}
146139
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package software.amazon.lambda.powertools.metrics.internal;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import software.amazon.lambda.powertools.metrics.Metrics;
5+
6+
import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId;
7+
8+
final class MetricsUtils {
9+
private static final String TRACE_ID_PROPERTY = "xray_trace_id";
10+
private static final String REQUEST_ID_PROPERTY = "function_request_id";
11+
12+
private MetricsUtils() {
13+
// Utility class
14+
}
15+
16+
static void addRequestIdAndXrayTraceIdIfAvailable(Context context, Metrics metrics) {
17+
if (context != null && context.getAwsRequestId() != null) {
18+
metrics.addMetadata(REQUEST_ID_PROPERTY, context.getAwsRequestId());
19+
}
20+
getXrayTraceId().ifPresent(traceId -> metrics.addMetadata(TRACE_ID_PROPERTY, traceId));
21+
}
22+
}

0 commit comments

Comments
 (0)