diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java index e0fc9b56b90..b272dbc08a2 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributeUtil.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Predicate; +import javax.annotation.Nullable; /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at @@ -107,13 +108,19 @@ public static Object applyAttributeLengthLimit(Object value, int lengthLimit) { } public static void addExceptionAttributes( - Throwable exception, BiConsumer, String> attributeConsumer) { + BiConsumer, String> attributeConsumer, Throwable exception) { + addExceptionAttributes(attributeConsumer, exception, exception.getMessage()); + } + + public static void addExceptionAttributes( + BiConsumer, String> attributeConsumer, + Throwable exception, + @Nullable String exceptionMessage) { String exceptionType = exception.getClass().getCanonicalName(); if (exceptionType != null) { attributeConsumer.accept(EXCEPTION_TYPE, exceptionType); } - String exceptionMessage = exception.getMessage(); if (exceptionMessage != null) { attributeConsumer.accept(EXCEPTION_MESSAGE, exceptionMessage); } diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilder.java index a73f4aa681b..d24cdc62f1a 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLogRecordBuilder.java @@ -53,7 +53,7 @@ SdkLogRecordBuilder setException(Throwable throwable) { return this; } - AttributeUtil.addExceptionAttributes(throwable, this::setAttribute); + AttributeUtil.addExceptionAttributes(this::setAttribute, throwable); return this; } diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java index 30ee4bbdebf..d8b580280d1 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java @@ -20,11 +20,11 @@ import io.opentelemetry.sdk.internal.InstrumentationScopeUtil; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.data.EventData; -import io.opentelemetry.sdk.trace.data.ExceptionEventData; import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; +import io.opentelemetry.sdk.trace.internal.data.LazyExceptionEventData; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -465,17 +465,9 @@ public ReadWriteSpan recordException(Throwable exception, Attributes additionalA additionalAttributes = Attributes.empty(); } - AttributesMap attributes = - AttributesMap.create( - spanLimits.getMaxNumberOfAttributes(), spanLimits.getMaxAttributeValueLength()); - - AttributeUtil.addExceptionAttributes(exception, attributes::put); - - additionalAttributes.forEach(attributes::put); - addTimedEvent( - ExceptionEventData.create( - clock.now(), exception, attributes, attributes.getTotalAddedValues())); + LazyExceptionEventData.create(clock.now(), exception, additionalAttributes, spanLimits)); + return this; } diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/data/LazyExceptionEventData.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/data/LazyExceptionEventData.java new file mode 100644 index 00000000000..aba715d0daf --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/data/LazyExceptionEventData.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal.data; + +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.internal.AttributeUtil; +import io.opentelemetry.sdk.internal.AttributesMap; +import io.opentelemetry.sdk.trace.SpanLimits; +import io.opentelemetry.sdk.trace.data.ExceptionEventData; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * An {@link ExceptionEventData} implementation with {@link #getAttributes()} lazily evaluated, + * allowing the (relatively) expensive exception attribute rendering to take place off the hot path. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@AutoValue +@Immutable +public abstract class LazyExceptionEventData implements ExceptionEventData { + + private static final AttributeKey EXCEPTION_TYPE = + AttributeKey.stringKey("exception.type"); + private static final AttributeKey EXCEPTION_MESSAGE = + AttributeKey.stringKey("exception.message"); + private static final AttributeKey EXCEPTION_STACKTRACE = + AttributeKey.stringKey("exception.stacktrace"); + private static final String EXCEPTION_EVENT_NAME = "exception"; + + @Override + public final String getName() { + return EXCEPTION_EVENT_NAME; + } + + // Autowire generates AutoValue_LazyExceptionEventData and $AutoValue_LazyExceptionEventData to + // account to memoize getAttributes. Unfortunately, $AutoValue_LazyExceptionEventData's + // exceptionMessage constructor param is properly annotated with @Nullable, but + // AutoValue_LazyExceptionEventData's is not. So we suppress the NullAway false positive. + @SuppressWarnings("NullAway") + public static ExceptionEventData create( + long epochNanos, + Throwable exception, + Attributes additionalAttributes, + SpanLimits spanLimits) { + // Compute exception message at initialization time to be conservative about possibility of + // Exception#geMessage() not being thread safe + return new AutoValue_LazyExceptionEventData( + epochNanos, exception, spanLimits, exception.getMessage(), additionalAttributes); + } + + abstract SpanLimits getSpanLimits(); + + @Nullable + abstract String getExceptionMessage(); + + public abstract Attributes getAdditionalAttributes(); + + @Override + @Memoized + public Attributes getAttributes() { + Throwable exception = getException(); + SpanLimits spanLimits = getSpanLimits(); + Attributes additionalAttributes = getAdditionalAttributes(); + + AttributesMap attributes = + AttributesMap.create( + spanLimits.getMaxNumberOfAttributes(), spanLimits.getMaxAttributeValueLength()); + + AttributeUtil.addExceptionAttributes(attributes::put, exception, getExceptionMessage()); + + additionalAttributes.forEach(attributes::put); + + return attributes.immutableCopy(); + } + + @Override + public int getTotalAttributeCount() { + // getAttributes() lazily adds 3 attributes to getAdditionalAttributes(): + // - exception.type + // - exception.message + // - exception.stacktrace + return getAdditionalAttributes().size() + 3; + } + + LazyExceptionEventData() {} +}