diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java index 362a75e39..02e06dd29 100644 --- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java +++ b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java @@ -24,6 +24,7 @@ import static java.util.Optional.empty; import static java.util.Optional.of; +import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; public final class LambdaHandlerProcessor { // SERVICE_NAME cannot be final for testing purposes @@ -85,4 +86,12 @@ public static void coldStartDone() { public static boolean isSamLocal() { return "true".equals(System.getenv("AWS_SAM_LOCAL")); } + + public static Optional getXrayTraceId() { + final String X_AMZN_TRACE_ID = getenv("_X_AMZN_TRACE_ID"); + if(X_AMZN_TRACE_ID != null) { + return of(X_AMZN_TRACE_ID.split(";")[0].replace("Root=", "")); + } + return empty(); + } } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/SystemWrapper.java b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java similarity index 60% rename from powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/SystemWrapper.java rename to powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java index c521fe77f..aef64378f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/SystemWrapper.java +++ b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java @@ -1,6 +1,6 @@ -package software.amazon.lambda.powertools.logging.internal; +package software.amazon.lambda.powertools.core.internal; -class SystemWrapper { +public class SystemWrapper { private SystemWrapper() { } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java index 2727a0ca5..832ccea61 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java @@ -38,10 +38,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Optional.empty; -import static java.util.Optional.of; import static java.util.Optional.ofNullable; import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.extractContext; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.getXrayTraceId; import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; @@ -50,7 +50,6 @@ import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKey; import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKeys; import static software.amazon.lambda.powertools.logging.LoggingUtils.objectMapper; -import static software.amazon.lambda.powertools.logging.internal.SystemWrapper.getenv; @Aspect public final class LambdaLoggingAspect { @@ -199,12 +198,4 @@ private Optional asJson(final ProceedingJoinPoint pjp, private Logger logger(final ProceedingJoinPoint pjp) { return LogManager.getLogger(pjp.getSignature().getDeclaringType()); } - - private static Optional getXrayTraceId() { - final String X_AMZN_TRACE_ID = getenv("_X_AMZN_TRACE_ID"); - if(X_AMZN_TRACE_ID != null) { - return of(X_AMZN_TRACE_ID.split(";")[0].replace("Root=", "")); - } - return empty(); - } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java index 64c3f13e0..526c9bac8 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java @@ -41,6 +41,7 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.core.internal.SystemWrapper; import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled; import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabledForStream; import software.amazon.lambda.powertools.logging.handlers.PowerToolDisabled; @@ -66,7 +67,7 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; -import static software.amazon.lambda.powertools.logging.internal.SystemWrapper.getenv; +import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; class LambdaLoggingAspectTest { diff --git a/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java b/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java index 348f489da..f59658e9c 100644 --- a/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java +++ b/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java @@ -16,7 +16,7 @@ public static long dimensionsCount() { return metricsContext().getDimensions().size(); } - private static MetricsContext metricsContext() { + public static MetricsContext metricsContext() { try { Field f = metricsLogger().getClass().getDeclaredField("context"); f.setAccessible(true); diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java index e787ff588..314b53d24 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java @@ -1,10 +1,19 @@ package software.amazon.lambda.powertools.metrics; +import java.util.Optional; import java.util.function.Consumer; +import software.amazon.cloudwatchlogs.emf.config.SystemWrapper; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; +import software.amazon.cloudwatchlogs.emf.model.MetricsContext; +import software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper; import software.amazon.cloudwatchlogs.emf.model.Unit; +import static java.util.Optional.ofNullable; +import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.getXrayTraceId; +import static software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspect.REQUEST_ID_PROPERTY; +import static software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspect.TRACE_ID_PROPERTY; + /** * A class used to retrieve the instance of the {@code MetricsLogger} used by * {@code Metrics}. @@ -26,8 +35,36 @@ public static MetricsLogger metricsLogger() { return metricsLogger; } + /** + * Add and immediately flush a single metric. It will use the default namespace + * specified either on {@link Metrics} annotation or via POWERTOOLS_METRICS_NAMESPACE env var. + * It by default captures AwsRequestId as property if used together with {@link Metrics} annotation. It will also + * capture XrayTraceId as property if tracing is enabled. + * + * @param name the name of the metric + * @param value the value of the metric + * @param unit the unit type of the metric + * @param logger the MetricsLogger + */ + public static void withSingleMetric(final String name, + final double value, + final Unit unit, + final Consumer logger) { + MetricsLogger metricsLogger = new MetricsLogger(); + try { + metricsLogger.setNamespace(defaultNameSpace()); + metricsLogger.putMetric(name, value, unit); + captureRequestAndTraceId(metricsLogger); + logger.accept(metricsLogger); + } finally { + metricsLogger.flush(); + } + } + /** * Add and immediately flush a single metric. + * It by default captures AwsRequestId as property if used together with {@link Metrics} annotation. It will also + * capture XrayTraceId as property if tracing is enabled. * * @param name the name of the metric * @param value the value of the metric @@ -44,9 +81,30 @@ public static void withSingleMetric(final String name, try { metricsLogger.setNamespace(namespace); metricsLogger.putMetric(name, value, unit); + captureRequestAndTraceId(metricsLogger); logger.accept(metricsLogger); } finally { metricsLogger.flush(); } } + + private static void captureRequestAndTraceId(MetricsLogger metricsLogger) { + awsRequestId(). + ifPresent(requestId -> metricsLogger.putProperty(REQUEST_ID_PROPERTY, requestId)); + + getXrayTraceId() + .ifPresent(traceId -> metricsLogger.putProperty(TRACE_ID_PROPERTY, traceId)); + } + + private static String defaultNameSpace() { + MetricsContext context = MetricsLoggerHelper.metricsContext(); + return "aws-embedded-metrics".equals(context.getNamespace()) ? + SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE"): context.getNamespace(); + } + + private static Optional awsRequestId() { + MetricsContext context = MetricsLoggerHelper.metricsContext(); + return ofNullable(context.getProperty(REQUEST_ID_PROPERTY)) + .map(Object::toString); + } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java index 1ff04fee3..e2bf4f2db 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java @@ -10,6 +10,7 @@ import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.MetricsContext; import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.ValidationException; @@ -27,6 +28,8 @@ @Aspect public class LambdaMetricsAspect { private static final String NAMESPACE = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); + public static final String TRACE_ID_PROPERTY = "XrayTraceId"; + public static final String REQUEST_ID_PROPERTY = "AwsRequestId"; @SuppressWarnings({"EmptyMethod"}) @Pointcut("@annotation(metrics)") @@ -49,9 +52,12 @@ public Object around(ProceedingJoinPoint pjp, extractContext(pjp).ifPresent((context) -> { coldStartSingleMetricIfApplicable(context.getAwsRequestId(), context.getFunctionName(), metrics); - logger.putProperty("AwsRequestId", context.getAwsRequestId()); + logger.putProperty(REQUEST_ID_PROPERTY, context.getAwsRequestId()); }); + LambdaHandlerProcessor.getXrayTraceId() + .ifPresent(traceId -> logger.putProperty(TRACE_ID_PROPERTY, traceId)); + try { return pjp.proceed(proceedArgs); @@ -75,7 +81,7 @@ && isColdStart()) { metricsLogger.setNamespace(namespace(metrics)); metricsLogger.putMetric("ColdStart", 1, Unit.COUNT); metricsLogger.setDimensions(DimensionSet.of("Service", service(metrics), "FunctionName", functionName)); - metricsLogger.putProperty("AwsRequestId", awsRequestId); + metricsLogger.putProperty(REQUEST_ID_PROPERTY, awsRequestId); metricsLogger.flush(); } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java index 55fecb09e..7fc8529e7 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java @@ -18,6 +18,7 @@ import static java.util.Collections.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mockStatic; +import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; class MetricsLoggerTest { @@ -44,8 +45,10 @@ static void beforeAll() { @Test void singleMetricsCaptureUtility() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { + try (MockedStatic mocked = mockStatic(SystemWrapper.class); + MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, "test", metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); @@ -57,7 +60,38 @@ void singleMetricsCaptureUtility() { assertThat(logAsJson) .containsEntry("Metric1", 1.0) .containsEntry("Dimension1", "Value1") - .containsKey("_aws"); + .containsKey("_aws") + .containsEntry("XrayTraceId", "1-5759e988-bd862e3fe1be46a994272793"); + }); + } + } + + @Test + void singleMetricsCaptureUtilityWithDefaultNameSpace() { + try (MockedStatic mocked = mockStatic(SystemWrapper.class); + MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); + mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE")).thenReturn("GlobalName"); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + + MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, + metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); + + assertThat(out.toString()) + .satisfies(s -> { + Map logAsJson = readAsJson(s); + + assertThat(logAsJson) + .containsEntry("Metric1", 1.0) + .containsEntry("Dimension1", "Value1") + .containsKey("_aws") + .containsEntry("XrayTraceId", "1-5759e988-bd862e3fe1be46a994272793"); + + Map aws = (Map) logAsJson.get("_aws"); + + assertThat(aws.get("CloudWatchMetrics")) + .asString() + .contains("Namespace=GlobalName"); }); } } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java index 7bccb3f48..160109787 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java @@ -3,10 +3,12 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; +import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.metrics.Metrics; import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; public class PowertoolsMetricsEnabledHandler implements RequestHandler { @@ -16,6 +18,10 @@ public Object handleRequest(Object input, Context context) { MetricsLogger metricsLogger = metricsLogger(); metricsLogger.putMetric("Metric1", 1, Unit.BYTES); + + withSingleMetric("Metric2", 1, Unit.COUNT, + log -> log.setDimensions(DimensionSet.of("Dimension1", "Value1"))); + return null; } } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java index 60086132b..dfb67cc08 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java @@ -36,6 +36,7 @@ import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; +import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; public class LambdaMetricsAspectTest { @Mock @@ -71,13 +72,33 @@ void tearDown() { @Test public void metricsWithoutColdStart() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { + try (MockedStatic mocked = mockStatic(SystemWrapper.class); + MockedStatic internalWrapper = mockStatic(software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { + mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); + internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""); + requestHandler.handleRequest("input", context); - assertThat(out.toString()) + assertThat(out.toString().split("\n")) + .hasSize(2) .satisfies(s -> { - Map logAsJson = readAsJson(s); + Map logAsJson = readAsJson(s[0]); + + assertThat(logAsJson) + .containsEntry("Metric2", 1.0) + .containsEntry("Dimension1", "Value1") + .containsKey("_aws") + .containsEntry("XrayTraceId", "1-5759e988-bd862e3fe1be46a994272793") + .containsEntry("AwsRequestId", "123ABC"); + + Map aws = (Map) logAsJson.get("_aws"); + + assertThat(aws.get("CloudWatchMetrics")) + .asString() + .contains("Namespace=ExampleApplication"); + + logAsJson = readAsJson(s[1]); assertThat(logAsJson) .containsEntry("Metric1", 1.0)