Skip to content
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 @@ -226,6 +227,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 Down Expand Up @@ -231,6 +232,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,14 @@

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 java.util.concurrent.TimeUnit.SECONDS;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.logs.LogRecordBuilder;
import io.opentelemetry.api.logs.Logger;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
Expand All @@ -18,6 +23,7 @@
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.time.Instant;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -72,6 +78,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 +89,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 @@ -104,6 +112,17 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
this.propagateOperationListenersToOnEnd = builder.propagateOperationListenersToOnEnd;
this.enabled = builder.enabled;
this.spanSuppressor = builder.buildSpanSuppressor();

if (emitExceptionAsLogs()) {
this.logger = builder.buildLogger();
this.exceptionEventExtractor =
builder.exceptionEventExtractor != null
? builder.exceptionEventExtractor
: defaultExceptionEventExtractor();
} else {
this.logger = null;
this.exceptionEventExtractor = null;
}
}

/**
Expand Down Expand Up @@ -260,7 +279,12 @@ private void doEnd(

if (error != null) {
error = errorCauseExtractor.extract(error);
span.recordException(error);
if (emitExceptionAsSpanEvents()) {
span.recordException(error);
}
if (emitExceptionAsLogs() && exceptionEventExtractor != null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the extractor is not set the exception would be lost. Is there a plan to introduce a default extractor or do we need to change the instrumenter api to force user to always provide an extractor?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a default

name is {instrumentationName}.exception
severity is WARN

cc @lmolkova

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed the default to

  • name: "exception"
  • severity: WARN

based on open-telemetry/semantic-conventions#3311

emitExceptionLog(context, error, request);
}
}

UnsafeAttributes attributes = new UnsafeAttributes();
Expand Down Expand Up @@ -301,6 +325,27 @@ 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);
logRecordBuilder.setException(throwable);
logRecordBuilder.emit();
}

private static <REQUEST>
InternalExceptionEventExtractor<REQUEST> defaultExceptionEventExtractor() {
return (logRecordBuilder, context, request) -> {
logRecordBuilder.setEventName("exception");
logRecordBuilder.setSeverity(Severity.WARN);
};
}

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