Skip to content

Commit 50a677c

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

13 files changed

Lines changed: 423 additions & 76 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-stability.opt-in=exception")
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-stability.opt-in=exception/dup")
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: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
package io.opentelemetry.instrumentation.api.instrumenter;
77

88
import io.opentelemetry.api.OpenTelemetry;
9+
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder;
10+
import io.opentelemetry.api.logs.LogRecordBuilder;
11+
import io.opentelemetry.api.logs.Logger;
12+
import io.opentelemetry.api.logs.Severity;
913
import io.opentelemetry.api.trace.Span;
1014
import io.opentelemetry.api.trace.SpanBuilder;
1115
import io.opentelemetry.api.trace.SpanKind;
@@ -16,7 +20,11 @@
1620
import io.opentelemetry.instrumentation.api.internal.InstrumenterAccess;
1721
import io.opentelemetry.instrumentation.api.internal.InstrumenterContext;
1822
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
23+
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
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 = SemconvStability.emitStableExceptionSemconv() ? 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 (SemconvStability.emitOldExceptionSemconv()) {
275+
span.recordException(error);
276+
}
277+
if (SemconvStability.emitStableExceptionSemconv()) {
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/main/java/io/opentelemetry/instrumentation/api/internal/SemconvStability.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public final class SemconvStability {
2727
private static final boolean emitOldServicePeerSemconv;
2828
private static final boolean emitStableServicePeerSemconv;
2929

30+
private static final boolean emitOldExceptionSemconv;
31+
private static final boolean emitStableExceptionSemconv;
32+
3033
static {
3134
boolean oldDatabase = true;
3235
boolean stableDatabase = false;
@@ -37,6 +40,9 @@ public final class SemconvStability {
3740
boolean oldServicePeer = true;
3841
boolean stableServicePeer = false;
3942

43+
boolean oldException = true;
44+
boolean stableException = false;
45+
4046
String value = System.getProperty("otel.semconv-stability.opt-in");
4147
if (value == null) {
4248
value = System.getenv("OTEL_SEMCONV_STABILITY_OPT_IN");
@@ -73,6 +79,15 @@ public final class SemconvStability {
7379
oldServicePeer = true;
7480
stableServicePeer = true;
7581
}
82+
83+
if (values.contains("exception")) {
84+
oldException = false;
85+
stableException = true;
86+
}
87+
if (values.contains("exception/dup")) {
88+
oldException = true;
89+
stableException = true;
90+
}
7691
}
7792

7893
emitOldDatabaseSemconv = oldDatabase;
@@ -83,6 +98,9 @@ public final class SemconvStability {
8398

8499
emitOldServicePeerSemconv = oldServicePeer;
85100
emitStableServicePeerSemconv = stableServicePeer;
101+
102+
emitOldExceptionSemconv = oldException;
103+
emitStableExceptionSemconv = stableException;
86104
}
87105

88106
public static boolean emitOldDatabaseSemconv() {
@@ -101,6 +119,14 @@ public static boolean emitStableServicePeerSemconv() {
101119
return emitStableServicePeerSemconv;
102120
}
103121

122+
public static boolean emitOldExceptionSemconv() {
123+
return emitOldExceptionSemconv;
124+
}
125+
126+
public static boolean emitStableExceptionSemconv() {
127+
return emitStableExceptionSemconv;
128+
}
129+
104130
private static final Map<String, String> dbSystemNameMap = new HashMap<>();
105131

106132
static {

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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
@@ -27,14 +28,18 @@
2728
import io.opentelemetry.context.propagation.TextMapGetter;
2829
import io.opentelemetry.instrumentation.api.internal.Experimental;
2930
import io.opentelemetry.instrumentation.api.internal.SchemaUrlProvider;
31+
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
3032
import io.opentelemetry.instrumentation.api.internal.SpanKey;
3133
import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider;
3234
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
35+
import io.opentelemetry.sdk.logs.data.LogRecordData;
3336
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
3437
import io.opentelemetry.sdk.trace.data.LinkData;
3538
import io.opentelemetry.sdk.trace.data.StatusData;
39+
import io.opentelemetry.semconv.ExceptionAttributes;
3640
import java.util.Collections;
3741
import java.util.HashMap;
42+
import java.util.List;
3843
import java.util.Map;
3944
import java.util.concurrent.atomic.AtomicReference;
4045
import java.util.stream.Collectors;
@@ -228,14 +233,31 @@ void server_error() {
228233
Context context = instrumenter.start(Context.root(), REQUEST);
229234
assertThat(Span.fromContext(context).getSpanContext().isValid()).isTrue();
230235

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

233239
otelTesting
234240
.assertTraces()
235241
.hasTracesSatisfyingExactly(
236242
trace ->
237243
trace.hasSpansSatisfyingExactly(
238-
span -> span.hasName("span").hasStatus(StatusData.error())));
244+
span -> {
245+
span.hasName("span").hasStatus(StatusData.error());
246+
if (SemconvStability.emitOldExceptionSemconv()) {
247+
span.hasException(error);
248+
}
249+
}));
250+
251+
if (SemconvStability.emitStableExceptionSemconv()) {
252+
List<LogRecordData> logs = otelTesting.getLogRecords();
253+
assertThat(logs).hasSize(1);
254+
LogRecordData log = logs.get(0);
255+
assertThat(log.getSeverity()).isEqualTo(Severity.ERROR);
256+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_TYPE))
257+
.isEqualTo("java.lang.IllegalStateException");
258+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_MESSAGE)).isEqualTo("test");
259+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_STACKTRACE)).isNotNull();
260+
}
239261
}
240262

241263
@Test
@@ -337,14 +359,31 @@ void client_error() {
337359
assertThat(spanContext.isValid()).isTrue();
338360
assertThat(request).containsKey("traceparent");
339361

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

342365
otelTesting
343366
.assertTraces()
344367
.hasTracesSatisfyingExactly(
345368
trace ->
346369
trace.hasSpansSatisfyingExactly(
347-
span -> span.hasName("span").hasStatus(StatusData.error())));
370+
span -> {
371+
span.hasName("span").hasStatus(StatusData.error());
372+
if (SemconvStability.emitOldExceptionSemconv()) {
373+
span.hasException(error);
374+
}
375+
}));
376+
377+
if (SemconvStability.emitStableExceptionSemconv()) {
378+
List<LogRecordData> logs = otelTesting.getLogRecords();
379+
assertThat(logs).hasSize(1);
380+
LogRecordData log = logs.get(0);
381+
assertThat(log.getSeverity()).isEqualTo(Severity.ERROR);
382+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_TYPE))
383+
.isEqualTo("java.lang.IllegalStateException");
384+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_MESSAGE)).isEqualTo("test");
385+
assertThat(log.getAttributes().get(ExceptionAttributes.EXCEPTION_STACKTRACE)).isNotNull();
386+
}
348387
}
349388

350389
@Test

0 commit comments

Comments
 (0)