From 2d514d3b384ddd5c3bdcf201c6bf84c879267689 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 3 Jun 2026 11:30:47 -0700 Subject: [PATCH 1/5] Add FaaS exception event extractor helper --- .../FaasExceptionEventExtractors.java | 38 +++++++++++++++++++ .../AwsLambdaFunctionInstrumenterFactory.java | 13 ++++--- .../AwsLambdaEventsInstrumenterFactory.java | 13 +++++-- 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractors.java diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractors.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractors.java new file mode 100644 index 000000000000..23c49489eea4 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractors.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.internal.Experimental; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class FaasExceptionEventExtractors { + + /** + * Configures the FaaS invocation exception event name and severity. Only takes effect when + * emitting exceptions as logs is enabled via the {@code otel.semconv.exception.signal.preview} + * flag. + */ + @CanIgnoreReturnValue + public static + InstrumenterBuilder setFaasInvocationExceptionEventExtractor( + InstrumenterBuilder builder) { + Experimental.setExceptionEventExtractor( + builder, + (logRecordBuilder, context, request) -> { + logRecordBuilder.setEventName("faas.invocation.exception"); + logRecordBuilder.setSeverity(Severity.ERROR); + }); + return builder; + } + + private FaasExceptionEventExtractors() {} +} diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java index 277c358ca8e2..664626ef3c9b 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; +import static io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal.FaasExceptionEventExtractors.setFaasInvocationExceptionEventExtractor; + import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; @@ -19,11 +21,12 @@ public final class AwsLambdaFunctionInstrumenterFactory { public static AwsLambdaFunctionInstrumenter createInstrumenter(OpenTelemetry openTelemetry) { return new AwsLambdaFunctionInstrumenter( openTelemetry, - Instrumenter.builder( - openTelemetry, - "io.opentelemetry.aws-lambda-core-1.0", - AwsLambdaFunctionInstrumenterFactory::spanName) - .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()) + setFaasInvocationExceptionEventExtractor( + Instrumenter.builder( + openTelemetry, + "io.opentelemetry.aws-lambda-core-1.0", + AwsLambdaFunctionInstrumenterFactory::spanName) + .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor())) .buildInstrumenter(SpanKindExtractor.alwaysServer())); } diff --git a/instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java b/instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java index e5d615113253..7fe10b050324 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java +++ b/instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.awslambdaevents.common.v2_2.internal; +import static io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal.FaasExceptionEventExtractors.setFaasInvocationExceptionEventExtractor; + import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -24,10 +26,13 @@ public static AwsLambdaFunctionInstrumenter createInstrumenter( OpenTelemetry openTelemetry, String instrumentationName, Set knownMethods) { return new AwsLambdaFunctionInstrumenter( openTelemetry, - Instrumenter.builder( - openTelemetry, instrumentationName, AwsLambdaEventsInstrumenterFactory::spanName) - .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()) - .addAttributesExtractor(new ApiGatewayProxyAttributesExtractor(knownMethods)) + setFaasInvocationExceptionEventExtractor( + Instrumenter.builder( + openTelemetry, + instrumentationName, + AwsLambdaEventsInstrumenterFactory::spanName) + .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()) + .addAttributesExtractor(new ApiGatewayProxyAttributesExtractor(knownMethods))) .buildInstrumenter(SpanKindExtractor.alwaysServer())); } From 91b5bfd247abf45cc29b8c987143a858be74907e Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Wed, 3 Jun 2026 17:32:01 -0700 Subject: [PATCH 2/5] Add unit and integration tests for FaaS exception event extractor --- .../FaasExceptionEventExtractorsTest.java | 57 +++++++++++++++++++ .../javaagent/build.gradle.kts | 29 ++++++++-- .../library/build.gradle.kts | 16 ++++++ .../AwsLambdaFunctionInstrumenterFactory.java | 18 +++--- .../v1_0/AbstractAwsLambdaTest.java | 48 +++++++++++++--- .../AwsLambdaEventsInstrumenterFactory.java | 18 +++--- 6 files changed, 156 insertions(+), 30 deletions(-) create mode 100644 instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractorsTest.java diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractorsTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractorsTest.java new file mode 100644 index 000000000000..93f4c9ce9c73 --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractorsTest.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal; + +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; + +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import java.util.List; +import org.assertj.core.api.AbstractAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class FaasExceptionEventExtractorsTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + @Test + void faasInvocationExceptionLog() { + InstrumenterBuilder builder = + Instrumenter.builder(otelTesting.getOpenTelemetry(), "test", unused -> "span"); + FaasExceptionEventExtractors.setFaasInvocationExceptionEventExtractor(builder); + Instrumenter instrumenter = builder.buildInstrumenter(); + + Context context = instrumenter.start(Context.root(), "request"); + IllegalStateException error = new IllegalStateException("test"); + instrumenter.end(context, "request", "response", error); + + List logs = otelTesting.getLogRecords(); + if (emitExceptionAsLogs()) { + assertThat(logs).hasSize(1); + assertThat(logs.get(0)) + .hasSeverity(Severity.ERROR) + .hasEventName("faas.invocation.exception") + .hasAttributesSatisfyingExactly( + equalTo(EXCEPTION_TYPE, "java.lang.IllegalStateException"), + equalTo(EXCEPTION_MESSAGE, "test"), + satisfies(EXCEPTION_STACKTRACE, AbstractAssert::isNotNull)); + } else { + assertThat(logs).isEmpty(); + } + } +} diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts index 0c71782903d3..029494eb4eef 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts @@ -20,11 +20,28 @@ dependencies { testImplementation(project(":instrumentation:aws-lambda:aws-lambda-core-1.0:testing")) } -tasks.test { - // required on jdk17 - jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") - jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") - jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") +tasks { + withType().configureEach { + // required on jdk17 + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") + jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") - systemProperty("collectMetadata", otelProps.collectMetadata) + systemProperty("collectMetadata", otelProps.collectMetadata) + } + + val testExceptionSignalLogs by registering(Test::class) { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + + filter { + includeTestsMatching("AwsLambdaTest") + } + jvmArgs("-Dotel.semconv.exception.signal.preview=logs") + systemProperty("metadataConfig", "otel.semconv.exception.signal.preview=logs") + } + + check { + dependsOn(testExceptionSignalLogs) + } } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts index df605add2f71..da45702a2716 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts @@ -34,3 +34,19 @@ tasks.withType().configureEach { jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED") jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") } + +tasks { + val testExceptionSignalLogs by registering(Test::class) { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + + filter { + includeTestsMatching("AwsLambdaTest") + } + jvmArgs("-Dotel.semconv.exception.signal.preview=logs") + } + + check { + dependsOn(testExceptionSignalLogs) + } +} diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java index 664626ef3c9b..4f97ac1600af 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java @@ -9,6 +9,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; @@ -19,15 +20,16 @@ public final class AwsLambdaFunctionInstrumenterFactory { public static AwsLambdaFunctionInstrumenter createInstrumenter(OpenTelemetry openTelemetry) { + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, + "io.opentelemetry.aws-lambda-core-1.0", + AwsLambdaFunctionInstrumenterFactory::spanName) + .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()); + setFaasInvocationExceptionEventExtractor(builder); + return new AwsLambdaFunctionInstrumenter( - openTelemetry, - setFaasInvocationExceptionEventExtractor( - Instrumenter.builder( - openTelemetry, - "io.opentelemetry.aws-lambda-core-1.0", - AwsLambdaFunctionInstrumenterFactory::spanName) - .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor())) - .buildInstrumenter(SpanKindExtractor.alwaysServer())); + openTelemetry, builder.buildInstrumenter(SpanKindExtractor.alwaysServer())); } private static String spanName(AwsLambdaRequest input) { diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java index 209a0bd31a95..5b94a082e100 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java @@ -5,17 +5,28 @@ package io.opentelemetry.instrumentation.awslambdacore.v1_0; +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs; +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID; +import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.when; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; +import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.trace.data.StatusData; +import java.util.List; +import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -80,13 +91,36 @@ void handlerTracedWithException() { .waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( - span -> - span.hasName("my_function") - .hasKind(SpanKind.SERVER) - .hasStatus(StatusData.error()) - .hasException(thrown) - .hasAttributesSatisfyingExactly( - equalTo(FAAS_INVOCATION_ID, "1-22-333")))); + span -> { + span.hasName("my_function") + .hasKind(SpanKind.SERVER) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly(equalTo(FAAS_INVOCATION_ID, "1-22-333")); + span.hasException(emitExceptionAsSpanEvents() ? thrown : null); + })); + + if (emitExceptionAsLogs()) { + assertInvocationExceptionLog(); + } + } + + private void assertInvocationExceptionLog() { + Awaitility.await() + .untilAsserted( + () -> { + List logs = + testing().logRecords().stream() + .filter(log -> "faas.invocation.exception".equals(log.getEventName())) + .collect(toList()); + assertThat(logs).hasSize(1); + assertThat(logs.get(0)) + .hasSeverity(Severity.ERROR) + .hasEventName("faas.invocation.exception") + .hasAttributesSatisfyingExactly( + satisfies(EXCEPTION_TYPE, val -> val.isNotNull()), + satisfies(EXCEPTION_MESSAGE, val -> val.isNotNull()), + satisfies(EXCEPTION_STACKTRACE, val -> val.isNotNull())); + }); } /** diff --git a/instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java b/instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java index 7fe10b050324..cf3d676e9e6c 100644 --- a/instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java +++ b/instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java @@ -10,6 +10,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.AwsLambdaFunctionAttributesExtractor; @@ -24,16 +25,15 @@ public final class AwsLambdaEventsInstrumenterFactory { public static AwsLambdaFunctionInstrumenter createInstrumenter( OpenTelemetry openTelemetry, String instrumentationName, Set knownMethods) { + InstrumenterBuilder builder = + Instrumenter.builder( + openTelemetry, instrumentationName, AwsLambdaEventsInstrumenterFactory::spanName) + .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()) + .addAttributesExtractor(new ApiGatewayProxyAttributesExtractor(knownMethods)); + setFaasInvocationExceptionEventExtractor(builder); + return new AwsLambdaFunctionInstrumenter( - openTelemetry, - setFaasInvocationExceptionEventExtractor( - Instrumenter.builder( - openTelemetry, - instrumentationName, - AwsLambdaEventsInstrumenterFactory::spanName) - .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()) - .addAttributesExtractor(new ApiGatewayProxyAttributesExtractor(knownMethods))) - .buildInstrumenter(SpanKindExtractor.alwaysServer())); + openTelemetry, builder.buildInstrumenter(SpanKindExtractor.alwaysServer())); } private static String spanName(AwsLambdaRequest input) { From b35fb2f577734a2a138346f8fc8ad743d1d27ad4 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 5 Jun 2026 13:51:18 -0700 Subject: [PATCH 3/5] Simplify AWS Lambda exception log assertions --- .../javaagent/build.gradle.kts | 3 -- .../v1_0/AwsLambdaStreamHandlerTest.java | 17 +++++++-- .../library/build.gradle.kts | 3 -- ...ambdaStreamWrapperHttpPropagationTest.java | 17 +++++++-- .../v1_0/AwsLambdaStreamWrapperTest.java | 17 +++++++-- .../v1_0/AbstractAwsLambdaTest.java | 36 +++++-------------- 6 files changed, 53 insertions(+), 40 deletions(-) diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts index 029494eb4eef..f4625ed2c3c4 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts @@ -34,9 +34,6 @@ tasks { testClassesDirs = sourceSets.test.get().output.classesDirs classpath = sourceSets.test.get().runtimeClasspath - filter { - includeTestsMatching("AwsLambdaTest") - } jvmArgs("-Dotel.semconv.exception.signal.preview=logs") systemProperty("metadataConfig", "otel.semconv.exception.signal.preview=logs") } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaStreamHandlerTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaStreamHandlerTest.java index 30538b1ff5ab..acb2c8cc7470 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaStreamHandlerTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaStreamHandlerTest.java @@ -5,15 +5,18 @@ package io.opentelemetry.javaagent.instrumentation.awslambdacore.v1_0; +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs; +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.when; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; @@ -86,8 +89,18 @@ void handlerTracedWithException() { span.hasName("my_function") .hasKind(SpanKind.SERVER) .hasStatus(StatusData.error()) - .hasException(thrown) + .hasException(emitExceptionAsSpanEvents() ? thrown : null) .hasAttributesSatisfyingExactly(equalTo(FAAS_INVOCATION_ID, "1-22-333")))); + + if (emitExceptionAsLogs()) { + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasSeverity(Severity.ERROR) + .hasEventName("faas.invocation.exception") + .hasException(thrown) + .hasTotalAttributeCount(3)); + } } private static class RequestStreamHandlerTestImpl implements RequestStreamHandler { diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts index da45702a2716..d4848deb307a 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts @@ -40,9 +40,6 @@ tasks { testClassesDirs = sourceSets.test.get().output.classesDirs classpath = sourceSets.test.get().runtimeClasspath - filter { - includeTestsMatching("AwsLambdaTest") - } jvmArgs("-Dotel.semconv.exception.signal.preview=logs") } diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java index 5394d7dd5e01..fc4e5e1bdb0d 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java @@ -5,12 +5,14 @@ package io.opentelemetry.instrumentation.awslambdacore.v1_0; +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs; +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_ACCOUNT_ID; import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_RESOURCE_ID; import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.when; @@ -19,6 +21,7 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; +import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; @@ -126,13 +129,23 @@ void handlerTracedWithException() { .hasTraceId("4fd0b6131f19f39af59518d127b0cafe") .hasParentSpanId("0000000000000456") .hasStatus(StatusData.error()) - .hasException(thrown) + .hasException(emitExceptionAsSpanEvents() ? thrown : null) .hasAttributesSatisfyingExactly( equalTo( CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), equalTo(CLOUD_ACCOUNT_ID, "123456789"), equalTo(FAAS_INVOCATION_ID, "1-22-333")))); + + if (emitExceptionAsLogs()) { + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasSeverity(Severity.ERROR) + .hasEventName("faas.invocation.exception") + .hasException(thrown) + .hasTotalAttributeCount(3)); + } } public static class TestRequestHandler implements RequestStreamHandler { diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java index 08b024f38441..a3c80e1d2237 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java @@ -5,17 +5,20 @@ package io.opentelemetry.instrumentation.awslambdacore.v1_0; +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs; +import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_ACCOUNT_ID; import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_RESOURCE_ID; import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.when; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; @@ -107,13 +110,23 @@ void handlerTracedWithException() { span.hasName("my_function") .hasKind(SpanKind.SERVER) .hasStatus(StatusData.error()) - .hasException(thrown) + .hasException(emitExceptionAsSpanEvents() ? thrown : null) .hasAttributesSatisfyingExactly( equalTo( CLOUD_RESOURCE_ID, "arn:aws:lambda:us-east-1:123456789:function:test"), equalTo(CLOUD_ACCOUNT_ID, "123456789"), equalTo(FAAS_INVOCATION_ID, "1-22-333")))); + + if (emitExceptionAsLogs()) { + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasSeverity(Severity.ERROR) + .hasEventName("faas.invocation.exception") + .hasException(thrown) + .hasTotalAttributeCount(3)); + } } public static class TestRequestHandler implements RequestStreamHandler { diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java index 5b94a082e100..cec29bc2a82b 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java @@ -9,12 +9,7 @@ import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; -import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; -import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; -import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID; -import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.when; @@ -23,10 +18,7 @@ import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.trace.data.StatusData; -import java.util.List; -import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -100,29 +92,17 @@ void handlerTracedWithException() { })); if (emitExceptionAsLogs()) { - assertInvocationExceptionLog(); + testing() + .waitAndAssertLogRecords( + logRecord -> + logRecord + .hasSeverity(Severity.ERROR) + .hasEventName("faas.invocation.exception") + .hasException(thrown) + .hasTotalAttributeCount(3)); } } - private void assertInvocationExceptionLog() { - Awaitility.await() - .untilAsserted( - () -> { - List logs = - testing().logRecords().stream() - .filter(log -> "faas.invocation.exception".equals(log.getEventName())) - .collect(toList()); - assertThat(logs).hasSize(1); - assertThat(logs.get(0)) - .hasSeverity(Severity.ERROR) - .hasEventName("faas.invocation.exception") - .hasAttributesSatisfyingExactly( - satisfies(EXCEPTION_TYPE, val -> val.isNotNull()), - satisfies(EXCEPTION_MESSAGE, val -> val.isNotNull()), - satisfies(EXCEPTION_STACKTRACE, val -> val.isNotNull())); - }); - } - /** * For more details about active tracing see * https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html From ee3b12120a5ec032777fdcb68787e71a60bd7f5a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 5 Jun 2026 17:04:07 -0700 Subject: [PATCH 4/5] simplify --- .../awslambdacore/v1_0/AbstractAwsLambdaTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java index cec29bc2a82b..a5a2fd2040c7 100644 --- a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java +++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java @@ -83,13 +83,13 @@ void handlerTracedWithException() { .waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( - span -> { - span.hasName("my_function") - .hasKind(SpanKind.SERVER) - .hasStatus(StatusData.error()) - .hasAttributesSatisfyingExactly(equalTo(FAAS_INVOCATION_ID, "1-22-333")); - span.hasException(emitExceptionAsSpanEvents() ? thrown : null); - })); + span -> + span.hasName("my_function") + .hasKind(SpanKind.SERVER) + .hasStatus(StatusData.error()) + .hasException(emitExceptionAsSpanEvents() ? thrown : null) + .hasAttributesSatisfyingExactly( + equalTo(FAAS_INVOCATION_ID, "1-22-333")))); if (emitExceptionAsLogs()) { testing() From fb8c80f00da0fc7ba6a8a6d6b729eedf91b8bc4f Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Sat, 6 Jun 2026 16:34:01 -0700 Subject: [PATCH 5/5] Address review comment: relax FaaS helper builder type --- .../faas/internal/FaasExceptionEventExtractors.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractors.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractors.java index 23c49489eea4..0d780d5d043c 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractors.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/faas/internal/FaasExceptionEventExtractors.java @@ -5,7 +5,6 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal; -import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; import io.opentelemetry.instrumentation.api.internal.Experimental; @@ -21,17 +20,14 @@ public final class FaasExceptionEventExtractors { * emitting exceptions as logs is enabled via the {@code otel.semconv.exception.signal.preview} * flag. */ - @CanIgnoreReturnValue - public static - InstrumenterBuilder setFaasInvocationExceptionEventExtractor( - InstrumenterBuilder builder) { + public static void setFaasInvocationExceptionEventExtractor( + InstrumenterBuilder builder) { Experimental.setExceptionEventExtractor( builder, (logRecordBuilder, context, request) -> { logRecordBuilder.setEventName("faas.invocation.exception"); logRecordBuilder.setSeverity(Severity.ERROR); }); - return builder; } private FaasExceptionEventExtractors() {}