Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0d2f88b
Add opt-in support for emitting exceptions as log signals
trask Feb 22, 2026
ca470b9
Merge branch 'main' into exceptions-over-log-signal
trask Feb 26, 2026
5732f14
Merge remote-tracking branch 'upstream/main' into exceptions-over-log…
trask Mar 3, 2026
d06b0e9
Add default ExceptionEventExtractor
trask Mar 3, 2026
9b2df30
update assertion pattern
trask Mar 9, 2026
79d41ef
Merge branch 'main' into exceptions-over-log-signal
trask Mar 9, 2026
02f15a8
update
trask Mar 9, 2026
7964b79
Merge remote-tracking branch 'upstream/main' into exceptions-over-log…
trask Mar 9, 2026
1df0fb1
update
trask Mar 10, 2026
078fb6b
update fallback event name based on latest semconv PR updates
trask Mar 10, 2026
bc15ede
spotless
trask Mar 10, 2026
fdde948
Merge branch 'main' into exceptions-over-log-signal
trask Apr 30, 2026
debd045
update to latest semconv
trask May 1, 2026
54c294e
simplify
trask May 1, 2026
ce9ef4b
Update HTTP test schema URL expectations
trask May 2, 2026
fa4c66c
Update Netty 3.8 connection schema URL
trask May 2, 2026
cb39859
Fix PR CI failures
trask May 3, 2026
7d2faa2
back
trask May 4, 2026
ad1c897
spotless
trask May 4, 2026
1c3e7b9
fix
trask May 4, 2026
ba08868
Apply review feedback
trask May 8, 2026
d82a2e2
tests
trask May 8, 2026
8d4f7bb
fix
trask May 8, 2026
403d742
spotless
trask May 8, 2026
ed72463
fix
trask May 8, 2026
6081ccf
Address review comments: collectMetadata for all Test tasks; include …
trask May 8, 2026
e69afd1
up
trask May 8, 2026
e931a9f
Potential fix for pull request finding
trask May 8, 2026
ea5c42e
declarative config
trask May 8, 2026
15bcc10
review
trask May 8, 2026
4768165
review
trask May 8, 2026
b6295c4
Fix WebFlux exception signal log assertions
trask May 9, 2026
88d9766
spotless
trask May 9, 2026
2ddd571
Assert no exception span event in logs-only mode
trask May 9, 2026
c22e3fa
fix
trask May 9, 2026
cde1b63
Merge branch 'main' into exceptions-over-log-signal
trask May 9, 2026
2d29ba0
review
trask May 9, 2026
c204a4d
Drop redundant Assertions.assertThat static import in spring-webflux …
trask May 11, 2026
8ea9f27
Merge remote-tracking branch 'upstream/main' into exceptions-over-log…
trask May 11, 2026
0051a0b
rename opt-in to preview
trask May 12, 2026
d0ecb52
Potential fix for pull request finding
trask May 12, 2026
f721eef
Fix Netty connect exception log event name
trask May 15, 2026
4ff6027
Clarify val lambda-parameter rule excludes custom assertion helpers
trask Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion instrumentation-api-incubator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,21 @@ tasks {
inputs.dir(jflexOutputDir)
}

val testExceptionSignalLogs by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
jvmArgs("-Dotel.semconv.exception.signal.opt-in=logs")
inputs.dir(jflexOutputDir)
}

val testExceptionSignalLogsDup by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
jvmArgs("-Dotel.semconv.exception.signal.opt-in=logs/dup")
inputs.dir(jflexOutputDir)
}

check {
dependsOn(testStableSemconv, testBothSemconv)
dependsOn(testStableSemconv, testBothSemconv, testExceptionSignalLogs, testExceptionSignalLogsDup)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig;
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics;
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientServicePeerAttributesExtractor;
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExceptionEventExtractors;
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor;
import io.opentelemetry.instrumentation.api.incubator.semconv.http.internal.HttpClientUrlTemplateUtil;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
Expand Down Expand Up @@ -223,6 +224,7 @@ public Instrumenter<REQUEST, RESPONSE> build() {
.addAttributesExtractors(additionalExtractors)
.addOperationMetrics(HttpClientMetrics.get())
.setSchemaUrl(SchemaUrls.V1_37_0);
Experimental.setExceptionEventExtractor(builder, HttpExceptionEventExtractors.client());
if (emitExperimentalHttpClientTelemetry) {
builder
.addAttributesExtractor(HttpExperimentalAttributesExtractor.create(attributesGetter))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.instrumentation.api.incubator.config.internal.CommonConfig;
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExceptionEventExtractors;
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor;
import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpServerExperimentalMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
Expand All @@ -19,6 +20,7 @@
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;
import io.opentelemetry.instrumentation.api.internal.Experimental;
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractor;
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesExtractorBuilder;
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter;
Expand Down Expand Up @@ -215,6 +217,7 @@ public InstrumenterBuilder<REQUEST, RESPONSE> instrumenterBuilder() {
.addContextCustomizer(httpServerRouteBuilder.build())
.addOperationMetrics(HttpServerMetrics.get())
.setSchemaUrl(SchemaUrls.V1_37_0);
Experimental.setExceptionEventExtractor(builder, HttpExceptionEventExtractors.server());
if (emitExperimentalHttpServerTelemetry) {
builder
.addAttributesExtractor(HttpExperimentalAttributesExtractor.create(attributesGetter))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.instrumenter;

import io.opentelemetry.api.logs.LogRecordBuilder;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.internal.InternalExceptionEventExtractor;

/**
* Extractor that populates the exception event {@link LogRecordBuilder} for a request. This allows
* instrumentations to set the event name, severity, and any additional attributes on the exception
* log event.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
@FunctionalInterface
public interface ExceptionEventExtractor<REQUEST> extends InternalExceptionEventExtractor<REQUEST> {

/**
* Returns an {@link ExceptionEventExtractor} that always sets the given event name and severity.
*/
static <REQUEST> ExceptionEventExtractor<REQUEST> create(String eventName, Severity severity) {
return (logRecordBuilder, context, request) -> {
logRecordBuilder.setEventName(eventName);
logRecordBuilder.setSeverity(severity);
};
}

/**
* Populates the exception event {@link LogRecordBuilder} with the event name, severity, and any
* additional attributes for the given context and request.
*/
@Override
void extract(LogRecordBuilder logRecordBuilder, Context context, REQUEST request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.db;

import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.instrumentation.api.incubator.instrumenter.ExceptionEventExtractor;

/**
* {@link ExceptionEventExtractor} constants for DB client instrumentations.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public final class DbExceptionEventExtractors {

/** Exception event extractor for DB client spans. */
public static <REQUEST> ExceptionEventExtractor<REQUEST> client() {
return ExceptionEventExtractor.create("db.client.operation.exception", Severity.WARN);
}

private DbExceptionEventExtractors() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai;

import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.instrumentation.api.incubator.instrumenter.ExceptionEventExtractor;

/**
* {@link ExceptionEventExtractor} constants for GenAI client instrumentations.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public final class GenAiExceptionEventExtractors {

/** Exception event extractor for GenAI client spans. */
public static <REQUEST> ExceptionEventExtractor<REQUEST> client() {
return ExceptionEventExtractor.create("gen_ai.client.operation.exception", Severity.WARN);
}

private GenAiExceptionEventExtractors() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.http;

import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.instrumentation.api.incubator.instrumenter.ExceptionEventExtractor;

/**
* {@link ExceptionEventExtractor} constants for HTTP client and server instrumentations.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public final class HttpExceptionEventExtractors {

/** Exception event extractor for HTTP client spans. */
public static <REQUEST> ExceptionEventExtractor<REQUEST> client() {
return ExceptionEventExtractor.create("http.client.request.exception", Severity.WARN);
}

/** Exception event extractor for HTTP server spans. */
public static <REQUEST> ExceptionEventExtractor<REQUEST> server() {
return ExceptionEventExtractor.create("http.server.request.exception", Severity.ERROR);
}

private HttpExceptionEventExtractors() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.messaging;

import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.instrumentation.api.incubator.instrumenter.ExceptionEventExtractor;

/**
* {@link ExceptionEventExtractor} constants for messaging instrumentations.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public final class MessagingExceptionEventExtractors {

/** Exception event extractor for messaging client operation spans. */
public static <REQUEST> ExceptionEventExtractor<REQUEST> client() {
return ExceptionEventExtractor.create("messaging.client.operation.exception", Severity.WARN);
}

/** Exception event extractor for messaging process spans. */
public static <REQUEST> ExceptionEventExtractor<REQUEST> process() {
return ExceptionEventExtractor.create("messaging.process.exception", Severity.ERROR);
}

private MessagingExceptionEventExtractors() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.rpc;

import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.instrumentation.api.incubator.instrumenter.ExceptionEventExtractor;

/**
* {@link ExceptionEventExtractor} constants for RPC client and server instrumentations.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public final class RpcExceptionEventExtractors {

/** Exception event extractor for RPC client spans. */
public static <REQUEST> ExceptionEventExtractor<REQUEST> client() {
return ExceptionEventExtractor.create("rpc.client.call.exception", Severity.WARN);
}

/** Exception event extractor for RPC server spans. */
public static <REQUEST> ExceptionEventExtractor<REQUEST> server() {
return ExceptionEventExtractor.create("rpc.server.call.exception", Severity.ERROR);
}

private RpcExceptionEventExtractors() {}
}
16 changes: 16 additions & 0 deletions instrumentation-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,20 @@ tasks {
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
}

val testExceptionSignalLogs by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
jvmArgs("-Dotel.semconv.exception.signal.opt-in=logs")
}

val testExceptionSignalLogsDup by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
jvmArgs("-Dotel.semconv.exception.signal.opt-in=logs/dup")
}

check {
dependsOn(testExceptionSignalLogs, testExceptionSignalLogsDup)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@

package io.opentelemetry.instrumentation.api.instrumenter;

import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsLogs;
import static io.opentelemetry.instrumentation.api.internal.SemconvExceptionSignal.emitExceptionAsSpanEvents;
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 java.util.concurrent.TimeUnit.SECONDS;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder;
import io.opentelemetry.api.logs.LogRecordBuilder;
import io.opentelemetry.api.logs.Logger;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
Expand All @@ -18,7 +26,10 @@
import io.opentelemetry.instrumentation.api.internal.InstrumenterAccess;
import io.opentelemetry.instrumentation.api.internal.InstrumenterContext;
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
import io.opentelemetry.instrumentation.api.internal.InternalExceptionEventExtractor;
import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Instant;
import javax.annotation.Nullable;

Expand Down Expand Up @@ -72,6 +83,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder

private final String instrumentationName;
private final Tracer tracer;
@Nullable private final Logger logger;
private final SpanNameExtractor<? super REQUEST> spanNameExtractor;
private final SpanKindExtractor<? super REQUEST> spanKindExtractor;
private final SpanStatusExtractor<? super REQUEST, ? super RESPONSE> spanStatusExtractor;
Expand All @@ -82,6 +94,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
private final AttributesExtractor<? super REQUEST, ? super RESPONSE>[]
operationListenerAttributesExtractors;
private final ErrorCauseExtractor errorCauseExtractor;
@Nullable private final InternalExceptionEventExtractor<? super REQUEST> exceptionEventExtractor;
private final boolean propagateOperationListenersToOnEnd;
private final boolean enabled;
private final SpanSuppressor spanSuppressor;
Expand All @@ -91,6 +104,10 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
Instrumenter(InstrumenterBuilder<REQUEST, RESPONSE> builder) {
this.instrumentationName = builder.instrumentationName;
this.tracer = builder.buildTracer();
this.logger =
emitExceptionAsLogs() && builder.exceptionEventExtractor != null
? builder.buildLogger()
: null;
this.spanNameExtractor = builder.spanNameExtractor;
this.spanKindExtractor = builder.spanKindExtractor;
this.spanStatusExtractor = builder.spanStatusExtractor;
Expand All @@ -101,6 +118,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
this.operationListenerAttributesExtractors =
builder.operationListenerAttributesExtractors.toArray(new AttributesExtractor[0]);
this.errorCauseExtractor = builder.errorCauseExtractor;
this.exceptionEventExtractor = builder.exceptionEventExtractor;
this.propagateOperationListenersToOnEnd = builder.propagateOperationListenersToOnEnd;
this.enabled = builder.enabled;
this.spanSuppressor = builder.buildSpanSuppressor();
Expand Down Expand Up @@ -260,7 +278,12 @@ private void doEnd(

if (error != null) {
error = errorCauseExtractor.extract(error);
span.recordException(error);
if (emitExceptionAsSpanEvents()) {
span.recordException(error);
}
if (emitExceptionAsLogs() && exceptionEventExtractor != null) {
Comment thread
trask marked this conversation as resolved.
Comment thread
trask marked this conversation as resolved.
emitExceptionLog(context, error, request);
}
Comment thread
trask marked this conversation as resolved.
}

UnsafeAttributes attributes = new UnsafeAttributes();
Expand Down Expand Up @@ -301,6 +324,32 @@ private void doEnd(
}
}

private void emitExceptionLog(Context context, Throwable throwable, REQUEST request) {
if (logger == null || exceptionEventExtractor == null) {
// this condition is to keep nullaway happy
// doEnd already guards on exceptionEventExtractor != null, so this is unreachable
return;
}
LogRecordBuilder logRecordBuilder = logger.logRecordBuilder();
Comment thread
laurit marked this conversation as resolved.
logRecordBuilder.setContext(context);
exceptionEventExtractor.extract(logRecordBuilder, context, request);

if (logRecordBuilder instanceof ExtendedLogRecordBuilder) {
((ExtendedLogRecordBuilder) logRecordBuilder).setException(throwable);
} else {
logRecordBuilder.setAttribute(EXCEPTION_TYPE, throwable.getClass().getName());
String message = throwable.getMessage();
if (message != null) {
logRecordBuilder.setAttribute(EXCEPTION_MESSAGE, message);
}
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
Comment thread
laurit marked this conversation as resolved.
Outdated
logRecordBuilder.setAttribute(EXCEPTION_STACKTRACE, writer.toString());
}

logRecordBuilder.emit();
}

private static long getNanos(@Nullable Instant time) {
if (time == null) {
return System.nanoTime();
Expand Down
Loading
Loading