Skip to content

Commit dcf986a

Browse files
committed
Add opt-in support for emitting exceptions as logs
1 parent 8e5e906 commit dcf986a

13 files changed

Lines changed: 311 additions & 46 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
@@ -238,6 +238,7 @@ public Instrumenter<REQUEST, RESPONSE> build() {
238238
.setSpanStatusExtractor(
239239
spanStatusExtractorCustomizer.apply(
240240
HttpSpanStatusExtractor.create(attributesGetter)))
241+
.setExceptionEventName("http.client.exception")
241242
.addAttributesExtractor(httpAttributesExtractorBuilder.build())
242243
.addAttributesExtractors(additionalExtractors)
243244
.addOperationMetrics(HttpClientMetrics.get())

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ public InstrumenterBuilder<REQUEST, RESPONSE> instrumenterBuilder() {
209209
.setSpanStatusExtractor(
210210
spanStatusExtractorCustomizer.apply(
211211
HttpSpanStatusExtractor.create(attributesGetter)))
212+
.setExceptionEventName("http.server.exception")
212213
.addAttributesExtractor(httpAttributesExtractorBuilder.build())
213214
.addAttributesExtractors(additionalExtractors)
214215
.addContextCustomizer(httpServerRouteBuilder.build())

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: 24 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

@@ -195,6 +197,16 @@ public InstrumenterBuilder<REQUEST, RESPONSE> setErrorCauseExtractor(
195197
return this;
196198
}
197199

200+
/**
201+
* Sets the event name to use when emitting exceptions as log records. Only used when stable
202+
* exception semconv is enabled.
203+
*/
204+
@CanIgnoreReturnValue
205+
public InstrumenterBuilder<REQUEST, RESPONSE> setExceptionEventName(String exceptionEventName) {
206+
this.exceptionEventName = requireNonNull(exceptionEventName, "exceptionEventName");
207+
return this;
208+
}
209+
198210
/**
199211
* Allows enabling/disabling the {@link Instrumenter} based on the {@code enabled} value passed as
200212
* parameter. All instrumenters are enabled by default.
@@ -313,6 +325,18 @@ Tracer buildTracer() {
313325
return tracerBuilder.build();
314326
}
315327

328+
io.opentelemetry.api.logs.Logger buildLogger() {
329+
LoggerBuilder loggerBuilder = openTelemetry.getLogsBridge().loggerBuilder(instrumentationName);
330+
if (instrumentationVersion != null) {
331+
loggerBuilder.setInstrumentationVersion(instrumentationVersion);
332+
}
333+
String schemaUrl = getSchemaUrl();
334+
if (schemaUrl != null) {
335+
loggerBuilder.setSchemaUrl(schemaUrl);
336+
}
337+
return loggerBuilder.build();
338+
}
339+
316340
List<OperationListener> buildOperationListeners() {
317341
// just copy the listeners list if there are no metrics registered
318342
if (operationMetrics.isEmpty()) {

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
@@ -24,13 +24,19 @@ public final class SemconvStability {
2424
private static final boolean emitOldCodeSemconv;
2525
private static final boolean emitStableCodeSemconv;
2626

27+
private static final boolean emitOldExceptionSemconv;
28+
private static final boolean emitStableExceptionSemconv;
29+
2730
static {
2831
boolean oldDatabase = true;
2932
boolean stableDatabase = false;
3033

3134
boolean oldCode = true;
3235
boolean stableCode = false;
3336

37+
boolean oldException = true;
38+
boolean stableException = false;
39+
3440
String value = System.getProperty("otel.semconv-stability.opt-in");
3541
if (value == null) {
3642
value = System.getenv("OTEL_SEMCONV_STABILITY_OPT_IN");
@@ -58,13 +64,25 @@ public final class SemconvStability {
5864
oldCode = true;
5965
stableCode = true;
6066
}
67+
68+
if (values.contains("exception")) {
69+
oldException = false;
70+
stableException = true;
71+
}
72+
if (values.contains("exception/dup")) {
73+
oldException = true;
74+
stableException = true;
75+
}
6176
}
6277

6378
emitOldDatabaseSemconv = oldDatabase;
6479
emitStableDatabaseSemconv = stableDatabase;
6580

6681
emitOldCodeSemconv = oldCode;
6782
emitStableCodeSemconv = stableCode;
83+
84+
emitOldExceptionSemconv = oldException;
85+
emitStableExceptionSemconv = stableException;
6886
}
6987

7088
public static boolean emitOldDatabaseSemconv() {
@@ -75,6 +93,14 @@ public static boolean emitStableDatabaseSemconv() {
7593
return emitStableDatabaseSemconv;
7694
}
7795

96+
public static boolean emitOldExceptionSemconv() {
97+
return emitOldExceptionSemconv;
98+
}
99+
100+
public static boolean emitStableExceptionSemconv() {
101+
return emitStableExceptionSemconv;
102+
}
103+
78104
private static final Map<String, String> dbSystemNameMap = new HashMap<>();
79105

80106
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

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)