From ae10f710ee3b48d0c2f5e2eb9ceeaed73b388e03 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Fri, 26 Sep 2025 16:57:40 -0700 Subject: [PATCH 1/7] add compact log record exporter for lambda environment --- awsagentprovider/build.gradle.kts | 2 + ...sApplicationSignalsCustomizerProvider.java | 12 +- .../logs/CompactConsoleLogRecordExporter.java | 132 ++++++++++++++++++ ...ter.java => OtlpAwsLogRecordExporter.java} | 12 +- ...a => OtlpAwsLogRecordExporterBuilder.java} | 18 +-- ...licationSignalsCustomizerProviderTest.java | 4 +- .../otlp/aws/OtlpAwsExporterTest.java | 26 ++-- 7 files changed, 174 insertions(+), 32 deletions(-) create mode 100644 awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporter.java rename awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/{OtlpAwsLogsExporter.java => OtlpAwsLogRecordExporter.java} (91%) rename awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/{OtlpAwsLogsExporterBuilder.java => OtlpAwsLogRecordExporterBuilder.java} (70%) diff --git a/awsagentprovider/build.gradle.kts b/awsagentprovider/build.gradle.kts index 003e7d1ac8..407bb860fa 100644 --- a/awsagentprovider/build.gradle.kts +++ b/awsagentprovider/build.gradle.kts @@ -46,6 +46,8 @@ dependencies { implementation("com.amazonaws:aws-java-sdk-core:1.12.773") // Export configuration compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") + // For logging exporter + compileOnly("io.opentelemetry:opentelemetry-exporter-logging") // For Udp emitter compileOnly("io.opentelemetry:opentelemetry-exporter-otlp-common") diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index 8da1034ed6..26254d5622 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -65,8 +65,9 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.concurrent.Immutable; +import software.amazon.opentelemetry.javaagent.providers.exporter.aws.logs.CompactConsoleLogRecordExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.AwsCloudWatchEmfExporter; -import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogsExporterBuilder; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogRecordExporterBuilder; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporterBuilder; /** @@ -514,7 +515,7 @@ LogRecordExporter customizeLogsExporter( configProps.getString(OTEL_EXPORTER_OTLP_COMPRESSION_CONFIG, "none")); try { - return OtlpAwsLogsExporterBuilder.create( + return OtlpAwsLogRecordExporterBuilder.create( (OtlpHttpLogRecordExporter) logsExporter, configProps.getString(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)) .setCompression(compression) @@ -527,6 +528,13 @@ LogRecordExporter customizeLogsExporter( e); } } + String logsExporterConfig = configProps.getString(OTEL_LOGS_EXPORTER); + + if (isLambdaEnvironment() + && logsExporterConfig != null + && logsExporterConfig.equals("console")) { + return new CompactConsoleLogRecordExporter(); + } return logsExporter; } diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporter.java new file mode 100644 index 0000000000..e10540bd40 --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporter.java @@ -0,0 +1,132 @@ +/* + * Copyright 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.opentelemetry.javaagent.providers.exporter.aws.logs; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + * + * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +import io.opentelemetry.api.common.Value; +import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A compact console log exporter that changes the functionality of OpenTelemetry's {@link + * SystemOutLogRecordExporter} by removing whitespace around JSON delimiters in the printed log + * output. + * + *

This exporter uses the same formatting logic as {@code SystemOutLogRecordExporter} but applies + * compact formatting by removing spaces around characters like {@code {}[]:,} to produce more + * condensed log output. + */ +public class CompactConsoleLogRecordExporter implements LogRecordExporter { + private static final DateTimeFormatter ISO_FORMAT = DateTimeFormatter.ISO_DATE_TIME; + private final AtomicBoolean isShutdown = new AtomicBoolean(); + + private final LogRecordExporter parentExporter; + + public CompactConsoleLogRecordExporter() { + this.parentExporter = SystemOutLogRecordExporter.create(); + } + + @Override + public CompletableResultCode export(Collection logs) { + if (this.isShutdown.get()) { + return CompletableResultCode.ofFailure(); + } else { + StringBuilder stringBuilder = new StringBuilder(60); + + for (LogRecordData logRecord : logs) { + stringBuilder.setLength(0); + formatLog(stringBuilder, logRecord); + System.out.println(stringBuilder); + } + + return CompletableResultCode.ofSuccess(); + } + } + + @Override + public CompletableResultCode flush() { + return this.parentExporter.flush(); + } + + /** + * Shuts down the exporter. This method is copied and modified from + * SystemOutLogRecordExporter.shutdown(). + * + *

See: ... + */ + @Override + public CompletableResultCode shutdown() { + if (!this.isShutdown.compareAndSet(false, true)) { + System.out.println("Calling shutdown() multiple times."); + } + return CompletableResultCode.ofSuccess(); + } + + /** + * Formats log record data into a compact string representation. This method is copied from + * SystemOutLogRecordExporter.formatLog() and modified to apply compact formatting by removing + * whitespace around JSON delimiters. + * + *

See: ... + */ + static void formatLog(StringBuilder stringBuilder, LogRecordData log) { + InstrumentationScopeInfo instrumentationScopeInfo = log.getInstrumentationScopeInfo(); + Value body = log.getBodyValue(); + stringBuilder + .append( + ISO_FORMAT.format( + Instant.ofEpochMilli(TimeUnit.NANOSECONDS.toMillis(log.getTimestampEpochNanos())) + .atZone(ZoneOffset.UTC))) + .append(" ") + .append(log.getSeverity()) + .append(" '") + .append(body == null ? "" : body.asString()) + .append("' : ") + .append(log.getSpanContext().getTraceId()) + .append(" ") + .append(log.getSpanContext().getSpanId()) + .append(" [scopeInfo: ") + .append(instrumentationScopeInfo.getName()) + .append(":") + .append( + instrumentationScopeInfo.getVersion() == null + ? "" + : instrumentationScopeInfo.getVersion()) + .append("] ") + .append(log.getAttributes()); + + String compact = stringBuilder.toString().replaceAll("\\s*([{}\\[\\]:,])\\s*", "$1"); + stringBuilder.setLength(0); + stringBuilder.append(compact); + } +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporter.java similarity index 91% rename from awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporter.java rename to awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporter.java index f93b1f1c9a..5fbebb1cb1 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporter.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporter.java @@ -38,21 +38,21 @@ * documentation: "..."> */ -public final class OtlpAwsLogsExporter extends BaseOtlpAwsExporter implements LogRecordExporter { +public final class OtlpAwsLogRecordExporter extends BaseOtlpAwsExporter implements LogRecordExporter { private final OtlpHttpLogRecordExporterBuilder parentExporterBuilder; private final OtlpHttpLogRecordExporter parentExporter; - static OtlpAwsLogsExporter getDefault(String endpoint) { - return new OtlpAwsLogsExporter( + static OtlpAwsLogRecordExporter getDefault(String endpoint) { + return new OtlpAwsLogRecordExporter( OtlpHttpLogRecordExporter.getDefault(), endpoint, CompressionMethod.NONE); } - static OtlpAwsLogsExporter create( + static OtlpAwsLogRecordExporter create( OtlpHttpLogRecordExporter parent, String endpoint, CompressionMethod compression) { - return new OtlpAwsLogsExporter(parent, endpoint, compression); + return new OtlpAwsLogRecordExporter(parent, endpoint, compression); } - private OtlpAwsLogsExporter( + private OtlpAwsLogRecordExporter( OtlpHttpLogRecordExporter parentExporter, String endpoint, CompressionMethod compression) { super(endpoint, compression); diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporterBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporterBuilder.java similarity index 70% rename from awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporterBuilder.java rename to awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporterBuilder.java index bf91bd6d4e..c0c3a1f47f 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporterBuilder.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporterBuilder.java @@ -20,35 +20,35 @@ import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.CompressionMethod; -public class OtlpAwsLogsExporterBuilder { +public class OtlpAwsLogRecordExporterBuilder { private final OtlpHttpLogRecordExporter parentExporter; private final String endpoint; private String compression; - public static OtlpAwsLogsExporterBuilder create( + public static OtlpAwsLogRecordExporterBuilder create( OtlpHttpLogRecordExporter parentExporter, String endpoint) { - return new OtlpAwsLogsExporterBuilder(parentExporter, endpoint); + return new OtlpAwsLogRecordExporterBuilder(parentExporter, endpoint); } - public static OtlpAwsLogsExporter getDefault(String endpoint) { - return OtlpAwsLogsExporter.getDefault(endpoint); + public static OtlpAwsLogRecordExporter getDefault(String endpoint) { + return OtlpAwsLogRecordExporter.getDefault(endpoint); } - public OtlpAwsLogsExporterBuilder setCompression(String compression) { + public OtlpAwsLogRecordExporterBuilder setCompression(String compression) { this.compression = compression; return this; } - public OtlpAwsLogsExporter build() { + public OtlpAwsLogRecordExporter build() { CompressionMethod compression = CompressionMethod.NONE; if (this.compression != null && "gzip".equalsIgnoreCase(this.compression)) { compression = CompressionMethod.GZIP; } - return OtlpAwsLogsExporter.create(this.parentExporter, this.endpoint, compression); + return OtlpAwsLogRecordExporter.create(this.parentExporter, this.endpoint, compression); } - private OtlpAwsLogsExporterBuilder(OtlpHttpLogRecordExporter parentExporter, String endpoint) { + private OtlpAwsLogRecordExporterBuilder(OtlpHttpLogRecordExporter parentExporter, String endpoint) { this.parentExporter = requireNonNull(parentExporter, "Must set a parentExporter"); this.endpoint = requireNonNull(endpoint, "Must set an endpoint"); } diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java index 2140854d8f..b4d2032600 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java @@ -53,7 +53,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.MockedStatic; import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.AwsCloudWatchEmfExporter; -import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogsExporter; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogRecordExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporter; class AwsApplicationSignalsCustomizerProviderTest { @@ -74,7 +74,7 @@ void testShouldEnableSigV4LogsExporterIfConfigIsCorrect(Map vali validSigv4Config, defaultHttpLogsExporter, this.provider::customizeLogsExporter, - OtlpAwsLogsExporter.class); + OtlpAwsLogRecordExporter.class); } @ParameterizedTest diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/OtlpAwsExporterTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/OtlpAwsExporterTest.java index 4c809e8fec..dd8696bc04 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/OtlpAwsExporterTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/OtlpAwsExporterTest.java @@ -51,8 +51,8 @@ import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.CompressionMethod; -import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogsExporter; -import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogsExporterBuilder; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogRecordExporter; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogRecordExporterBuilder; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporterBuilder; @@ -282,7 +282,7 @@ public void export() { } } - static class OtlpAwsLogsExporterTest extends AbstractOtlpAwsExporterTest { + static class OtlpAwsLogRecordExporterTest extends AbstractOtlpAwsExporterTest { private static final String LOGS_OTLP_ENDPOINT = "https://logs.us-east-1.amazonaws.com/v1/logs"; @Mock private OtlpHttpLogRecordExporterBuilder mockBuilder; @@ -306,15 +306,15 @@ void setup() { @Test void testLogsExporterCompressionDefaultsToNone() { - OtlpAwsLogsExporter exporter = - OtlpAwsLogsExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT).build(); + OtlpAwsLogRecordExporter exporter = + OtlpAwsLogRecordExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT).build(); assertEquals(CompressionMethod.NONE, exporter.getCompression()); } @Test void testLogsExporterCompressionCanBeSetToGzip() { - OtlpAwsLogsExporter exporter = - OtlpAwsLogsExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) + OtlpAwsLogRecordExporter exporter = + OtlpAwsLogRecordExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) .setCompression("gzip") .build(); assertEquals(CompressionMethod.GZIP, exporter.getCompression()); @@ -322,8 +322,8 @@ void testLogsExporterCompressionCanBeSetToGzip() { @Test void testLogsExporterCompressionIgnoresCaseForGzip() { - OtlpAwsLogsExporter exporter = - OtlpAwsLogsExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) + OtlpAwsLogRecordExporter exporter = + OtlpAwsLogRecordExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) .setCompression("GZIP") .build(); assertEquals(CompressionMethod.GZIP, exporter.getCompression()); @@ -331,8 +331,8 @@ void testLogsExporterCompressionIgnoresCaseForGzip() { @Test void testLogsExporterCompressionDefaultsToNoneForUnknownValue() { - OtlpAwsLogsExporter exporter = - OtlpAwsLogsExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) + OtlpAwsLogRecordExporter exporter = + OtlpAwsLogRecordExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) .setCompression("unknown") .build(); assertEquals(CompressionMethod.NONE, exporter.getCompression()); @@ -343,8 +343,8 @@ private static final class MockOtlpAwsLogsExporterWrapper implements OtlpAwsExpo private MockOtlpAwsLogsExporterWrapper(OtlpHttpLogRecordExporter mockExporter) { this.exporter = - OtlpAwsLogsExporterBuilder.create( - mockExporter, OtlpAwsLogsExporterTest.LOGS_OTLP_ENDPOINT) + OtlpAwsLogRecordExporterBuilder.create( + mockExporter, OtlpAwsLogRecordExporterTest.LOGS_OTLP_ENDPOINT) .build(); } From d5be8c8156a6c8360d85249ea1b2f5ef0e9f119f Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 30 Sep 2025 19:01:35 -0700 Subject: [PATCH 2/7] test --- ...sApplicationSignalsCustomizerProvider.java | 19 +- .../logs/CompactConsoleLogRecordExporter.java | 267 ++++++++++---- .../aws/logs/OtlpAwsLogRecordExporter.java | 3 +- .../logs/OtlpAwsLogRecordExporterBuilder.java | 3 +- ...licationSignalsCustomizerProviderTest.java | 10 + .../CompactConsoleLogRecordExporterTest.java | 339 ++++++++++++++++++ lambda-layer/otel-instrument | 4 +- .../com/amazon/sampleapp/LambdaHandler.java | 22 +- 8 files changed, 586 insertions(+), 81 deletions(-) create mode 100644 awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporterTest.java diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index ce81544820..76c2c46f64 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -67,6 +67,7 @@ import javax.annotation.concurrent.Immutable; import software.amazon.opentelemetry.javaagent.providers.exporter.aws.logs.CompactConsoleLogRecordExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.AwsCloudWatchEmfExporter; +import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.ConsoleEmfExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogRecordExporterBuilder; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporterBuilder; @@ -91,7 +92,10 @@ public final class AwsApplicationSignalsCustomizerProvider // https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-envvars.html static final String AWS_REGION = "aws.region"; static final String AWS_DEFAULT_REGION = "aws.default.region"; + // TODO: We should clean up and get rid of using AWS_LAMBDA_FUNCTION_NAME and default to + // upstream config property implementation. static final String AWS_LAMBDA_FUNCTION_NAME_CONFIG = "AWS_LAMBDA_FUNCTION_NAME"; + static final String AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG = "aws.lambda.function.name"; static final String LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT = "LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT"; @@ -189,6 +193,10 @@ private static Optional getAwsRegionFromConfig(ConfigProperties configPr return Optional.ofNullable(configProps.getString(AWS_DEFAULT_REGION)); } + static boolean isLambdaEnvironment(ConfigProperties props) { + return props.getString(AWS_LAMBDA_FUNCTION_NAME_CONFIG) != null; + } + static boolean isLambdaEnvironment() { return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG) != null; } @@ -530,9 +538,10 @@ LogRecordExporter customizeLogsExporter( } String logsExporterConfig = configProps.getString(OTEL_LOGS_EXPORTER); - if (isLambdaEnvironment() + if (isLambdaEnvironment(configProps) && logsExporterConfig != null && logsExporterConfig.equals("console")) { + logger.info("INSIDE COMPACT CONSOLE LOG RECORD FUNCTION"); return new CompactConsoleLogRecordExporter(); } @@ -541,20 +550,26 @@ LogRecordExporter customizeLogsExporter( MetricExporter customizeMetricExporter( MetricExporter metricExporter, ConfigProperties configProps) { + if (isEmfExporterEnabled) { Map headers = AwsApplicationSignalsConfigUtils.parseOtlpHeaders( configProps.getString(OTEL_EXPORTER_OTLP_LOGS_HEADERS)); Optional awsRegion = getAwsRegionFromConfig(configProps); + String namespace = headers.get(AWS_EMF_METRICS_NAMESPACE); if (awsRegion.isPresent()) { if (headers.containsKey(AWS_OTLP_LOGS_GROUP_HEADER) && headers.containsKey(AWS_OTLP_LOGS_STREAM_HEADER)) { - String namespace = headers.get(AWS_EMF_METRICS_NAMESPACE); String logGroup = headers.get(AWS_OTLP_LOGS_GROUP_HEADER); String logStream = headers.get(AWS_OTLP_LOGS_STREAM_HEADER); return new AwsCloudWatchEmfExporter(namespace, logGroup, logStream, awsRegion.get()); } + + if (isLambdaEnvironment(configProps)) { + logger.info("INSIDE LAMBDA FUNCTION"); + return new ConsoleEmfExporter(namespace); + } logger.warning( String.format( "Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to have values for %s, %s, and %s", diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporter.java index e10540bd40..8a0ddf42fa 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporter.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporter.java @@ -22,111 +22,246 @@ * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. */ -import io.opentelemetry.api.common.Value; -import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import io.opentelemetry.exporter.internal.otlp.IncubatingUtil; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.resources.Resource; +import java.io.PrintStream; import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** - * A compact console log exporter that changes the functionality of OpenTelemetry's {@link - * SystemOutLogRecordExporter} by removing whitespace around JSON delimiters in the printed log - * output. - * - *

This exporter uses the same formatting logic as {@code SystemOutLogRecordExporter} but applies - * compact formatting by removing spaces around characters like {@code {}[]:,} to produce more - * condensed log output. + * A {@link LogRecordExporter} that prints {@link LogRecordData} to standard out based on upstream's + * implementation of SystemOutLogRecordExporter, see: ... */ +@SuppressWarnings("SystemOut") public class CompactConsoleLogRecordExporter implements LogRecordExporter { - private static final DateTimeFormatter ISO_FORMAT = DateTimeFormatter.ISO_DATE_TIME; + private static final DateTimeFormatter ISO_FORMAT = DateTimeFormatter.ISO_INSTANT; + private static final ObjectMapper MAPPER = + new ObjectMapper().disable(SerializationFeature.INDENT_OUTPUT); private final AtomicBoolean isShutdown = new AtomicBoolean(); - - private final LogRecordExporter parentExporter; + private final PrintStream printStream; public CompactConsoleLogRecordExporter() { - this.parentExporter = SystemOutLogRecordExporter.create(); + this(System.out); + } + + public CompactConsoleLogRecordExporter(PrintStream printStream) { + this.printStream = printStream; } @Override public CompletableResultCode export(Collection logs) { - if (this.isShutdown.get()) { + if (isShutdown.get()) { return CompletableResultCode.ofFailure(); - } else { - StringBuilder stringBuilder = new StringBuilder(60); - - for (LogRecordData logRecord : logs) { - stringBuilder.setLength(0); - formatLog(stringBuilder, logRecord); - System.out.println(stringBuilder); - } + } - return CompletableResultCode.ofSuccess(); + for (LogRecordData log : logs) { + this.printStream.println(this.toCompactJson(log)); + this.printStream.flush(); } + return CompletableResultCode.ofSuccess(); } @Override public CompletableResultCode flush() { - return this.parentExporter.flush(); + this.printStream.flush(); + return CompletableResultCode.ofSuccess(); } - /** - * Shuts down the exporter. This method is copied and modified from - * SystemOutLogRecordExporter.shutdown(). - * - *

See: ... - */ @Override public CompletableResultCode shutdown() { if (!this.isShutdown.compareAndSet(false, true)) { - System.out.println("Calling shutdown() multiple times."); + this.printStream.println("Calling shutdown() multiple times."); } return CompletableResultCode.ofSuccess(); } + @Override + public String toString() { + return "CompactConsoleLogRecordExporter{}"; + } + /** - * Formats log record data into a compact string representation. This method is copied from - * SystemOutLogRecordExporter.formatLog() and modified to apply compact formatting by removing - * whitespace around JSON delimiters. + * Converts OpenTelemetry log data to compact JSON format. OTel Java's SystemOutLogRecordExporter + * uses a concise text format, this implementation outputs a compact JSON representation based on + * OTel JavaScript's _exportInfo: ... * - *

See: ... + *

Example output: + * + *

+   *     {"body":"This is a test log","severityNumber":9,"severityText":"INFO","attributes":{},"droppedAttributes":0,"timestamp":"2025-09-30T22:37:56.724Z","observedTimestamp":"2025-09-30T22:37:56.724Z","traceId":"","spanId":"","traceFlags":0,"resource":{}}
+   * 
+ * + * @param log the log record data to convert + * @return compact JSON string representation of the log record */ - static void formatLog(StringBuilder stringBuilder, LogRecordData log) { - InstrumentationScopeInfo instrumentationScopeInfo = log.getInstrumentationScopeInfo(); - Value body = log.getBodyValue(); - stringBuilder - .append( - ISO_FORMAT.format( - Instant.ofEpochMilli(TimeUnit.NANOSECONDS.toMillis(log.getTimestampEpochNanos())) - .atZone(ZoneOffset.UTC))) - .append(" ") - .append(log.getSeverity()) - .append(" '") - .append(body == null ? "" : body.asString()) - .append("' : ") - .append(log.getSpanContext().getTraceId()) - .append(" ") - .append(log.getSpanContext().getSpanId()) - .append(" [scopeInfo: ") - .append(instrumentationScopeInfo.getName()) - .append(":") - .append( - instrumentationScopeInfo.getVersion() == null - ? "" - : instrumentationScopeInfo.getVersion()) - .append("] ") - .append(log.getAttributes()); - - String compact = stringBuilder.toString().replaceAll("\\s*([{}\\[\\]:,])\\s*", "$1"); - stringBuilder.setLength(0); - stringBuilder.append(compact); + private String toCompactJson(LogRecordData log) { + LogRecordDataTemplate template = LogRecordDataTemplate.parse(log); + + try { + return MAPPER.writeValueAsString(template); + } catch (Exception e) { + this.printStream.println("Error serializing log record: " + e.getMessage()); + return "{}"; + } + } + + /** Data object that structures OTel log record data for JSON serialization. */ + @SuppressWarnings("unused") + private static final class LogRecordDataTemplate { + @JsonProperty("resource") + private final ResourceTemplate resourceTemplate; + + @JsonProperty("body") + private final String body; + + @JsonProperty("severityNumber") + private final int severityNumber; + + @JsonProperty("severityText") + private final String severityText; + + @JsonProperty("attributes") + private final Map attributes; + + @JsonProperty("droppedAttributes") + private final int droppedAttributes; + + @JsonProperty("timestamp") + private final String timestamp; + + @JsonProperty("observedTimestamp") + private final String observedTimestamp; + + @JsonProperty("traceId") + private final String traceId; + + @JsonProperty("spanId") + private final String spanId; + + @JsonProperty("traceFlags") + private final int traceFlags; + + @JsonProperty("instrumentationScope") + private final InstrumentationScopeTemplate instrumentationScope; + + private LogRecordDataTemplate( + String body, + int severityNumber, + String severityText, + Map attributes, + int droppedAttributes, + String timestamp, + String observedTimestamp, + String traceId, + String spanId, + int traceFlags, + ResourceTemplate resourceTemplate, + InstrumentationScopeTemplate instrumentationScope) { + this.resourceTemplate = resourceTemplate; + this.body = body; + this.severityNumber = severityNumber; + this.severityText = severityText; + this.attributes = attributes; + this.droppedAttributes = droppedAttributes; + this.timestamp = timestamp; + this.observedTimestamp = observedTimestamp; + this.traceId = traceId; + this.spanId = spanId; + this.traceFlags = traceFlags; + this.instrumentationScope = instrumentationScope; + } + + private static LogRecordDataTemplate parse(LogRecordData log) { + // https://github.com/open-telemetry/opentelemetry-java/blob/48684d6d33048030b133b4f6479d45afddcdc313/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogMarshaler.java#L59 + Map attributes = new HashMap<>(); + log.getAttributes() + .forEach((key, value) -> attributes.put(key.getKey(), String.valueOf(value))); + + int attributeSize = + IncubatingUtil.isExtendedLogRecordData(log) + ? IncubatingUtil.extendedAttributesSize(log) + : log.getAttributes().size(); + + return new LogRecordDataTemplate( + log.getBodyValue() != null ? log.getBodyValue().asString() : null, + log.getSeverity().getSeverityNumber(), + log.getSeverity().name(), + attributes, + log.getTotalAttributeCount() - attributeSize, + formatTimestamp(log.getTimestampEpochNanos()), + formatTimestamp(log.getObservedTimestampEpochNanos()), + log.getSpanContext().isValid() ? log.getSpanContext().getTraceId() : "", + log.getSpanContext().isValid() ? log.getSpanContext().getSpanId() : "", + log.getSpanContext().getTraceFlags().asByte(), + ResourceTemplate.parse(log.getResource()), + InstrumentationScopeTemplate.parse(log.getInstrumentationScopeInfo())); + } + + private static String formatTimestamp(long nanos) { + return nanos != 0 + ? ISO_FORMAT.format( + Instant.ofEpochMilli(TimeUnit.NANOSECONDS.toMillis(nanos)).atZone(ZoneOffset.UTC)) + : null; + } + } + + @SuppressWarnings("unused") + private static final class ResourceTemplate { + @JsonProperty("attributes") + private final Map attributes; + + @JsonProperty("schemaUrl") + private final String schemaUrl; + + private ResourceTemplate(Map attributes, String schemaUrl) { + this.attributes = attributes; + this.schemaUrl = schemaUrl != null ? schemaUrl : ""; + } + + private static ResourceTemplate parse(Resource resource) { + Map attributes = new HashMap<>(); + resource + .getAttributes() + .forEach((key, value) -> attributes.put(key.getKey(), String.valueOf(value))); + return new ResourceTemplate(attributes, resource.getSchemaUrl()); + } + } + + @SuppressWarnings("unused") + private static final class InstrumentationScopeTemplate { + @JsonProperty("name") + private final String name; + + @JsonProperty("version") + private final String version; + + @JsonProperty("schemaUrl") + private final String schemaUrl; + + private InstrumentationScopeTemplate(String name, String version, String schemaUrl) { + this.name = name != null ? name : ""; + this.version = version != null ? version : ""; + this.schemaUrl = schemaUrl != null ? schemaUrl : ""; + } + + private static InstrumentationScopeTemplate parse(InstrumentationScopeInfo scope) { + return new InstrumentationScopeTemplate( + scope.getName(), scope.getVersion(), scope.getSchemaUrl()); + } } } diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporter.java index 5fbebb1cb1..0a74968813 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporter.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporter.java @@ -38,7 +38,8 @@ * documentation: "..."> */ -public final class OtlpAwsLogRecordExporter extends BaseOtlpAwsExporter implements LogRecordExporter { +public final class OtlpAwsLogRecordExporter extends BaseOtlpAwsExporter + implements LogRecordExporter { private final OtlpHttpLogRecordExporterBuilder parentExporterBuilder; private final OtlpHttpLogRecordExporter parentExporter; diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporterBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporterBuilder.java index c0c3a1f47f..53be35931a 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporterBuilder.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogRecordExporterBuilder.java @@ -48,7 +48,8 @@ public OtlpAwsLogRecordExporter build() { return OtlpAwsLogRecordExporter.create(this.parentExporter, this.endpoint, compression); } - private OtlpAwsLogRecordExporterBuilder(OtlpHttpLogRecordExporter parentExporter, String endpoint) { + private OtlpAwsLogRecordExporterBuilder( + OtlpHttpLogRecordExporter parentExporter, String endpoint) { this.parentExporter = requireNonNull(parentExporter, "Must set a parentExporter"); this.endpoint = requireNonNull(endpoint, "Must set an endpoint"); } diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java index 6178a8766c..36838b0ea4 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java @@ -52,6 +52,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.MockedStatic; +import software.amazon.opentelemetry.javaagent.providers.exporter.aws.logs.CompactConsoleLogRecordExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.AwsCloudWatchEmfExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogRecordExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporter; @@ -108,6 +109,15 @@ void testShouldNotUseSigv4LogsExporterIfValidatorThrows() { } } + @Test + void testLambdaEnvironmentUsesCompactLogsExporter() { + customizeExporterTest( + Map.of(OTEL_LOGS_EXPORTER, "console", AWS_LAMBDA_FUNCTION_NAME_CONFIG, "test-function"), + defaultHttpLogsExporter, + this.provider::customizeLogsExporter, + CompactConsoleLogRecordExporter.class); + } + @ParameterizedTest @MethodSource("validSigv4TracesConfigProvider") void testShouldEnableSigV4SpanExporterIfConfigIsCorrect(Map validSigv4Config) { diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporterTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporterTest.java new file mode 100644 index 0000000000..f77967d4bf --- /dev/null +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/logs/CompactConsoleLogRecordExporterTest.java @@ -0,0 +1,339 @@ +/* + * Copyright 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.opentelemetry.javaagent.providers.exporter.aws.logs; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceId; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.logs.TestLogRecordData; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class CompactConsoleLogRecordExporterTest { + + private static final String TRACE_ID_KEY = "traceId"; + private static final String SPAN_ID_KEY = "spanId"; + private static final String TRACE_FLAGS_KEY = "traceFlags"; + private static final String BODY_KEY = "body"; + private static final String SEVERITY_NUMBER_KEY = "severityNumber"; + private static final String SEVERITY_TEXT_KEY = "severityText"; + private static final String TIMESTAMP_KEY = "timestamp"; + private static final String OBSERVED_TIMESTAMP_KEY = "observedTimestamp"; + private static final String INSTRUMENTATION_SCOPE_KEY = "instrumentationScope"; + private static final String RESOURCE_KEY = "resource"; + private static final String ATTRIBUTES_KEY = "attributes"; + private static final String DROPPED_ATTRIBUTES_KEY = "droppedAttributes"; + private static final String SCOPE_NAME_KEY = "name"; + private static final String SCOPE_VERSION_KEY = "version"; + private static final String SCOPE_SCHEMA_URL_KEY = "schemaUrl"; + private static final String RESOURCE_SCHEMA_URL_KEY = "schemaUrl"; + + private OutputStream outputStream; + private PrintStream printStream; + private CompactConsoleLogRecordExporter exporter; + + @BeforeEach + void setUp() { + this.outputStream = new ByteArrayOutputStream(); + this.printStream = new PrintStream(this.outputStream); + this.exporter = new CompactConsoleLogRecordExporter(this.printStream); + } + + @Test + void testExportWithAllFieldsSet() { + SpanContext spanContext = + SpanContext.create( + "12345678901234567890123456789012", + "1234567890123456", + TraceFlags.getSampled(), + TraceState.getDefault()); + Resource resource = + Resource.empty().toBuilder() + .put("service.name", "test-service") + .setSchemaUrl("https://opentelemetry.io/schemas/1.0.0") + .build(); + InstrumentationScopeInfo scope = + InstrumentationScopeInfo.builder("test-scope") + .setVersion("1.0.0") + .setSchemaUrl("https://opentelemetry.io/schemas/1.0.0") + .build(); + + LogRecordData logRecord = + TestLogRecordData.builder() + .setResource(resource) + .setInstrumentationScopeInfo(scope) + .setBody("Test log message") + .setSeverity(Severity.INFO) + .setAttributes(Attributes.builder().put("key", "value").build()) + .setTotalAttributeCount(3) + .setTimestamp(Instant.ofEpochSecond(1000000000L)) + .setObservedTimestamp(Instant.ofEpochSecond(1000000000L)) + .setSpanContext(spanContext) + .build(); + + this.exporter.export(Collections.singletonList(logRecord)); + + validateJsonOutput(this.outputStream.toString().trim(), logRecord); + } + + @Test + void testSpanContextValidation() { + SpanContext spanContext = + SpanContext.create( + "12345678901234567890123456789012", + "1234567890123456", + TraceFlags.getSampled(), + TraceState.getDefault()); + + LogRecordData logRecord = + TestLogRecordData.builder() + .setResource(Resource.empty()) + .setInstrumentationScopeInfo(InstrumentationScopeInfo.builder("test-scope").build()) + .setBody("Test message") + .setSeverity(Severity.INFO) + .setTimestamp(Instant.ofEpochSecond(1000000000L)) + .setObservedTimestamp(Instant.ofEpochSecond(1000000000L)) + .setSpanContext(spanContext) + .build(); + + this.exporter.export(Collections.singletonList(logRecord)); + + validateJsonOutput(this.outputStream.toString().trim(), logRecord); + } + + @ParameterizedTest + @MethodSource("nullFieldsTestCases") + void testExportWithNullFields(LogRecordData logRecord) { + this.exporter.export(Collections.singletonList(logRecord)); + validateJsonOutput(this.outputStream.toString().trim(), logRecord); + } + + @Test + void testFlushBehavior() { + assertTrue(this.exporter.flush().isSuccess()); + } + + @Test + void testShutdownBehavior() { + assertTrue(this.exporter.shutdown().isSuccess()); + } + + @ParameterizedTest + @MethodSource("invalidSpanContextTestCases") + void testInvalidSpanContextExportsEmpty(String traceId, String spanId) { + SpanContext spanContext = + SpanContext.create(traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()); + + LogRecordData logRecord = + TestLogRecordData.builder() + .setResource(Resource.empty()) + .setInstrumentationScopeInfo(InstrumentationScopeInfo.builder("test-scope").build()) + .setBody("Test message") + .setSeverity(Severity.INFO) + .setTimestamp(Instant.ofEpochSecond(1000000000L)) + .setObservedTimestamp(Instant.ofEpochSecond(1000000000L)) + .setSpanContext(spanContext) + .build(); + + this.exporter.export(Collections.singletonList(logRecord)); + validateJsonOutput(this.outputStream.toString().trim(), logRecord); + } + + private static void validateJsonOutput( + String actualJsonString, LogRecordData expectedLogRecordData) { + ObjectMapper mapper = new ObjectMapper(); + Map actualParsedJson = + assertDoesNotThrow( + () -> mapper.readValue(actualJsonString, new TypeReference>() {})); + + // Validate nested objects exist + assertTrue(actualParsedJson.containsKey(INSTRUMENTATION_SCOPE_KEY)); + assertTrue(actualParsedJson.containsKey(RESOURCE_KEY)); + assertTrue(actualParsedJson.containsKey(ATTRIBUTES_KEY)); + + // Validate body field and value + assertTrue(actualParsedJson.containsKey(BODY_KEY)); + String expectedBody = + expectedLogRecordData.getBodyValue() != null + ? expectedLogRecordData.getBodyValue().asString() + : null; + assertEquals(expectedBody, actualParsedJson.get(BODY_KEY)); + + // Validate instrumentationScope structure and values + assertInstanceOf(Map.class, actualParsedJson.get(INSTRUMENTATION_SCOPE_KEY)); + Map instrumentationScope = + (Map) actualParsedJson.get(INSTRUMENTATION_SCOPE_KEY); + assertTrue(instrumentationScope.containsKey(SCOPE_NAME_KEY)); + assertTrue(instrumentationScope.containsKey(SCOPE_VERSION_KEY)); + assertTrue(instrumentationScope.containsKey(SCOPE_SCHEMA_URL_KEY)); + assertEquals( + expectedLogRecordData.getInstrumentationScopeInfo().getName(), + instrumentationScope.get(SCOPE_NAME_KEY)); + assertEquals( + expectedLogRecordData.getInstrumentationScopeInfo().getVersion() != null + ? expectedLogRecordData.getInstrumentationScopeInfo().getVersion() + : "", + instrumentationScope.get(SCOPE_VERSION_KEY)); + assertEquals( + expectedLogRecordData.getInstrumentationScopeInfo().getSchemaUrl() != null + ? expectedLogRecordData.getInstrumentationScopeInfo().getSchemaUrl() + : "", + instrumentationScope.get(SCOPE_SCHEMA_URL_KEY)); + + // Validate resource structure and values + assertInstanceOf(Map.class, actualParsedJson.get(RESOURCE_KEY)); + Map resource = (Map) actualParsedJson.get(RESOURCE_KEY); + assertTrue(resource.containsKey(ATTRIBUTES_KEY)); + assertTrue(resource.containsKey(RESOURCE_SCHEMA_URL_KEY)); + assertInstanceOf(Map.class, resource.get(ATTRIBUTES_KEY)); + assertEquals( + expectedLogRecordData.getResource().getSchemaUrl() != null + ? expectedLogRecordData.getResource().getSchemaUrl() + : "", + resource.get(RESOURCE_SCHEMA_URL_KEY)); + + // Validate attributes match expected + assertInstanceOf(Map.class, actualParsedJson.get(ATTRIBUTES_KEY)); + Map actualAttributes = + (Map) actualParsedJson.get(ATTRIBUTES_KEY); + expectedLogRecordData + .getAttributes() + .forEach( + (key, value) -> { + assertTrue(actualAttributes.containsKey(key.getKey())); + assertEquals( + String.valueOf(value), String.valueOf(actualAttributes.get(key.getKey()))); + }); + + // Validate timestamp fields and values + assertTrue(actualParsedJson.containsKey(TIMESTAMP_KEY)); + assertTrue(actualParsedJson.containsKey(OBSERVED_TIMESTAMP_KEY)); + assertEquals( + expectedLogRecordData.getTimestampEpochNanos(), + Instant.parse((String) actualParsedJson.get(TIMESTAMP_KEY)).toEpochMilli() * 1_000_000L); + assertEquals( + expectedLogRecordData.getObservedTimestampEpochNanos(), + Instant.parse((String) actualParsedJson.get(OBSERVED_TIMESTAMP_KEY)).toEpochMilli() + * 1_000_000L); + + // Validate droppedAttributes field and value + assertTrue(actualParsedJson.containsKey(DROPPED_ATTRIBUTES_KEY)); + int expectedDroppedAttributes = + expectedLogRecordData.getTotalAttributeCount() + - expectedLogRecordData.getAttributes().size(); + assertEquals(expectedDroppedAttributes, actualParsedJson.get(DROPPED_ATTRIBUTES_KEY)); + + // Validate traceId, spanId, and traceFlags fields + assertTrue(actualParsedJson.containsKey(TRACE_ID_KEY)); + assertTrue(actualParsedJson.containsKey(SPAN_ID_KEY)); + assertTrue(actualParsedJson.containsKey(TRACE_FLAGS_KEY)); + + SpanContext spanContext = expectedLogRecordData.getSpanContext(); + if (spanContext != null) { + if (TraceId.isValid(spanContext.getTraceId()) && SpanId.isValid(spanContext.getSpanId())) { + assertEquals(spanContext.getTraceId(), actualParsedJson.get(TRACE_ID_KEY)); + assertEquals(spanContext.getSpanId(), actualParsedJson.get(SPAN_ID_KEY)); + } else { + assertEquals("", actualParsedJson.get(TRACE_ID_KEY)); + assertEquals("", actualParsedJson.get(SPAN_ID_KEY)); + } + assertEquals( + (int) spanContext.getTraceFlags().asByte(), actualParsedJson.get(TRACE_FLAGS_KEY)); + } + + // Validate severity fields + assertTrue(actualParsedJson.containsKey(SEVERITY_NUMBER_KEY)); + assertTrue(actualParsedJson.containsKey(SEVERITY_TEXT_KEY)); + assertEquals( + expectedLogRecordData.getSeverity().getSeverityNumber(), + actualParsedJson.get(SEVERITY_NUMBER_KEY)); + assertEquals( + expectedLogRecordData.getSeverity().name(), actualParsedJson.get(SEVERITY_TEXT_KEY)); + } + + static Stream invalidSpanContextTestCases() { + return Stream.of( + Arguments.of( + "00000000000000000000000000000000", + "1234567890123456"), // invalid traceId, valid spanId + Arguments.of( + "12345678901234567890123456789012", + "0000000000000000"), // valid traceId, invalid spanId + Arguments.of("00000000000000000000000000000000", "0000000000000000"), // both invalid + Arguments.of( + "1234567890123456789012345678901g", "1234567890123456"), // invalid hex in traceId + Arguments.of( + "12345678901234567890123456789012", "123456789012345g") // invalid hex in spanId + ); + } + + static Stream nullFieldsTestCases() { + return Stream.of( + Arguments.of( + TestLogRecordData.builder() + .setResource(Resource.empty()) + .setInstrumentationScopeInfo(InstrumentationScopeInfo.builder("test-scope").build()) + .setBody("Test message") + .setSeverity(Severity.INFO) + .setTimestamp(Instant.ofEpochSecond(1000000000L)) + .setObservedTimestamp(Instant.ofEpochSecond(1000000000L)) + .build()), + Arguments.of( + TestLogRecordData.builder() + .setResource(Resource.empty()) + .setInstrumentationScopeInfo(InstrumentationScopeInfo.builder("test-scope").build()) + .setSeverity(Severity.INFO) + .setTimestamp(Instant.ofEpochSecond(1000000000L)) + .setObservedTimestamp(Instant.ofEpochSecond(1000000000L)) + .build()), + Arguments.of( + TestLogRecordData.builder() + .setResource(Resource.empty()) + .setInstrumentationScopeInfo(InstrumentationScopeInfo.builder("test-scope").build()) + .setBody("Test message") + .setSeverity(Severity.INFO) + .setTimestamp(Instant.ofEpochSecond(1000000000L)) + .setObservedTimestamp(Instant.ofEpochSecond(1000000000L)) + .setSpanContext( + SpanContext.create( + "abcdef1234567890abcdef1234567890", + "abcdef1234567890", + TraceFlags.getSampled(), + TraceState.getDefault())) + .build())); + } +} diff --git a/lambda-layer/otel-instrument b/lambda-layer/otel-instrument index a718c8ab75..0e0254861a 100644 --- a/lambda-layer/otel-instrument +++ b/lambda-layer/otel-instrument @@ -21,8 +21,8 @@ export OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED=false export OTEL_TRACES_EXPORTER=${OTEL_TRACES_EXPORTER:-"otlp"} # Disable metrics and logs export by default if not specified -export OTEL_METRICS_EXPORTER=${OTEL_METRICS_EXPORTER:-"none"} -export OTEL_LOGS_EXPORTER=${OTEL_LOGS_EXPORTER:-"none"} +export OTEL_METRICS_EXPORTER=${OTEL_METRICS_EXPORTER:-"awsemf"} +export OTEL_LOGS_EXPORTER=${OTEL_LOGS_EXPORTER:-"console"} # Enable Application Signals by default if not specified export OTEL_AWS_APPLICATION_SIGNALS_ENABLED=${OTEL_AWS_APPLICATION_SIGNALS_ENABLED:-"true"} diff --git a/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java b/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java index bc8a7543ac..4a06081311 100644 --- a/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java +++ b/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java @@ -9,6 +9,7 @@ import java.net.http.HttpResponse; import java.util.HashMap; import java.util.Map; +import java.util.logging.Logger; import org.json.JSONObject; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.ListBucketsResponse; @@ -16,12 +17,13 @@ public class LambdaHandler implements RequestHandler> { + private static final Logger logger = Logger.getLogger(LambdaHandler.class.getName()); HttpClient client = HttpClient.newHttpClient(); private final S3Client s3Client = S3Client.create(); @Override public Map handleRequest(Object input, Context context) { - System.out.println("Executing LambdaHandler"); + logger.info("Executing LambdaHandler"); // https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime // try and get the trace id from environment variable _X_AMZN_TRACE_ID. If it's not present @@ -32,13 +34,13 @@ public Map handleRequest(Object input, Context context) { ? System.getenv("_X_AMZN_TRACE_ID") : System.getProperty("com.amazonaws.xray.traceHeader"); - System.out.println("Trace ID: " + traceId); + logger.info("Trace ID: " + traceId); JSONObject responseBody = new JSONObject(); responseBody.put("traceId", traceId); // Make a remote call using OkHttp - System.out.println("Making a remote call using Java HttpClient"); + logger.info("Making a remote call using Java HttpClient"); String url = "https://aws.amazon.com/"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) @@ -46,24 +48,26 @@ public Map handleRequest(Object input, Context context) { .build(); try { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - System.out.println("Response status code: " + response.statusCode()); + logger.info("Response status code: " + response.statusCode()); responseBody.put("httpRequest", "Request successful"); } catch (Exception e) { - System.err.println("Error: " + e.getMessage()); + logger.severe("Error: " + e.getMessage()); responseBody.put("httpRequest", "Request failed"); } - System.out.println("Remote call done"); + logger.info("Remote call done"); // Make a S3 ListBuckets call to list the S3 buckets in the account - System.out.println("Making a S3 ListBuckets call"); + logger.info("Making a S3 ListBuckets call"); try { ListBucketsResponse listBucketsResponse = s3Client.listBuckets(); responseBody.put("s3Request", "ListBuckets successful"); } catch (S3Exception e) { - System.err.println("Error listing buckets: " + e.awsErrorDetails().errorMessage()); + logger.severe("Error listing buckets: " + e.awsErrorDetails().errorMessage()); responseBody.put("s3Request", "Error listing buckets: " + e.awsErrorDetails().errorMessage()); } - System.out.println("S3 HeadBucket call done"); + logger.info("S3 ListBuckets call done"); + + logger.info("Lambda function completed processing request successfully"); // return a response in the ApiGateway proxy format return Map.of( From 3ddfa799d5db5f1ac9a64bae405ae5ad4c38db4c Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Thu, 2 Oct 2025 14:45:52 -0700 Subject: [PATCH 3/7] add more unit tests --- ...sApplicationSignalsCustomizerProvider.java | 3 +- ...licationSignalsCustomizerProviderTest.java | 147 +++++++++++++++++- 2 files changed, 140 insertions(+), 10 deletions(-) diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index 76c2c46f64..62c1ac0943 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -194,7 +194,7 @@ private static Optional getAwsRegionFromConfig(ConfigProperties configPr } static boolean isLambdaEnvironment(ConfigProperties props) { - return props.getString(AWS_LAMBDA_FUNCTION_NAME_CONFIG) != null; + return props.getString(AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG) != null; } static boolean isLambdaEnvironment() { @@ -541,7 +541,6 @@ LogRecordExporter customizeLogsExporter( if (isLambdaEnvironment(configProps) && logsExporterConfig != null && logsExporterConfig.equals("console")) { - logger.info("INSIDE COMPACT CONSOLE LOG RECORD FUNCTION"); return new CompactConsoleLogRecordExporter(); } diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java index 36838b0ea4..f6edc610a1 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java @@ -40,6 +40,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -54,6 +55,7 @@ import org.mockito.MockedStatic; import software.amazon.opentelemetry.javaagent.providers.exporter.aws.logs.CompactConsoleLogRecordExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.AwsCloudWatchEmfExporter; +import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.ConsoleEmfExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogRecordExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporter; @@ -110,14 +112,30 @@ void testShouldNotUseSigv4LogsExporterIfValidatorThrows() { } @Test - void testLambdaEnvironmentUsesCompactLogsExporter() { + void testLambdaShouldEnableCompactLogsExporterIfConfigIsCorrect() { + Map lambdaConfig = + Map.of( + OTEL_LOGS_EXPORTER, "console", AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function"); + DefaultConfigProperties configProps = DefaultConfigProperties.createFromMap(lambdaConfig); + this.provider.customizeProperties(configProps); + customizeExporterTest( - Map.of(OTEL_LOGS_EXPORTER, "console", AWS_LAMBDA_FUNCTION_NAME_CONFIG, "test-function"), + lambdaConfig, defaultHttpLogsExporter, this.provider::customizeLogsExporter, CompactConsoleLogRecordExporter.class); } + @ParameterizedTest + @MethodSource("invalidCompactLogsConfigProvider") + void testShouldNotUseCompactLogsExporterIfConfigIsIncorrect(Map invalidConfig) { + customizeExporterTest( + invalidConfig, + defaultHttpLogsExporter, + this.provider::customizeLogsExporter, + OtlpHttpLogRecordExporter.class); + } + @ParameterizedTest @MethodSource("validSigv4TracesConfigProvider") void testShouldEnableSigV4SpanExporterIfConfigIsCorrect(Map validSigv4Config) { @@ -233,8 +251,8 @@ void testSigv4ShouldNotDisableApplicationSignalsSpanExporter() { } @ParameterizedTest - @MethodSource("validEmfConfigProvider") - void testShouldEnableEmfExporterIfConfigIsCorrect(Map validEmfConfig) { + @MethodSource("validCloudWatchEmfConfigProvider") + void testShouldEnableCloudWatchEmfExporterIfConfigIsCorrect(Map validEmfConfig) { DefaultConfigProperties configProps = DefaultConfigProperties.createFromMap(validEmfConfig); this.provider.customizeProperties(configProps); @@ -246,8 +264,41 @@ void testShouldEnableEmfExporterIfConfigIsCorrect(Map validEmfCo } @ParameterizedTest - @MethodSource("invalidEmfConfigProvider") - void testShouldNotUseEmfExporterIfConfigIsIncorrect(Map invalidEmfConfig) { + @MethodSource("validCloudWatchEmfConfigProvider") + void testLambdaShouldEnableCloudWatchEmfExporterIfConfigIsCorrect( + Map validEmfConfig) { + Map lambdaCloudWatchEmfConfig = new HashMap<>(validEmfConfig); + lambdaCloudWatchEmfConfig.put(AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function"); + DefaultConfigProperties configProps = + DefaultConfigProperties.createFromMap(lambdaCloudWatchEmfConfig); + this.provider.customizeProperties(configProps); + + customizeExporterTest( + lambdaCloudWatchEmfConfig, + defaultHttpMetricsExporter, + this.provider::customizeMetricExporter, + AwsCloudWatchEmfExporter.class); + } + + @ParameterizedTest + @MethodSource("validConsoleEmfConfigProvider") + void testLambdaShouldEnableConsoleEmfExporterIfConfigIsCorrect( + Map lambdaConsoleEmfConfig) { + DefaultConfigProperties configProps = + DefaultConfigProperties.createFromMap(lambdaConsoleEmfConfig); + this.provider.customizeProperties(configProps); + + customizeExporterTest( + lambdaConsoleEmfConfig, + defaultHttpMetricsExporter, + this.provider::customizeMetricExporter, + ConsoleEmfExporter.class); + } + + @ParameterizedTest + @MethodSource("invalidCloudWatchEmfConfigProvider") + void testShouldNotUseCloudWatchEmfExporterIfConfigIsIncorrect( + Map invalidEmfConfig) { DefaultConfigProperties configProps = DefaultConfigProperties.createFromMap(invalidEmfConfig); this.provider.customizeProperties(configProps); @@ -258,6 +309,39 @@ void testShouldNotUseEmfExporterIfConfigIsIncorrect(Map invalidE OtlpHttpMetricExporter.class); } + @ParameterizedTest + @MethodSource("invalidConsoleEmfConfigProvider") + void testShouldNotUseConsoleEmfExporterIfConfigIsIncorrect( + Map invalidConsoleEmfConfig) { + DefaultConfigProperties configProps = + DefaultConfigProperties.createFromMap(invalidConsoleEmfConfig); + this.provider.customizeProperties(configProps); + + customizeExporterTest( + invalidConsoleEmfConfig, + defaultHttpMetricsExporter, + this.provider::customizeMetricExporter, + OtlpHttpMetricExporter.class); + } + + // @ParameterizedTest + // @MethodSource("invalidConsoleEmfConfigProvider") + // void testLambdaShouldNotUseCloudWatchEmfExporterIfConfigIsIncorrect( + // Map invalidEmfConfig) { + // Map lambdaCloudWatchEmfConfig = new HashMap<>(invalidEmfConfig); + // lambdaCloudWatchEmfConfig.put(AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function"); + // + // DefaultConfigProperties configProps = + // DefaultConfigProperties.createFromMap(lambdaCloudWatchEmfConfig); + // this.provider.customizeProperties(configProps); + // + // customizeExporterTest( + // lambdaCloudWatchEmfConfig, + // defaultHttpMetricsExporter, + // this.provider::customizeMetricExporter, + // OtlpHttpMetricExporter.class); + // } + @Test void setAdaptiveSamplingConfigFromString_validConfig() throws JsonProcessingException { assertThat(AwsApplicationSignalsCustomizerProvider.parseConfigString("version: 1").getVersion()) @@ -572,7 +656,7 @@ static Stream invalidSigv4LogsConfigProvider() { return args.stream().map(Arguments::of); } - static Stream invalidEmfConfigProvider() { + static Stream invalidCloudWatchEmfConfigProvider() { List> args = new ArrayList<>(); Map wrongExporter = @@ -620,7 +704,7 @@ static Stream invalidEmfConfigProvider() { return args.stream().map(Arguments::of); } - static Stream validEmfConfigProvider() { + static Stream validCloudWatchEmfConfigProvider() { List> args = new ArrayList<>(); Map awsRegionConfig = @@ -646,4 +730,51 @@ static Stream validEmfConfigProvider() { return args.stream().map(Arguments::of); } + + static Stream invalidCompactLogsConfigProvider() { + return Stream.of( + Arguments.of(Map.of(OTEL_LOGS_EXPORTER, "console")), + Arguments.of( + Map.of( + OTEL_LOGS_EXPORTER, "otlp", AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function")), + Arguments.of( + Map.of( + OTEL_LOGS_EXPORTER, "none", AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function")), + Arguments.of(Map.of(AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function")), + Arguments.of(Map.of())); + } + + static Stream validConsoleEmfConfigProvider() { + return Stream.of( + Arguments.of( + Map.of( + OTEL_METRICS_EXPORTER, "awsemf", + OTEL_EXPORTER_OTLP_LOGS_HEADERS, "x-aws-metric-namespace=test-namespace", + AWS_REGION, "us-east-1", + AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function")), + Arguments.of( + Map.of( + OTEL_METRICS_EXPORTER, "awsemf", + OTEL_EXPORTER_OTLP_LOGS_HEADERS, "x-aws-metric-namespace=another-namespace", + AWS_DEFAULT_REGION, "us-west-2", + AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "another-function"))); + } + + static Stream invalidConsoleEmfConfigProvider() { + return Stream.of( + Arguments.of( + Map.of( + OTEL_METRICS_EXPORTER, "otlp", + OTEL_EXPORTER_OTLP_LOGS_HEADERS, "x-aws-metric-namespace=test-namespace", + AWS_REGION, "us-east-1", + AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function")), + Arguments.of( + Map.of( + OTEL_METRICS_EXPORTER, "awsemf", + AWS_REGION, "us-east-1")), + Arguments.of( + Map.of( + OTEL_METRICS_EXPORTER, "awsemf", + OTEL_EXPORTER_OTLP_LOGS_HEADERS, "x-aws-metric-namespace=test-namespace"))); + } } From be68b2a8a26a94c030f58d47672005ce708948b7 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Thu, 2 Oct 2025 14:48:30 -0700 Subject: [PATCH 4/7] remove otel logging dependency --- awsagentprovider/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/awsagentprovider/build.gradle.kts b/awsagentprovider/build.gradle.kts index 407bb860fa..003e7d1ac8 100644 --- a/awsagentprovider/build.gradle.kts +++ b/awsagentprovider/build.gradle.kts @@ -46,8 +46,6 @@ dependencies { implementation("com.amazonaws:aws-java-sdk-core:1.12.773") // Export configuration compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") - // For logging exporter - compileOnly("io.opentelemetry:opentelemetry-exporter-logging") // For Udp emitter compileOnly("io.opentelemetry:opentelemetry-exporter-otlp-common") From 05579933ceb0d26008543fadce91b43110b65efe Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Thu, 2 Oct 2025 14:56:52 -0700 Subject: [PATCH 5/7] remove change to lambda alyer --- lambda-layer/otel-instrument | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambda-layer/otel-instrument b/lambda-layer/otel-instrument index 0e0254861a..a718c8ab75 100644 --- a/lambda-layer/otel-instrument +++ b/lambda-layer/otel-instrument @@ -21,8 +21,8 @@ export OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED=false export OTEL_TRACES_EXPORTER=${OTEL_TRACES_EXPORTER:-"otlp"} # Disable metrics and logs export by default if not specified -export OTEL_METRICS_EXPORTER=${OTEL_METRICS_EXPORTER:-"awsemf"} -export OTEL_LOGS_EXPORTER=${OTEL_LOGS_EXPORTER:-"console"} +export OTEL_METRICS_EXPORTER=${OTEL_METRICS_EXPORTER:-"none"} +export OTEL_LOGS_EXPORTER=${OTEL_LOGS_EXPORTER:-"none"} # Enable Application Signals by default if not specified export OTEL_AWS_APPLICATION_SIGNALS_ENABLED=${OTEL_AWS_APPLICATION_SIGNALS_ENABLED:-"true"} From aeaf838ad43cd9f430a748292bb75dfa85998900 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Thu, 2 Oct 2025 16:30:22 -0700 Subject: [PATCH 6/7] polish unit tests --- ...sApplicationSignalsCustomizerProvider.java | 1 - ...licationSignalsCustomizerProviderTest.java | 47 ++++++++++++------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index 62c1ac0943..92c704e5aa 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -566,7 +566,6 @@ MetricExporter customizeMetricExporter( } if (isLambdaEnvironment(configProps)) { - logger.info("INSIDE LAMBDA FUNCTION"); return new ConsoleEmfExporter(namespace); } logger.warning( diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java index f6edc610a1..bcc3fed6fc 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProviderTest.java @@ -324,23 +324,23 @@ void testShouldNotUseConsoleEmfExporterIfConfigIsIncorrect( OtlpHttpMetricExporter.class); } - // @ParameterizedTest - // @MethodSource("invalidConsoleEmfConfigProvider") - // void testLambdaShouldNotUseCloudWatchEmfExporterIfConfigIsIncorrect( - // Map invalidEmfConfig) { - // Map lambdaCloudWatchEmfConfig = new HashMap<>(invalidEmfConfig); - // lambdaCloudWatchEmfConfig.put(AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function"); - // - // DefaultConfigProperties configProps = - // DefaultConfigProperties.createFromMap(lambdaCloudWatchEmfConfig); - // this.provider.customizeProperties(configProps); - // - // customizeExporterTest( - // lambdaCloudWatchEmfConfig, - // defaultHttpMetricsExporter, - // this.provider::customizeMetricExporter, - // OtlpHttpMetricExporter.class); - // } + @ParameterizedTest + @MethodSource("invalidLambdaCloudWatchEmfConfigProvider") + void testLambdaShouldNotUseCloudWatchEmfExporterIfConfigIsIncorrect( + Map invalidEmfConfig) { + Map lambdaCloudWatchEmfConfig = new HashMap<>(invalidEmfConfig); + lambdaCloudWatchEmfConfig.put(AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG, "test-function"); + + DefaultConfigProperties configProps = + DefaultConfigProperties.createFromMap(lambdaCloudWatchEmfConfig); + this.provider.customizeProperties(configProps); + + customizeExporterTest( + lambdaCloudWatchEmfConfig, + defaultHttpMetricsExporter, + this.provider::customizeMetricExporter, + OtlpHttpMetricExporter.class); + } @Test void setAdaptiveSamplingConfigFromString_validConfig() throws JsonProcessingException { @@ -777,4 +777,17 @@ static Stream invalidConsoleEmfConfigProvider() { OTEL_METRICS_EXPORTER, "awsemf", OTEL_EXPORTER_OTLP_LOGS_HEADERS, "x-aws-metric-namespace=test-namespace"))); } + + static Stream invalidLambdaCloudWatchEmfConfigProvider() { + return Stream.of( + Arguments.of( + Map.of( + OTEL_METRICS_EXPORTER, "otlp", + OTEL_EXPORTER_OTLP_LOGS_HEADERS, "x-aws-metric-namespace=test-namespace", + AWS_REGION, "us-east-1")), + Arguments.of( + Map.of( + OTEL_METRICS_EXPORTER, "awsemf", + OTEL_EXPORTER_OTLP_LOGS_HEADERS, "x-aws-metric-namespace=test-namespace"))); + } } From a5b223fc7fbe6b4b75f29e3c1f719059971677ec Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Thu, 2 Oct 2025 17:08:21 -0700 Subject: [PATCH 7/7] polish unit tests --- .../providers/AwsApplicationSignalsCustomizerProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index 92c704e5aa..301552e956 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -570,7 +570,7 @@ MetricExporter customizeMetricExporter( } logger.warning( String.format( - "Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to have values for %s, %s, and %s", + "Improper EMF Exporter configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to have values for %s, %s, and %s", AWS_OTLP_LOGS_GROUP_HEADER, AWS_OTLP_LOGS_STREAM_HEADER, AWS_EMF_METRICS_NAMESPACE)); @@ -578,7 +578,7 @@ MetricExporter customizeMetricExporter( } else { logger.warning( String.format( - "Improper configuration: AWS region not found in environment variables please set %s or %s", + "Improper EMF Exporter configuration: AWS region not found in environment variables please set %s or %s", AWS_REGION, AWS_DEFAULT_REGION)); } }