Skip to content

Commit efba62a

Browse files
authored
Emit FaaS exceptions as logs under the opt-in preview (#18892)
1 parent b31e8f9 commit efba62a

10 files changed

Lines changed: 204 additions & 23 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal;
7+
8+
import io.opentelemetry.api.logs.Severity;
9+
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
10+
import io.opentelemetry.instrumentation.api.internal.Experimental;
11+
12+
/**
13+
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
14+
* any time.
15+
*/
16+
public final class FaasExceptionEventExtractors {
17+
18+
/**
19+
* Configures the FaaS invocation exception event name and severity. Only takes effect when
20+
* emitting exceptions as logs is enabled via the {@code otel.semconv.exception.signal.preview}
21+
* flag.
22+
*/
23+
public static <REQUEST> void setFaasInvocationExceptionEventExtractor(
24+
InstrumenterBuilder<REQUEST, ?> builder) {
25+
Experimental.setExceptionEventExtractor(
26+
builder,
27+
(logRecordBuilder, context, request) -> {
28+
logRecordBuilder.setEventName("faas.invocation.exception");
29+
logRecordBuilder.setSeverity(Severity.ERROR);
30+
});
31+
}
32+
33+
private FaasExceptionEventExtractors() {}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal;
7+
8+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs;
9+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
10+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
11+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
12+
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE;
13+
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE;
14+
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE;
15+
16+
import io.opentelemetry.api.logs.Severity;
17+
import io.opentelemetry.context.Context;
18+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
19+
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
20+
import io.opentelemetry.sdk.logs.data.LogRecordData;
21+
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
22+
import java.util.List;
23+
import org.assertj.core.api.AbstractAssert;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.RegisterExtension;
26+
27+
class FaasExceptionEventExtractorsTest {
28+
29+
@RegisterExtension
30+
static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
31+
32+
@Test
33+
void faasInvocationExceptionLog() {
34+
InstrumenterBuilder<String, String> builder =
35+
Instrumenter.builder(otelTesting.getOpenTelemetry(), "test", unused -> "span");
36+
FaasExceptionEventExtractors.setFaasInvocationExceptionEventExtractor(builder);
37+
Instrumenter<String, String> instrumenter = builder.buildInstrumenter();
38+
39+
Context context = instrumenter.start(Context.root(), "request");
40+
IllegalStateException error = new IllegalStateException("test");
41+
instrumenter.end(context, "request", "response", error);
42+
43+
List<LogRecordData> logs = otelTesting.getLogRecords();
44+
if (emitExceptionAsLogs()) {
45+
assertThat(logs).hasSize(1);
46+
assertThat(logs.get(0))
47+
.hasSeverity(Severity.ERROR)
48+
.hasEventName("faas.invocation.exception")
49+
.hasAttributesSatisfyingExactly(
50+
equalTo(EXCEPTION_TYPE, "java.lang.IllegalStateException"),
51+
equalTo(EXCEPTION_MESSAGE, "test"),
52+
satisfies(EXCEPTION_STACKTRACE, AbstractAssert::isNotNull));
53+
} else {
54+
assertThat(logs).isEmpty();
55+
}
56+
}
57+
}

instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/build.gradle.kts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,25 @@ dependencies {
2020
testImplementation(project(":instrumentation:aws-lambda:aws-lambda-core-1.0:testing"))
2121
}
2222

23-
tasks.test {
24-
// required on jdk17
25-
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
26-
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
27-
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
23+
tasks {
24+
withType<Test>().configureEach {
25+
// required on jdk17
26+
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
27+
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
28+
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
2829

29-
systemProperty("collectMetadata", otelProps.collectMetadata)
30+
systemProperty("collectMetadata", otelProps.collectMetadata)
31+
}
32+
33+
val testExceptionSignalLogs by registering(Test::class) {
34+
testClassesDirs = sourceSets.test.get().output.classesDirs
35+
classpath = sourceSets.test.get().runtimeClasspath
36+
37+
jvmArgs("-Dotel.semconv.exception.signal.preview=logs")
38+
systemProperty("metadataConfig", "otel.semconv.exception.signal.preview=logs")
39+
}
40+
41+
check {
42+
dependsOn(testExceptionSignalLogs)
43+
}
3044
}

instrumentation/aws-lambda/aws-lambda-core-1.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awslambdacore/v1_0/AwsLambdaStreamHandlerTest.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55

66
package io.opentelemetry.javaagent.instrumentation.awslambdacore.v1_0;
77

8+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs;
9+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents;
10+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
811
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
912
import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID;
1013
import static java.nio.charset.StandardCharsets.UTF_8;
11-
import static org.assertj.core.api.Assertions.assertThat;
1214
import static org.assertj.core.api.Assertions.catchThrowable;
1315
import static org.mockito.Mockito.when;
1416

1517
import com.amazonaws.services.lambda.runtime.Context;
1618
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
19+
import io.opentelemetry.api.logs.Severity;
1720
import io.opentelemetry.api.trace.SpanKind;
1821
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
1922
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
@@ -86,8 +89,18 @@ void handlerTracedWithException() {
8689
span.hasName("my_function")
8790
.hasKind(SpanKind.SERVER)
8891
.hasStatus(StatusData.error())
89-
.hasException(thrown)
92+
.hasException(emitExceptionAsSpanEvents() ? thrown : null)
9093
.hasAttributesSatisfyingExactly(equalTo(FAAS_INVOCATION_ID, "1-22-333"))));
94+
95+
if (emitExceptionAsLogs()) {
96+
testing.waitAndAssertLogRecords(
97+
logRecord ->
98+
logRecord
99+
.hasSeverity(Severity.ERROR)
100+
.hasEventName("faas.invocation.exception")
101+
.hasException(thrown)
102+
.hasTotalAttributeCount(3));
103+
}
91104
}
92105

93106
private static class RequestStreamHandlerTestImpl implements RequestStreamHandler {

instrumentation/aws-lambda/aws-lambda-core-1.0/library/build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,16 @@ tasks.withType<Test>().configureEach {
3434
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
3535
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
3636
}
37+
38+
tasks {
39+
val testExceptionSignalLogs by registering(Test::class) {
40+
testClassesDirs = sourceSets.test.get().output.classesDirs
41+
classpath = sourceSets.test.get().runtimeClasspath
42+
43+
jvmArgs("-Dotel.semconv.exception.signal.preview=logs")
44+
}
45+
46+
check {
47+
dependsOn(testExceptionSignalLogs)
48+
}
49+
}

instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55

66
package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal;
77

8+
import static io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal.FaasExceptionEventExtractors.setFaasInvocationExceptionEventExtractor;
9+
810
import io.opentelemetry.api.OpenTelemetry;
911
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
12+
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
1013
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
1114
import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest;
1215

@@ -17,14 +20,16 @@
1720
public final class AwsLambdaFunctionInstrumenterFactory {
1821

1922
public static AwsLambdaFunctionInstrumenter createInstrumenter(OpenTelemetry openTelemetry) {
20-
return new AwsLambdaFunctionInstrumenter(
21-
openTelemetry,
22-
Instrumenter.builder(
23+
InstrumenterBuilder<AwsLambdaRequest, Object> builder =
24+
Instrumenter.<AwsLambdaRequest, Object>builder(
2325
openTelemetry,
2426
"io.opentelemetry.aws-lambda-core-1.0",
2527
AwsLambdaFunctionInstrumenterFactory::spanName)
26-
.addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor())
27-
.buildInstrumenter(SpanKindExtractor.alwaysServer()));
28+
.addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor());
29+
setFaasInvocationExceptionEventExtractor(builder);
30+
31+
return new AwsLambdaFunctionInstrumenter(
32+
openTelemetry, builder.buildInstrumenter(SpanKindExtractor.alwaysServer()));
2833
}
2934

3035
private static String spanName(AwsLambdaRequest input) {

instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperHttpPropagationTest.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
package io.opentelemetry.instrumentation.awslambdacore.v1_0;
77

8+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs;
9+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents;
10+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
811
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
912
import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_ACCOUNT_ID;
1013
import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_RESOURCE_ID;
1114
import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID;
1215
import static java.nio.charset.StandardCharsets.UTF_8;
13-
import static org.assertj.core.api.Assertions.assertThat;
1416
import static org.assertj.core.api.Assertions.catchThrowable;
1517
import static org.mockito.Mockito.when;
1618

@@ -19,6 +21,7 @@
1921
import com.fasterxml.jackson.core.JsonFactory;
2022
import com.fasterxml.jackson.core.JsonParser;
2123
import com.fasterxml.jackson.core.JsonToken;
24+
import io.opentelemetry.api.logs.Severity;
2225
import io.opentelemetry.api.trace.SpanKind;
2326
import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda;
2427
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
@@ -126,13 +129,23 @@ void handlerTracedWithException() {
126129
.hasTraceId("4fd0b6131f19f39af59518d127b0cafe")
127130
.hasParentSpanId("0000000000000456")
128131
.hasStatus(StatusData.error())
129-
.hasException(thrown)
132+
.hasException(emitExceptionAsSpanEvents() ? thrown : null)
130133
.hasAttributesSatisfyingExactly(
131134
equalTo(
132135
CLOUD_RESOURCE_ID,
133136
"arn:aws:lambda:us-east-1:123456789:function:test"),
134137
equalTo(CLOUD_ACCOUNT_ID, "123456789"),
135138
equalTo(FAAS_INVOCATION_ID, "1-22-333"))));
139+
140+
if (emitExceptionAsLogs()) {
141+
testing.waitAndAssertLogRecords(
142+
logRecord ->
143+
logRecord
144+
.hasSeverity(Severity.ERROR)
145+
.hasEventName("faas.invocation.exception")
146+
.hasException(thrown)
147+
.hasTotalAttributeCount(3));
148+
}
136149
}
137150

138151
public static class TestRequestHandler implements RequestStreamHandler {

instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AwsLambdaStreamWrapperTest.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55

66
package io.opentelemetry.instrumentation.awslambdacore.v1_0;
77

8+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs;
9+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents;
10+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
811
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
912
import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_ACCOUNT_ID;
1013
import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_RESOURCE_ID;
1114
import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID;
1215
import static java.nio.charset.StandardCharsets.UTF_8;
13-
import static org.assertj.core.api.Assertions.assertThat;
1416
import static org.assertj.core.api.Assertions.catchThrowable;
1517
import static org.mockito.Mockito.when;
1618

1719
import com.amazonaws.services.lambda.runtime.Context;
1820
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
21+
import io.opentelemetry.api.logs.Severity;
1922
import io.opentelemetry.api.trace.SpanKind;
2023
import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.WrappedLambda;
2124
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
@@ -107,13 +110,23 @@ void handlerTracedWithException() {
107110
span.hasName("my_function")
108111
.hasKind(SpanKind.SERVER)
109112
.hasStatus(StatusData.error())
110-
.hasException(thrown)
113+
.hasException(emitExceptionAsSpanEvents() ? thrown : null)
111114
.hasAttributesSatisfyingExactly(
112115
equalTo(
113116
CLOUD_RESOURCE_ID,
114117
"arn:aws:lambda:us-east-1:123456789:function:test"),
115118
equalTo(CLOUD_ACCOUNT_ID, "123456789"),
116119
equalTo(FAAS_INVOCATION_ID, "1-22-333"))));
120+
121+
if (emitExceptionAsLogs()) {
122+
testing.waitAndAssertLogRecords(
123+
logRecord ->
124+
logRecord
125+
.hasSeverity(Severity.ERROR)
126+
.hasEventName("faas.invocation.exception")
127+
.hasException(thrown)
128+
.hasTotalAttributeCount(3));
129+
}
117130
}
118131

119132
public static class TestRequestHandler implements RequestStreamHandler {

instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package io.opentelemetry.instrumentation.awslambdacore.v1_0;
77

8+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs;
9+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents;
810
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
911
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
1012
import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INVOCATION_ID;
@@ -13,6 +15,7 @@
1315

1416
import com.amazonaws.services.lambda.runtime.Context;
1517
import com.amazonaws.services.lambda.runtime.RequestHandler;
18+
import io.opentelemetry.api.logs.Severity;
1619
import io.opentelemetry.api.trace.SpanKind;
1720
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
1821
import io.opentelemetry.sdk.trace.data.StatusData;
@@ -84,9 +87,20 @@ void handlerTracedWithException() {
8487
span.hasName("my_function")
8588
.hasKind(SpanKind.SERVER)
8689
.hasStatus(StatusData.error())
87-
.hasException(thrown)
90+
.hasException(emitExceptionAsSpanEvents() ? thrown : null)
8891
.hasAttributesSatisfyingExactly(
8992
equalTo(FAAS_INVOCATION_ID, "1-22-333"))));
93+
94+
if (emitExceptionAsLogs()) {
95+
testing()
96+
.waitAndAssertLogRecords(
97+
logRecord ->
98+
logRecord
99+
.hasSeverity(Severity.ERROR)
100+
.hasEventName("faas.invocation.exception")
101+
.hasException(thrown)
102+
.hasTotalAttributeCount(3));
103+
}
90104
}
91105

92106
/**

instrumentation/aws-lambda/aws-lambda-events-common-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/common/v2_2/internal/AwsLambdaEventsInstrumenterFactory.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55

66
package io.opentelemetry.instrumentation.awslambdaevents.common.v2_2.internal;
77

8+
import static io.opentelemetry.instrumentation.api.incubator.semconv.faas.internal.FaasExceptionEventExtractors.setFaasInvocationExceptionEventExtractor;
9+
810
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
911
import io.opentelemetry.api.OpenTelemetry;
1012
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
13+
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
1114
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
1215
import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest;
1316
import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.AwsLambdaFunctionAttributesExtractor;
@@ -22,13 +25,15 @@ public final class AwsLambdaEventsInstrumenterFactory {
2225

2326
public static AwsLambdaFunctionInstrumenter createInstrumenter(
2427
OpenTelemetry openTelemetry, String instrumentationName, Set<String> knownMethods) {
25-
return new AwsLambdaFunctionInstrumenter(
26-
openTelemetry,
27-
Instrumenter.builder(
28+
InstrumenterBuilder<AwsLambdaRequest, Object> builder =
29+
Instrumenter.<AwsLambdaRequest, Object>builder(
2830
openTelemetry, instrumentationName, AwsLambdaEventsInstrumenterFactory::spanName)
2931
.addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor())
30-
.addAttributesExtractor(new ApiGatewayProxyAttributesExtractor(knownMethods))
31-
.buildInstrumenter(SpanKindExtractor.alwaysServer()));
32+
.addAttributesExtractor(new ApiGatewayProxyAttributesExtractor(knownMethods));
33+
setFaasInvocationExceptionEventExtractor(builder);
34+
35+
return new AwsLambdaFunctionInstrumenter(
36+
openTelemetry, builder.buildInstrumenter(SpanKindExtractor.alwaysServer()));
3237
}
3338

3439
private static String spanName(AwsLambdaRequest input) {

0 commit comments

Comments
 (0)