Skip to content

Commit 35293bd

Browse files
committed
Add opt-in support for emitting exceptions as logs
1 parent 6bc49ac commit 35293bd

12 files changed

Lines changed: 402 additions & 83 deletions

File tree

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ public Instrumenter<REQUEST, RESPONSE> build() {
242242
.addAttributesExtractors(additionalExtractors)
243243
.addOperationMetrics(HttpClientMetrics.get())
244244
.setSchemaUrl(SchemaUrls.V1_37_0);
245+
Experimental.setExceptionEventName(builder, "http.client.exception");
245246
if (emitExperimentalHttpClientTelemetry) {
246247
builder
247248
.addAttributesExtractor(HttpExperimentalAttributesExtractor.create(attributesGetter))

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpServerInstrumenterBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
1818
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
1919
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
20+
import io.opentelemetry.instrumentation.api.internal.Experimental;
2021
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor;
2122
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder;
2223
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter;
@@ -214,6 +215,7 @@ public InstrumenterBuilder<REQUEST, RESPONSE> instrumenterBuilder() {
214215
.addContextCustomizer(httpServerRouteBuilder.build())
215216
.addOperationMetrics(HttpServerMetrics.get())
216217
.setSchemaUrl(SchemaUrls.V1_37_0);
218+
Experimental.setExceptionEventName(builder, "http.server.exception");
217219
if (emitExperimentalHttpServerTelemetry) {
218220
builder
219221
.addAttributesExtractor(HttpExperimentalAttributesExtractor.create(attributesGetter))

instrumentation-api/build.gradle.kts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,26 @@ tasks {
4444
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
4545
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
4646
}
47+
48+
val testStableSemconv by registering(Test::class) {
49+
testClassesDirs = sourceSets.test.get().output.classesDirs
50+
classpath = sourceSets.test.get().runtimeClasspath
51+
jvmArgs("-Dotel.semconv.exception.signal.opt-in=logs")
52+
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
53+
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
54+
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
55+
}
56+
57+
val testBothSemconv by registering(Test::class) {
58+
testClassesDirs = sourceSets.test.get().output.classesDirs
59+
classpath = sourceSets.test.get().runtimeClasspath
60+
jvmArgs("-Dotel.semconv.exception.signal.opt-in=both")
61+
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
62+
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
63+
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
64+
}
65+
66+
check {
67+
dependsOn(testStableSemconv, testBothSemconv)
68+
}
4769
}

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
package io.opentelemetry.instrumentation.api.instrumenter;
6+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs;
7+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents;
78

89
import io.opentelemetry.api.OpenTelemetry;
10+
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder;
11+
import io.opentelemetry.api.logs.LogRecordBuilder;
12+
import io.opentelemetry.api.logs.Logger;
13+
import io.opentelemetry.api.logs.Severity;
914
import io.opentelemetry.api.trace.Span;
1015
import io.opentelemetry.api.trace.SpanBuilder;
1116
import io.opentelemetry.api.trace.SpanKind;
@@ -17,6 +22,9 @@
1722
import io.opentelemetry.instrumentation.api.internal.InstrumenterContext;
1823
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
1924
import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics;
25+
import io.opentelemetry.semconv.ExceptionAttributes;
26+
import java.io.PrintWriter;
27+
import java.io.StringWriter;
2028
import java.time.Instant;
2129
import java.util.concurrent.TimeUnit;
2230
import javax.annotation.Nullable;
@@ -71,6 +79,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
7179

7280
private final String instrumentationName;
7381
private final Tracer tracer;
82+
@Nullable private final Logger logger;
7483
private final SpanNameExtractor<? super REQUEST> spanNameExtractor;
7584
private final SpanKindExtractor<? super REQUEST> spanKindExtractor;
7685
private final SpanStatusExtractor<? super REQUEST, ? super RESPONSE> spanStatusExtractor;
@@ -81,6 +90,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
8190
private final AttributesExtractor<? super REQUEST, ? super RESPONSE>[]
8291
operationListenerAttributesExtractors;
8392
private final ErrorCauseExtractor errorCauseExtractor;
93+
@Nullable private final String exceptionEventName;
8494
private final boolean propagateOperationListenersToOnEnd;
8595
private final boolean enabled;
8696
private final SpanSuppressor spanSuppressor;
@@ -90,6 +100,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
90100
Instrumenter(InstrumenterBuilder<REQUEST, RESPONSE> builder) {
91101
this.instrumentationName = builder.instrumentationName;
92102
this.tracer = builder.buildTracer();
103+
this.logger = emitExceptionAsLogs() ? builder.buildLogger() : null;
93104
this.spanNameExtractor = builder.spanNameExtractor;
94105
this.spanKindExtractor = builder.spanKindExtractor;
95106
this.spanStatusExtractor = builder.spanStatusExtractor;
@@ -100,6 +111,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
100111
this.operationListenerAttributesExtractors =
101112
builder.operationListenerAttributesExtractors.toArray(new AttributesExtractor[0]);
102113
this.errorCauseExtractor = builder.errorCauseExtractor;
114+
this.exceptionEventName = builder.exceptionEventName;
103115
this.propagateOperationListenersToOnEnd = builder.propagateOperationListenersToOnEnd;
104116
this.enabled = builder.enabled;
105117
this.spanSuppressor = builder.buildSpanSuppressor();
@@ -259,7 +271,12 @@ private void doEnd(
259271

260272
if (error != null) {
261273
error = errorCauseExtractor.extract(error);
262-
span.recordException(error);
274+
if (emitExceptionAsSpanEvents()) {
275+
span.recordException(error);
276+
}
277+
if (emitExceptionAsLogs()) {
278+
emitExceptionLog(context, error);
279+
}
263280
}
264281

265282
UnsafeAttributes attributes = new UnsafeAttributes();
@@ -300,6 +317,37 @@ private void doEnd(
300317
}
301318
}
302319

320+
private void emitExceptionLog(Context context, Throwable throwable) {
321+
if (logger == null) {
322+
// this condition is to keep nullaway happy
323+
// this can't happen since logger is non-null when stable exception semconv is enabled
324+
return;
325+
}
326+
LogRecordBuilder logRecordBuilder = logger.logRecordBuilder();
327+
logRecordBuilder.setContext(context);
328+
logRecordBuilder.setSeverity(Severity.ERROR);
329+
logRecordBuilder.setSeverityText("ERROR");
330+
if (exceptionEventName != null) {
331+
logRecordBuilder.setEventName(exceptionEventName);
332+
}
333+
334+
if (logRecordBuilder instanceof ExtendedLogRecordBuilder) {
335+
((ExtendedLogRecordBuilder) logRecordBuilder).setException(throwable);
336+
} else {
337+
logRecordBuilder.setAttribute(
338+
ExceptionAttributes.EXCEPTION_TYPE, throwable.getClass().getName());
339+
String message = throwable.getMessage();
340+
if (message != null) {
341+
logRecordBuilder.setAttribute(ExceptionAttributes.EXCEPTION_MESSAGE, message);
342+
}
343+
StringWriter writer = new StringWriter();
344+
throwable.printStackTrace(new PrintWriter(writer));
345+
logRecordBuilder.setAttribute(ExceptionAttributes.EXCEPTION_STACKTRACE, writer.toString());
346+
}
347+
348+
logRecordBuilder.emit();
349+
}
350+
303351
private static long getNanos(@Nullable Instant time) {
304352
if (time == null) {
305353
return System.nanoTime();

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import io.opentelemetry.api.OpenTelemetry;
1414
import io.opentelemetry.api.incubator.ExtendedOpenTelemetry;
1515
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
16+
import io.opentelemetry.api.logs.LoggerBuilder;
1617
import io.opentelemetry.api.metrics.Meter;
1718
import io.opentelemetry.api.metrics.MeterBuilder;
1819
import io.opentelemetry.api.trace.SpanKind;
@@ -70,6 +71,7 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
7071
SpanStatusExtractor<? super REQUEST, ? super RESPONSE> spanStatusExtractor =
7172
SpanStatusExtractor.getDefault();
7273
ErrorCauseExtractor errorCauseExtractor = ErrorCauseExtractor.getDefault();
74+
@Nullable String exceptionEventName;
7375
boolean propagateOperationListenersToOnEnd = false;
7476
boolean enabled = true;
7577

@@ -79,6 +81,9 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
7981
builder.operationListenerAttributesExtractors.add(
8082
requireNonNull(
8183
operationListenerAttributesExtractor, "operationListenerAttributesExtractor")));
84+
Experimental.internalSetExceptionEventName(
85+
(builder, exceptionEventName) ->
86+
builder.exceptionEventName = requireNonNull(exceptionEventName, "exceptionEventName"));
8287
}
8388

8489
InstrumenterBuilder(
@@ -313,6 +318,18 @@ Tracer buildTracer() {
313318
return tracerBuilder.build();
314319
}
315320

321+
io.opentelemetry.api.logs.Logger buildLogger() {
322+
LoggerBuilder loggerBuilder = openTelemetry.getLogsBridge().loggerBuilder(instrumentationName);
323+
if (instrumentationVersion != null) {
324+
loggerBuilder.setInstrumentationVersion(instrumentationVersion);
325+
}
326+
String schemaUrl = getSchemaUrl();
327+
if (schemaUrl != null) {
328+
loggerBuilder.setSchemaUrl(schemaUrl);
329+
}
330+
return loggerBuilder.build();
331+
}
332+
316333
List<OperationListener> buildOperationListeners() {
317334
// just copy the listeners list if there are no metrics registered
318335
if (operationMetrics.isEmpty()) {

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public final class Experimental {
3232
private static volatile BiConsumer<InstrumenterBuilder<?, ?>, AttributesExtractor<?, ?>>
3333
operationListenerAttributesExtractorAdder;
3434

35+
@Nullable
36+
private static volatile BiConsumer<InstrumenterBuilder<?, ?>, String> exceptionEventNameSetter;
37+
3538
private Experimental() {}
3639

3740
public static void setRedactQueryParameters(
@@ -85,4 +88,20 @@ public static <REQUEST, RESPONSE> void internalAddOperationListenerAttributesExt
8588
Experimental.operationListenerAttributesExtractorAdder =
8689
(BiConsumer) operationListenerAttributesExtractorAdder;
8790
}
91+
92+
/**
93+
* Sets the event name to use when emitting exceptions as log records. Only used when stable
94+
* exception semconv is enabled.
95+
*/
96+
public static void setExceptionEventName(
97+
InstrumenterBuilder<?, ?> builder, String exceptionEventName) {
98+
if (exceptionEventNameSetter != null) {
99+
exceptionEventNameSetter.accept(builder, exceptionEventName);
100+
}
101+
}
102+
103+
public static void internalSetExceptionEventName(
104+
BiConsumer<InstrumenterBuilder<?, ?>, String> exceptionEventNameSetter) {
105+
Experimental.exceptionEventNameSetter = exceptionEventNameSetter;
106+
}
88107
}

instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterTest.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
package io.opentelemetry.instrumentation.api.instrumenter;
7-
6+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs;
7+
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents;
88
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
99
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
1010
import static java.util.Collections.emptyMap;
@@ -14,6 +14,7 @@
1414
import io.opentelemetry.api.common.AttributeKey;
1515
import io.opentelemetry.api.common.Attributes;
1616
import io.opentelemetry.api.common.AttributesBuilder;
17+
import io.opentelemetry.api.logs.Severity;
1718
import io.opentelemetry.api.trace.Span;
1819
import io.opentelemetry.api.trace.SpanContext;
1920
import io.opentelemetry.api.trace.SpanId;
@@ -30,11 +31,14 @@
3031
import io.opentelemetry.instrumentation.api.internal.SpanKey;
3132
import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider;
3233
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
34+
import io.opentelemetry.sdk.logs.data.LogRecordData;
3335
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
3436
import io.opentelemetry.sdk.trace.data.LinkData;
3537
import io.opentelemetry.sdk.trace.data.StatusData;
38+
import io.opentelemetry.semconv.ExceptionAttributes;
3639
import java.util.Collections;
3740
import java.util.HashMap;
41+
import java.util.List;
3842
import java.util.Map;
3943
import java.util.concurrent.atomic.AtomicReference;
4044
import java.util.stream.Collectors;
@@ -228,14 +232,31 @@ void server_error() {
228232
Context context = instrumenter.start(Context.root(), REQUEST);
229233
assertThat(Span.fromContext(context).getSpanContext().isValid()).isTrue();
230234

231-
instrumenter.end(context, REQUEST, RESPONSE, new IllegalStateException("test"));
235+
IllegalStateException error = new IllegalStateException("test");
236+
instrumenter.end(context, REQUEST, RESPONSE, error);
232237

233238
otelTesting
234239
.assertTraces()
235240
.hasTracesSatisfyingExactly(
236241
trace ->
237242
trace.hasSpansSatisfyingExactly(
238-
span -> span.hasName("span").hasStatus(StatusData.error())));
243+
span -> {
244+
span.hasName("span").hasStatus(StatusData.error());
245+
if (emitExceptionAsSpanEvents()) {
246+
span.hasException(error);
247+
}
248+
}));
249+
250+
if (emitExceptionAsLogs()) {
251+
List<LogRecordData> logs = otelTesting.getLogRecords();
252+
assertThat(logs).hasSize(1);
253+
LogRecordData log = logs.get(0);
254+
assertThat(log.getSeverity()).isEqualTo(Severity.ERROR);
255+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_TYPE))
256+
.isEqualTo("java.lang.IllegalStateException");
257+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_MESSAGE)).isEqualTo("test");
258+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_STACKTRACE)).isNotNull();
259+
}
239260
}
240261

241262
@Test
@@ -337,14 +358,31 @@ void client_error() {
337358
assertThat(spanContext.isValid()).isTrue();
338359
assertThat(request).containsKey("traceparent");
339360

340-
instrumenter.end(context, request, RESPONSE, new IllegalStateException("test"));
361+
IllegalStateException error = new IllegalStateException("test");
362+
instrumenter.end(context, request, RESPONSE, error);
341363

342364
otelTesting
343365
.assertTraces()
344366
.hasTracesSatisfyingExactly(
345367
trace ->
346368
trace.hasSpansSatisfyingExactly(
347-
span -> span.hasName("span").hasStatus(StatusData.error())));
369+
span -> {
370+
span.hasName("span").hasStatus(StatusData.error());
371+
if (emitExceptionAsSpanEvents()) {
372+
span.hasException(error);
373+
}
374+
}));
375+
376+
if (emitExceptionAsLogs()) {
377+
List<LogRecordData> logs = otelTesting.getLogRecords();
378+
assertThat(logs).hasSize(1);
379+
LogRecordData log = logs.get(0);
380+
assertThat(log.getSeverity()).isEqualTo(Severity.ERROR);
381+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_TYPE))
382+
.isEqualTo("java.lang.IllegalStateException");
383+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_MESSAGE)).isEqualTo("test");
384+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_STACKTRACE)).isNotNull();
385+
}
348386
}
349387

350388
@Test

instrumentation/okhttp/okhttp-3.0/javaagent/build.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,14 @@ testing {
4242
}
4343

4444
tasks {
45+
val testStableSemconv by registering(Test::class) {
46+
testClassesDirs = sourceSets.test.get().output.classesDirs
47+
classpath = sourceSets.test.get().runtimeClasspath
48+
jvmArgs("-Dotel.semconv-stability.opt-in=exception")
49+
}
50+
4551
check {
46-
dependsOn(testing.suites)
52+
dependsOn(testing.suites, testStableSemconv)
4753
}
4854

4955
test {

instrumentation/undertow-1.4/javaagent/build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ tasks.withType<Test>().configureEach {
2525
jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true")
2626
}
2727

28+
tasks {
29+
val testStableSemconv by registering(Test::class) {
30+
testClassesDirs = sourceSets.test.get().output.classesDirs
31+
classpath = sourceSets.test.get().runtimeClasspath
32+
jvmArgs("-Dotel.semconv-stability.opt-in=exception")
33+
jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true")
34+
}
35+
36+
check {
37+
dependsOn(testStableSemconv)
38+
}
39+
}
40+
2841
// since 2.3.x, undertow is compiled by JDK 11
2942
val latestDepTest = findProperty("testLatestDeps") as Boolean
3043
if (latestDepTest) {

0 commit comments

Comments
 (0)