Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

import org.crac.Core;
import org.crac.Resource;

import software.amazon.lambda.powertools.common.internal.ClassPreLoader;
import software.amazon.lambda.powertools.common.internal.LambdaConstants;
import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
import software.amazon.lambda.powertools.metrics.internal.RequestScopedMetricsProxy;
import software.amazon.lambda.powertools.metrics.model.DimensionSet;
import software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider;
import software.amazon.lambda.powertools.metrics.provider.MetricsProvider;
Expand All @@ -28,7 +30,7 @@
*/
public final class MetricsFactory implements Resource {
private static MetricsProvider provider = new EmfMetricsProvider();
private static Metrics metrics;
private static RequestScopedMetricsProxy metricsProxy;

// Dummy instance to register MetricsFactory with CRaC
private static final MetricsFactory INSTANCE = new MetricsFactory();
Expand All @@ -44,23 +46,23 @@ public final class MetricsFactory implements Resource {
* @return the singleton Metrics instance
*/
public static synchronized Metrics getMetricsInstance() {
if (metrics == null) {
metrics = provider.getMetricsInstance();
if (metricsProxy == null) {
metricsProxy = new RequestScopedMetricsProxy(provider);

// Apply default configuration from environment variables
String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE");
if (envNamespace != null) {
metrics.setNamespace(envNamespace);
metricsProxy.setNamespace(envNamespace);
}

// Only set Service dimension if it's not the default undefined value
String serviceName = LambdaHandlerProcessor.serviceName();
if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) {
metrics.setDefaultDimensions(DimensionSet.of("Service", serviceName));
metricsProxy.setDefaultDimensions(DimensionSet.of("Service", serviceName));
}
}

return metrics;
return metricsProxy;
}

/**
Expand All @@ -73,8 +75,8 @@ public static synchronized void setMetricsProvider(MetricsProvider metricsProvid
throw new IllegalArgumentException("Metrics provider cannot be null");
}
provider = metricsProvider;
// Reset the metrics instance so it will be recreated with the new provider
metrics = null;
// Reset the metrics proxy so it will be recreated with the new provider
metricsProxy = null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates.
* Licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

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

import java.time.Instant;
import java.util.HashMap;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

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

import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
import software.amazon.lambda.powertools.metrics.Metrics;
import software.amazon.lambda.powertools.metrics.model.DimensionSet;
import software.amazon.lambda.powertools.metrics.model.MetricResolution;
import software.amazon.lambda.powertools.metrics.model.MetricUnit;
import software.amazon.lambda.powertools.metrics.provider.MetricsProvider;

public class RequestScopedMetricsProxy implements Metrics {
private static final String DEFAULT_TRACE_ID = "DEFAULT";
private final ConcurrentMap<String, Metrics> metricsMap = new ConcurrentHashMap<>();
private final MetricsProvider provider;
private final AtomicReference<String> initialNamespace = new AtomicReference<>();
private final AtomicReference<DimensionSet> initialDefaultDimensions = new AtomicReference<>();
private final AtomicBoolean initialRaiseOnEmptyMetrics = new AtomicBoolean(false);

public RequestScopedMetricsProxy(MetricsProvider provider) {
this.provider = provider;
}

private String getTraceId() {
return LambdaHandlerProcessor.getXrayTraceId().orElse(DEFAULT_TRACE_ID);
}

private Metrics getOrCreateMetrics() {
String traceId = getTraceId();
return metricsMap.computeIfAbsent(traceId, key -> {
Metrics metrics = provider.getMetricsInstance();
String namespace = initialNamespace.get();
if (namespace != null) {
metrics.setNamespace(namespace);
}
DimensionSet dimensions = initialDefaultDimensions.get();
if (dimensions != null) {
metrics.setDefaultDimensions(dimensions);
}
metrics.setRaiseOnEmptyMetrics(initialRaiseOnEmptyMetrics.get());
return metrics;
});
}

// Configuration methods - called by MetricsFactory and MetricsBuilder
// These methods DO NOT eagerly create instances because they are typically called
// outside the Lambda handler (e.g., during class initialization) potentially on a different thread.
// We delay instance creation until the first operation that needs the metrics backend (e.g., addMetric).
// See {@link software.amazon.lambda.powertools.metrics.MetricsFactory#getMetricsInstance()}
// and {@link software.amazon.lambda.powertools.metrics.MetricsBuilder#build()}

@Override
public void setNamespace(String namespace) {
this.initialNamespace.set(namespace);
Optional.ofNullable(metricsMap.get(getTraceId())).ifPresent(m -> m.setNamespace(namespace));
}

@Override
public void setDefaultDimensions(DimensionSet dimensionSet) {
if (dimensionSet == null) {
throw new IllegalArgumentException("DimensionSet cannot be null");
}
this.initialDefaultDimensions.set(dimensionSet);
Optional.ofNullable(metricsMap.get(getTraceId())).ifPresent(m -> m.setDefaultDimensions(dimensionSet));
}

@Override
public void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) {
this.initialRaiseOnEmptyMetrics.set(raiseOnEmptyMetrics);
Optional.ofNullable(metricsMap.get(getTraceId())).ifPresent(m -> m.setRaiseOnEmptyMetrics(raiseOnEmptyMetrics));
}

@Override
public DimensionSet getDefaultDimensions() {
Metrics metrics = metricsMap.get(getTraceId());
if (metrics != null) {
return metrics.getDefaultDimensions();
}
DimensionSet dimensions = initialDefaultDimensions.get();
return dimensions != null ? dimensions : DimensionSet.of(new HashMap<>());
}

// Metrics operations - these eagerly create instances

@Override
public void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution) {
getOrCreateMetrics().addMetric(key, value, unit, resolution);
}

@Override
public void addDimension(DimensionSet dimensionSet) {
getOrCreateMetrics().addDimension(dimensionSet);
}

@Override
public void setTimestamp(Instant timestamp) {
getOrCreateMetrics().setTimestamp(timestamp);
}

@Override
public void addMetadata(String key, Object value) {
getOrCreateMetrics().addMetadata(key, value);
}

@Override
public void clearDefaultDimensions() {
getOrCreateMetrics().clearDefaultDimensions();
}

@Override
public void flush() {
// Always create instance to ensure validation and warnings are triggered. E.g. when raiseOnEmptyMetrics
// is enabled.
Metrics metrics = getOrCreateMetrics();
metrics.flush();
metricsMap.remove(getTraceId());
}

@Override
public void captureColdStartMetric(Context context, DimensionSet dimensions) {
getOrCreateMetrics().captureColdStartMetric(context, dimensions);
}

@Override
public void captureColdStartMetric(DimensionSet dimensions) {
getOrCreateMetrics().captureColdStartMetric(dimensions);
}

@Override
public void flushMetrics(Consumer<Metrics> metricsConsumer) {
getOrCreateMetrics().flushMetrics(metricsConsumer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
import com.fasterxml.jackson.databind.ObjectMapper;

import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor;
import software.amazon.lambda.powertools.metrics.model.MetricUnit;
import software.amazon.lambda.powertools.common.stubs.TestLambdaContext;
import software.amazon.lambda.powertools.metrics.model.MetricUnit;

/**
* Tests to verify the hierarchy of precedence for configuration:
Expand All @@ -44,7 +44,7 @@
*/
class ConfigurationPrecedenceTest {

private final PrintStream standardOut = System.out;
private static final PrintStream STANDARD_OUT = System.out;
private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
private final ObjectMapper objectMapper = new ObjectMapper();

Expand All @@ -65,10 +65,10 @@ void setUp() throws Exception {

@AfterEach
void tearDown() throws Exception {
System.setOut(standardOut);
System.setOut(STANDARD_OUT);

// Reset the singleton state between tests
java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics");
java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metricsProxy");
field.setAccessible(true);
field.set(null, null);

Expand Down Expand Up @@ -183,7 +183,7 @@ void shouldUseDefaultsWhenNoConfiguration() throws Exception {
assertThat(rootNode.has("Service")).isFalse();
}

private static class HandlerWithMetricsAnnotation implements RequestHandler<Map<String, Object>, String> {
private static final class HandlerWithMetricsAnnotation implements RequestHandler<Map<String, Object>, String> {
@Override
@FlushMetrics(namespace = "AnnotationNamespace", service = "AnnotationService")
public String handleRequest(Map<String, Object> input, Context context) {
Expand All @@ -193,7 +193,8 @@ public String handleRequest(Map<String, Object> input, Context context) {
}
}

private static class HandlerWithDefaultMetricsAnnotation implements RequestHandler<Map<String, Object>, String> {
private static final class HandlerWithDefaultMetricsAnnotation
implements RequestHandler<Map<String, Object>, String> {
@Override
@FlushMetrics
public String handleRequest(Map<String, Object> input, Context context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import software.amazon.lambda.powertools.metrics.internal.RequestScopedMetricsProxy;
import software.amazon.lambda.powertools.metrics.model.DimensionSet;
import software.amazon.lambda.powertools.metrics.model.MetricUnit;
import software.amazon.lambda.powertools.metrics.provider.MetricsProvider;
import software.amazon.lambda.powertools.metrics.testutils.TestMetrics;
import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider;

class MetricsBuilderTest {

private final PrintStream standardOut = System.out;
private static final PrintStream STANDARD_OUT = System.out;
private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
private final ObjectMapper objectMapper = new ObjectMapper();

Expand All @@ -46,10 +46,10 @@ void setUp() {

@AfterEach
void tearDown() throws Exception {
System.setOut(standardOut);
System.setOut(STANDARD_OUT);

// Reset the singleton state between tests
java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics");
java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metricsProxy");
field.setAccessible(true);
field.set(null, null);

Expand Down Expand Up @@ -151,7 +151,7 @@ void shouldBuildWithMultipleDefaultDimensions() throws Exception {
}

@Test
void shouldBuildWithCustomMetricsProvider() {
void shouldBuildWithCustomMetricsProvider() throws Exception {
// Given
MetricsProvider testProvider = new TestMetricsProvider();

Expand All @@ -161,7 +161,13 @@ void shouldBuildWithCustomMetricsProvider() {
.build();

// Then
assertThat(metrics).isInstanceOf(TestMetrics.class);
assertThat(metrics)
.isInstanceOf(RequestScopedMetricsProxy.class);

java.lang.reflect.Field providerField = metrics.getClass().getDeclaredField("provider");
providerField.setAccessible(true);
MetricsProvider actualProvider = (MetricsProvider) providerField.get(metrics);
assertThat(actualProvider).isSameAs(testProvider);
}

@Test
Expand Down
Loading
Loading