55
66package io .opentelemetry .instrumentation .api .instrumenter ;
77
8+ import static io .opentelemetry .instrumentation .api .internal .SemconvExceptionSignal .emitExceptionAsLogs ;
9+ import static io .opentelemetry .instrumentation .api .internal .SemconvExceptionSignal .emitExceptionAsSpanEvents ;
810import static java .util .concurrent .TimeUnit .SECONDS ;
911
1012import io .opentelemetry .api .OpenTelemetry ;
13+ import io .opentelemetry .api .logs .LogRecordBuilder ;
14+ import io .opentelemetry .api .logs .Logger ;
15+ import io .opentelemetry .api .logs .Severity ;
1116import io .opentelemetry .api .trace .Span ;
1217import io .opentelemetry .api .trace .SpanBuilder ;
1318import io .opentelemetry .api .trace .SpanKind ;
1823import io .opentelemetry .instrumentation .api .internal .InstrumenterAccess ;
1924import io .opentelemetry .instrumentation .api .internal .InstrumenterContext ;
2025import io .opentelemetry .instrumentation .api .internal .InstrumenterUtil ;
26+ import io .opentelemetry .instrumentation .api .internal .InternalExceptionEventExtractor ;
2127import io .opentelemetry .instrumentation .api .internal .SupportabilityMetrics ;
2228import java .time .Instant ;
2329import javax .annotation .Nullable ;
@@ -72,6 +78,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
7278
7379 private final String instrumentationName ;
7480 private final Tracer tracer ;
81+ @ Nullable private final Logger logger ;
7582 private final SpanNameExtractor <? super REQUEST > spanNameExtractor ;
7683 private final SpanKindExtractor <? super REQUEST > spanKindExtractor ;
7784 private final SpanStatusExtractor <? super REQUEST , ? super RESPONSE > spanStatusExtractor ;
@@ -82,6 +89,7 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
8289 private final AttributesExtractor <? super REQUEST , ? super RESPONSE >[]
8390 operationListenerAttributesExtractors ;
8491 private final ErrorCauseExtractor errorCauseExtractor ;
92+ @ Nullable private final InternalExceptionEventExtractor <? super REQUEST > exceptionEventExtractor ;
8593 private final boolean propagateOperationListenersToOnEnd ;
8694 private final boolean enabled ;
8795 private final SpanSuppressor spanSuppressor ;
@@ -104,6 +112,17 @@ public static <REQUEST, RESPONSE> InstrumenterBuilder<REQUEST, RESPONSE> builder
104112 this .propagateOperationListenersToOnEnd = builder .propagateOperationListenersToOnEnd ;
105113 this .enabled = builder .enabled ;
106114 this .spanSuppressor = builder .buildSpanSuppressor ();
115+
116+ if (emitExceptionAsLogs ()) {
117+ this .logger = builder .buildLogger ();
118+ this .exceptionEventExtractor =
119+ builder .exceptionEventExtractor != null
120+ ? builder .exceptionEventExtractor
121+ : defaultExceptionEventExtractor (this .spanKindExtractor );
122+ } else {
123+ this .logger = null ;
124+ this .exceptionEventExtractor = null ;
125+ }
107126 }
108127
109128 /**
@@ -260,7 +279,13 @@ private void doEnd(
260279
261280 if (error != null ) {
262281 error = errorCauseExtractor .extract (error );
263- span .recordException (error );
282+ if (emitExceptionAsSpanEvents ()) {
283+ span .recordException (error );
284+ }
285+ // Exception logs are intentionally emitted even when the span is not recording.
286+ if (emitExceptionAsLogs () && exceptionEventExtractor != null ) {
287+ emitExceptionLog (context , error , request , endTime );
288+ }
264289 }
265290
266291 UnsafeAttributes attributes = new UnsafeAttributes ();
@@ -301,6 +326,40 @@ private void doEnd(
301326 }
302327 }
303328
329+ private void emitExceptionLog (
330+ Context context , Throwable throwable , REQUEST request , @ Nullable Instant endTime ) {
331+ if (logger == null || exceptionEventExtractor == null ) {
332+ // this condition is to keep nullaway happy
333+ // doEnd already guards on exceptionEventExtractor != null, so this is unreachable
334+ return ;
335+ }
336+ LogRecordBuilder logRecordBuilder = logger .logRecordBuilder ();
337+ logRecordBuilder .setContext (context );
338+ if (endTime != null ) {
339+ logRecordBuilder .setTimestamp (endTime );
340+ }
341+ exceptionEventExtractor .extract (logRecordBuilder , context , request );
342+ logRecordBuilder .setException (throwable );
343+ logRecordBuilder .emit ();
344+ }
345+
346+ // Per semconv
347+ // (https://opentelemetry.io/docs/specs/semconv/general/recording-errors/#errors-in-logs),
348+ // SERVER and CONSUMER spans should record exceptions with ERROR severity, while CLIENT and
349+ // PRODUCER spans should use WARN.
350+ private static <REQUEST > InternalExceptionEventExtractor <REQUEST > defaultExceptionEventExtractor (
351+ SpanKindExtractor <? super REQUEST > spanKindExtractor ) {
352+ return (logRecordBuilder , context , request ) -> {
353+ logRecordBuilder .setEventName ("exception" );
354+ SpanKind spanKind = spanKindExtractor .extract (request );
355+ Severity severity =
356+ (spanKind == SpanKind .SERVER || spanKind == SpanKind .CONSUMER )
357+ ? Severity .ERROR
358+ : Severity .WARN ;
359+ logRecordBuilder .setSeverity (severity );
360+ };
361+ }
362+
304363 private static long getNanos (@ Nullable Instant time ) {
305364 if (time == null ) {
306365 return System .nanoTime ();
0 commit comments