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..de7fac88eba 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 @@ -8,12 +8,9 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; import java.util.function.Predicate; /** @@ -22,13 +19,6 @@ */ public final class AttributeUtil { - 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 AttributeUtil() {} /** @@ -105,26 +95,4 @@ public static Object applyAttributeLengthLimit(Object value, int lengthLimit) { } return value; } - - public static void addExceptionAttributes( - Throwable exception, BiConsumer, String> attributeConsumer) { - 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); - } - - StringWriter stringWriter = new StringWriter(); - try (PrintWriter printWriter = new PrintWriter(stringWriter)) { - exception.printStackTrace(printWriter); - } - String stackTrace = stringWriter.toString(); - if (stackTrace != null) { - attributeConsumer.accept(EXCEPTION_STACKTRACE, stackTrace); - } - } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributesMap.java index 7a09093a315..2e481723c9c 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributesMap.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/AttributesMap.java @@ -21,8 +21,8 @@ *

WARNING: In order to reduce memory allocation, this class extends {@link HashMap} when it * would be more appropriate to delegate. The problem with extending is that we don't enforce that * all {@link HashMap} methods for reading / writing data conform to the configured attribute - * limits. Therefore, it's easy to accidentally call something like {@link Map#putAll(Map)} or - * {@link Map#put(Object, Object)} and bypass the restrictions (see #7135). Callers MUST * take care to only call methods from {@link AttributesMap}, and not {@link HashMap}. * @@ -58,7 +58,10 @@ public static AttributesMap create(long capacity, int lengthLimit) { */ @Override @Nullable - public Object put(AttributeKey key, Object value) { + public Object put(AttributeKey key, @Nullable Object value) { + if (value == null) { + return null; + } totalAddedValues++; if (size() >= capacity && !containsKey(key)) { return null; @@ -66,6 +69,11 @@ public Object put(AttributeKey key, Object value) { return super.put(key, AttributeUtil.applyAttributeLengthLimit(value, lengthLimit)); } + /** Generic overload of {@link #put(AttributeKey, Object)}. */ + public void putIfCapacity(AttributeKey key, @Nullable T value) { + put(key, value); + } + /** Get the total number of attributes added, including those dropped for capacity limits. */ public int getTotalAddedValues() { return totalAddedValues; diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/DefaultExceptionAttributeResolver.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/DefaultExceptionAttributeResolver.java new file mode 100644 index 00000000000..44061c1644f --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/DefaultExceptionAttributeResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public final class DefaultExceptionAttributeResolver implements ExceptionAttributeResolver { + + private static final DefaultExceptionAttributeResolver INSTANCE = + new DefaultExceptionAttributeResolver(); + + private DefaultExceptionAttributeResolver() {} + + public static ExceptionAttributeResolver getInstance() { + return INSTANCE; + } + + @Override + public void setExceptionAttributes( + AttributeSetter attributeSetter, Throwable throwable, int maxAttributeLength) { + String exceptionType = throwable.getClass().getCanonicalName(); + if (exceptionType != null) { + attributeSetter.setAttribute(ExceptionAttributeResolver.EXCEPTION_TYPE, exceptionType); + } + + String exceptionMessage = throwable.getMessage(); + if (exceptionMessage != null) { + attributeSetter.setAttribute(ExceptionAttributeResolver.EXCEPTION_MESSAGE, exceptionMessage); + } + + StringWriter stringWriter = new StringWriter(); + try (PrintWriter printWriter = new PrintWriter(stringWriter)) { + throwable.printStackTrace(printWriter); + } + String exceptionStacktrace = stringWriter.toString(); + if (exceptionStacktrace != null) { + attributeSetter.setAttribute( + ExceptionAttributeResolver.EXCEPTION_STACKTRACE, exceptionStacktrace); + } + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExceptionAttributeResolver.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExceptionAttributeResolver.java new file mode 100644 index 00000000000..4b9d7a8dc9d --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExceptionAttributeResolver.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.internal; + +import io.opentelemetry.api.common.AttributeKey; +import javax.annotation.Nullable; + +/** + * Implementations resolve {@code exception.*} attributes attached to span events, logs, etc. + * + *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public interface ExceptionAttributeResolver { + + AttributeKey EXCEPTION_TYPE = AttributeKey.stringKey("exception.type"); + AttributeKey EXCEPTION_MESSAGE = AttributeKey.stringKey("exception.message"); + AttributeKey EXCEPTION_STACKTRACE = AttributeKey.stringKey("exception.stacktrace"); + + void setExceptionAttributes( + AttributeSetter attributeSetter, Throwable throwable, int maxAttributeLength); + + /** + * This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ + // TODO(jack-berg): Consider promoting to opentelemetry and extending with Span, LogRecordBuilder, + // AttributeBuilder, AttributesMap etc. + interface AttributeSetter { + void setAttribute(AttributeKey key, @Nullable T value); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java index 440e472666e..52959b1b617 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExtendedAttributesMap.java @@ -49,14 +49,23 @@ public static ExtendedAttributesMap create(long capacity, int lengthLimit) { } /** Add the attribute key value pair, applying capacity and length limits. */ - public void put(ExtendedAttributeKey key, T value) { + @Override + @Nullable + public Object put(ExtendedAttributeKey key, @Nullable Object value) { + if (value == null) { + return null; + } totalAddedValues++; - // TODO(jack-berg): apply capcity to nested entries + // TODO(jack-berg): apply capacity to nested entries if (size() >= capacity && !containsKey(key)) { - return; + return null; } // TODO(jack-berg): apply limits to nested entries - super.put(key, AttributeUtil.applyAttributeLengthLimit(value, lengthLimit)); + return super.put(key, AttributeUtil.applyAttributeLengthLimit(value, lengthLimit)); + } + + public void putIfCapacity(ExtendedAttributeKey key, @Nullable T value) { + put(key, value); } /** diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordBuilder.java index 9fabc98b9e8..41596afa3da 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/ExtendedSdkLogRecordBuilder.java @@ -13,7 +13,6 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.internal.AttributeUtil; import io.opentelemetry.sdk.internal.ExtendedAttributesMap; import java.time.Instant; import java.util.concurrent.TimeUnit; @@ -42,7 +41,12 @@ public ExtendedSdkLogRecordBuilder setException(Throwable throwable) { return this; } - AttributeUtil.addExceptionAttributes(throwable, this::setAttribute); + loggerSharedState + .getExceptionAttributeResolver() + .setExceptionAttributes( + this::setAttribute, + throwable, + loggerSharedState.getLogLimits().getMaxAttributeValueLength()); return this; } diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java index 768871e1e57..5b40f897a32 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java @@ -7,6 +7,7 @@ import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.resources.Resource; import java.util.function.Supplier; import javax.annotation.Nullable; @@ -21,17 +22,20 @@ final class LoggerSharedState { private final Supplier logLimitsSupplier; private final LogRecordProcessor logRecordProcessor; private final Clock clock; + private final ExceptionAttributeResolver exceptionAttributeResolver; @Nullable private volatile CompletableResultCode shutdownResult = null; LoggerSharedState( Resource resource, Supplier logLimitsSupplier, LogRecordProcessor logRecordProcessor, - Clock clock) { + Clock clock, + ExceptionAttributeResolver exceptionAttributeResolver) { this.resource = resource; this.logLimitsSupplier = logLimitsSupplier; this.logRecordProcessor = logRecordProcessor; this.clock = clock; + this.exceptionAttributeResolver = exceptionAttributeResolver; } Resource getResource() { @@ -50,6 +54,10 @@ Clock getClock() { return clock; } + ExceptionAttributeResolver getExceptionAttributeResolver() { + return exceptionAttributeResolver; + } + boolean hasBeenShutdown() { return shutdownResult != null; } diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java index 12f7baa201e..54825caf21e 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java @@ -13,6 +13,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.ComponentRegistry; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.logs.internal.LoggerConfig; import io.opentelemetry.sdk.resources.Resource; @@ -56,10 +57,12 @@ public static SdkLoggerProviderBuilder builder() { Supplier logLimitsSupplier, List processors, Clock clock, - ScopeConfigurator loggerConfigurator) { + ScopeConfigurator loggerConfigurator, + ExceptionAttributeResolver exceptionAttributeResolver) { LogRecordProcessor logRecordProcessor = LogRecordProcessor.composite(processors); this.sharedState = - new LoggerSharedState(resource, logLimitsSupplier, logRecordProcessor, clock); + new LoggerSharedState( + resource, logLimitsSupplier, logRecordProcessor, clock, exceptionAttributeResolver); this.loggerComponentRegistry = new ComponentRegistry<>( instrumentationScopeInfo -> diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java index 14d6d664f9c..25fdeaa4454 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java @@ -12,6 +12,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.DefaultExceptionAttributeResolver; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; import io.opentelemetry.sdk.logs.data.LogRecordData; @@ -37,6 +39,8 @@ public final class SdkLoggerProviderBuilder { private Clock clock = Clock.getDefault(); private ScopeConfiguratorBuilder loggerConfiguratorBuilder = LoggerConfig.configuratorBuilder(); + private ExceptionAttributeResolver exceptionAttributeResolver = + DefaultExceptionAttributeResolver.getInstance(); SdkLoggerProviderBuilder() {} @@ -168,6 +172,21 @@ SdkLoggerProviderBuilder addLoggerConfiguratorCondition( return this; } + /** + * Set the exception attribute resolver, which resolves {@code exception.*} attributes an + * exception is set on a log. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkLoggerProviderUtil#setExceptionAttributeResolver(SdkLoggerProviderBuilder, + * ExceptionAttributeResolver)}. + */ + SdkLoggerProviderBuilder setExceptionAttributeResolver( + ExceptionAttributeResolver exceptionAttributeResolver) { + requireNonNull(exceptionAttributeResolver, "exceptionAttributeResolver"); + this.exceptionAttributeResolver = exceptionAttributeResolver; + return this; + } + /** * Create a {@link SdkLoggerProvider} instance. * @@ -175,6 +194,11 @@ SdkLoggerProviderBuilder addLoggerConfiguratorCondition( */ public SdkLoggerProvider build() { return new SdkLoggerProvider( - resource, logLimitsSupplier, logRecordProcessors, clock, loggerConfiguratorBuilder.build()); + resource, + logLimitsSupplier, + logRecordProcessors, + clock, + loggerConfiguratorBuilder.build(), + exceptionAttributeResolver); } } diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java index fa659a747ec..417601bbad4 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java @@ -6,6 +6,7 @@ package io.opentelemetry.sdk.logs.internal; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; @@ -73,4 +74,20 @@ public static SdkLoggerProviderBuilder addLoggerConfiguratorCondition( } return sdkLoggerProviderBuilder; } + + /** Reflectively set exception attribute resolver to the {@link SdkLoggerProviderBuilder}. */ + public static void setExceptionAttributeResolver( + SdkLoggerProviderBuilder sdkLoggerProviderBuilder, + ExceptionAttributeResolver exceptionAttributeResolver) { + try { + Method method = + SdkLoggerProviderBuilder.class.getDeclaredMethod( + "setExceptionAttributeResolver", ExceptionAttributeResolver.class); + method.setAccessible(true); + method.invoke(sdkLoggerProviderBuilder, exceptionAttributeResolver); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setExceptionAttributeResolver on SdkLoggerProviderBuilder", e); + } + } } diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java index 29a3a846a56..31b46c65206 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java @@ -12,6 +12,7 @@ import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.DefaultExceptionAttributeResolver; import io.opentelemetry.sdk.resources.Resource; import org.junit.jupiter.api.Test; @@ -24,7 +25,11 @@ void shutdown() { when(logRecordProcessor.shutdown()).thenReturn(code); LoggerSharedState state = new LoggerSharedState( - Resource.empty(), LogLimits::getDefault, logRecordProcessor, Clock.getDefault()); + Resource.empty(), + LogLimits::getDefault, + logRecordProcessor, + Clock.getDefault(), + DefaultExceptionAttributeResolver.getInstance()); state.shutdown(); state.shutdown(); verify(logRecordProcessor, times(1)).shutdown(); diff --git a/sdk/logs/src/testIncubating/java/io/opentelemetry/sdk/logs/ExtendedLoggerBuilderTest.java b/sdk/logs/src/testIncubating/java/io/opentelemetry/sdk/logs/ExtendedLoggerBuilderTest.java index 3b166f8a163..17e1eb0ddff 100644 --- a/sdk/logs/src/testIncubating/java/io/opentelemetry/sdk/logs/ExtendedLoggerBuilderTest.java +++ b/sdk/logs/src/testIncubating/java/io/opentelemetry/sdk/logs/ExtendedLoggerBuilderTest.java @@ -14,7 +14,9 @@ import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder; import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; import org.junit.jupiter.api.Test; @@ -25,7 +27,7 @@ class ExtendedLoggerBuilderTest { SdkLoggerProvider.builder().addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)); @Test - void setException() { + void setException_DefaultResolver() { Logger logger = loggerProviderBuilder.build().get("logger"); ((ExtendedLogRecordBuilder) logger.logRecordBuilder()) @@ -45,4 +47,33 @@ void setException() { stacktrace.startsWith( "java.lang.Exception: error" + System.lineSeparator())))); } + + @Test + void setException_CustomResolver() { + SdkLoggerProviderUtil.setExceptionAttributeResolver( + loggerProviderBuilder, + new ExceptionAttributeResolver() { + @Override + public void setExceptionAttributes( + AttributeSetter attributeSetter, Throwable throwable, int maxAttributeLength) { + attributeSetter.setAttribute(ExceptionAttributeResolver.EXCEPTION_TYPE, "type"); + attributeSetter.setAttribute( + ExceptionAttributeResolver.EXCEPTION_STACKTRACE, "stacktrace"); + } + }); + + Logger logger = loggerProviderBuilder.build().get("logger"); + + ((ExtendedLogRecordBuilder) logger.logRecordBuilder()) + .setException(new Exception("error")) + .emit(); + + assertThat(exporter.getFinishedLogRecordItems()) + .satisfiesExactly( + logRecord -> + assertThat(logRecord) + .hasAttributesSatisfyingExactly( + equalTo(EXCEPTION_TYPE, "type"), + equalTo(EXCEPTION_STACKTRACE, "stacktrace"))); + } } 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 352ccf3135f..37deab7ffc8 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 @@ -17,6 +17,7 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.AttributeUtil; import io.opentelemetry.sdk.internal.AttributesMap; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.InstrumentationScopeUtil; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.data.EventData; @@ -48,6 +49,8 @@ final class SdkSpan implements ReadWriteSpan { private final SpanContext parentSpanContext; // Handler called when the span starts and ends. private final SpanProcessor spanProcessor; + // Resolves exception.* when an recordException is called + private final ExceptionAttributeResolver exceptionAttributeResolver; // The kind of the span. private final SpanKind kind; // The clock used to get the time. @@ -123,6 +126,7 @@ private SdkSpan( SpanContext parentSpanContext, SpanLimits spanLimits, SpanProcessor spanProcessor, + ExceptionAttributeResolver exceptionAttributeResolver, AnchoredClock clock, Resource resource, @Nullable AttributesMap attributes, @@ -137,6 +141,7 @@ private SdkSpan( this.name = name; this.kind = kind; this.spanProcessor = spanProcessor; + this.exceptionAttributeResolver = exceptionAttributeResolver; this.resource = resource; this.hasEnded = EndState.NOT_ENDED; this.clock = clock; @@ -169,6 +174,7 @@ static SdkSpan startSpan( Context parentContext, SpanLimits spanLimits, SpanProcessor spanProcessor, + ExceptionAttributeResolver exceptionAttributeResolver, Clock tracerClock, Resource resource, @Nullable AttributesMap attributes, @@ -207,6 +213,7 @@ static SdkSpan startSpan( parentSpan.getSpanContext(), spanLimits, spanProcessor, + exceptionAttributeResolver, clock, resource, attributes, @@ -465,11 +472,13 @@ public ReadWriteSpan recordException(Throwable exception, Attributes additionalA additionalAttributes = Attributes.empty(); } + int maxAttributeLength = spanLimits.getMaxAttributeValueLength(); AttributesMap attributes = AttributesMap.create( spanLimits.getMaxNumberOfAttributes(), spanLimits.getMaxAttributeValueLength()); - AttributeUtil.addExceptionAttributes(exception, attributes::put); + exceptionAttributeResolver.setExceptionAttributes( + attributes::putIfCapacity, exception, maxAttributeLength); additionalAttributes.forEach(attributes::put); diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java index b12107c7d43..c0f872265ec 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java @@ -226,6 +226,7 @@ public Span startSpan() { parentContext, spanLimits, tracerSharedState.getActiveSpanProcessor(), + tracerSharedState.getExceptionAttributesResolver(), tracerSharedState.getClock(), tracerSharedState.getResource(), recordedAttributes, diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java index 8959c59a115..f39ce565731 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java @@ -12,6 +12,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.ComponentRegistry; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; @@ -52,10 +53,17 @@ public static SdkTracerProviderBuilder builder() { Supplier spanLimitsSupplier, Sampler sampler, List spanProcessors, - ScopeConfigurator tracerConfigurator) { + ScopeConfigurator tracerConfigurator, + ExceptionAttributeResolver exceptionAttributeResolver) { this.sharedState = new TracerSharedState( - clock, idsGenerator, resource, spanLimitsSupplier, sampler, spanProcessors); + clock, + idsGenerator, + resource, + spanLimitsSupplier, + sampler, + spanProcessors, + exceptionAttributeResolver); this.tracerSdkComponentRegistry = new ComponentRegistry<>( instrumentationScopeInfo -> diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java index c9f604f76fc..194aa67bc17 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java @@ -10,6 +10,8 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.DefaultExceptionAttributeResolver; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; import io.opentelemetry.sdk.resources.Resource; @@ -35,6 +37,8 @@ public final class SdkTracerProviderBuilder { private Sampler sampler = DEFAULT_SAMPLER; private ScopeConfiguratorBuilder tracerConfiguratorBuilder = TracerConfig.configuratorBuilder(); + private ExceptionAttributeResolver exceptionAttributeResolver = + DefaultExceptionAttributeResolver.getInstance(); /** * Assign a {@link Clock}. {@link Clock} will be used each time a {@link Span} is started, ended @@ -214,6 +218,21 @@ SdkTracerProviderBuilder addTracerConfiguratorCondition( return this; } + /** + * Set the exception attribute resolver, which resolves {@code exception.*} attributes when {@link + * Span#recordException(Throwable)} + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkTracerProviderUtil#setExceptionAttributeResolver(SdkTracerProviderBuilder, + * ExceptionAttributeResolver)}. + */ + SdkTracerProviderBuilder setExceptionAttributeResolver( + ExceptionAttributeResolver exceptionAttributeResolver) { + requireNonNull(exceptionAttributeResolver, "exceptionAttributeResolver"); + this.exceptionAttributeResolver = exceptionAttributeResolver; + return this; + } + /** * Create a new {@link SdkTracerProvider} instance with the configuration. * @@ -227,7 +246,8 @@ public SdkTracerProvider build() { spanLimitsSupplier, sampler, spanProcessors, - tracerConfiguratorBuilder.build()); + tracerConfiguratorBuilder.build(), + exceptionAttributeResolver); } SdkTracerProviderBuilder() {} diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java index 37bb6675c06..74d43076b97 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java @@ -7,6 +7,7 @@ import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.util.List; @@ -26,6 +27,7 @@ final class TracerSharedState { private final Supplier spanLimitsSupplier; private final Sampler sampler; private final SpanProcessor activeSpanProcessor; + private final ExceptionAttributeResolver exceptionAttributeResolver; @Nullable private volatile CompletableResultCode shutdownResult = null; @@ -35,7 +37,8 @@ final class TracerSharedState { Resource resource, Supplier spanLimitsSupplier, Sampler sampler, - List spanProcessors) { + List spanProcessors, + ExceptionAttributeResolver exceptionAttributeResolver) { this.clock = clock; this.idGenerator = idGenerator; this.idGeneratorSafeToSkipIdValidation = idGenerator instanceof RandomIdGenerator; @@ -43,6 +46,7 @@ final class TracerSharedState { this.spanLimitsSupplier = spanLimitsSupplier; this.sampler = sampler; this.activeSpanProcessor = SpanProcessor.composite(spanProcessors); + this.exceptionAttributeResolver = exceptionAttributeResolver; } Clock getClock() { @@ -89,6 +93,11 @@ boolean hasBeenShutdown() { return shutdownResult != null; } + /** Return the {@link ExceptionAttributeResolver}. */ + ExceptionAttributeResolver getExceptionAttributesResolver() { + return exceptionAttributeResolver; + } + /** * Stops tracing, including shutting down processors and set to {@code true} {@link * #hasBeenShutdown()}. diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java index 753a312fa1f..f9ca8fafc56 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java @@ -6,6 +6,7 @@ package io.opentelemetry.sdk.trace.internal; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; @@ -72,4 +73,20 @@ public static void addTracerConfiguratorCondition( "Error calling addTracerConfiguratorCondition on SdkTracerProviderBuilder", e); } } + + /** Reflectively set exception attribute resolver to the {@link SdkTracerProviderBuilder}. */ + public static void setExceptionAttributeResolver( + SdkTracerProviderBuilder sdkTracerProviderBuilder, + ExceptionAttributeResolver exceptionAttributeResolver) { + try { + Method method = + SdkTracerProviderBuilder.class.getDeclaredMethod( + "setExceptionAttributeResolver", ExceptionAttributeResolver.class); + method.setAccessible(true); + method.invoke(sdkTracerProviderBuilder, exceptionAttributeResolver); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setExceptionAttributeResolver on SdkTracerProviderBuilder", e); + } + } } diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java index 625313dbcb5..fcfec65d47f 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java @@ -14,6 +14,7 @@ import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -37,6 +38,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.AttributesMap; +import io.opentelemetry.sdk.internal.DefaultExceptionAttributeResolver; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.InstrumentationScopeUtil; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.time.TestClock; @@ -942,7 +945,8 @@ void addLink() { .build(), parentSpanId, null, - null); + null, + DefaultExceptionAttributeResolver.getInstance()); try { Span span1 = createTestSpan(SpanKind.INTERNAL); Span span2 = createTestSpan(SpanKind.INTERNAL); @@ -1032,6 +1036,7 @@ void addLink_FaultIn() { Context.root(), SpanLimits.getDefault(), spanProcessor, + DefaultExceptionAttributeResolver.getInstance(), testClock, resource, null, @@ -1292,6 +1297,39 @@ void recordException_SpanLimits() { assertThat(event.getTotalAttributeCount() - event.getAttributes().size()).isPositive(); } + @Test + void recordException_CustomResolver() { + ExceptionAttributeResolver exceptionAttributeResolver = + new ExceptionAttributeResolver() { + @Override + public void setExceptionAttributes( + AttributeSetter attributeSetter, Throwable throwable, int maxAttributeLength) { + attributeSetter.setAttribute(ExceptionAttributeResolver.EXCEPTION_TYPE, "type"); + attributeSetter.setAttribute( + ExceptionAttributeResolver.EXCEPTION_STACKTRACE, "stacktrace"); + } + }; + + SdkSpan span = + createTestSpan( + SpanKind.INTERNAL, + SpanLimits.getDefault(), + parentSpanId, + null, + Collections.singletonList(link), + exceptionAttributeResolver); + + span.recordException(new IllegalStateException("error")); + + List events = span.toSpanData().getEvents(); + assertThat(events.size()).isEqualTo(1); + EventData event = events.get(0); + assertThat(event) + .hasAttributesSatisfyingExactly( + equalTo(ExceptionAttributeResolver.EXCEPTION_TYPE, "type"), + equalTo(ExceptionAttributeResolver.EXCEPTION_STACKTRACE, "stacktrace")); + } + @Test void badArgsIgnored() { SdkSpan span = createTestRootSpan(); @@ -1343,6 +1381,7 @@ void onStartOnEndNotRequired() { Context.root(), spanLimits, spanProcessor, + DefaultExceptionAttributeResolver.getInstance(), testClock, resource, AttributesMap.create( @@ -1424,7 +1463,8 @@ private SdkSpan createTestSpanWithAttributes(Map attribute SpanLimits.getDefault(), null, attributesMap, - Collections.singletonList(link)); + Collections.singletonList(link), + DefaultExceptionAttributeResolver.getInstance()); } private SdkSpan createTestRootSpan() { @@ -1433,17 +1473,28 @@ private SdkSpan createTestRootSpan() { SpanLimits.getDefault(), SpanId.getInvalid(), null, - Collections.singletonList(link)); + Collections.singletonList(link), + DefaultExceptionAttributeResolver.getInstance()); } private SdkSpan createTestSpan(SpanKind kind) { return createTestSpan( - kind, SpanLimits.getDefault(), parentSpanId, null, Collections.singletonList(link)); + kind, + SpanLimits.getDefault(), + parentSpanId, + null, + Collections.singletonList(link), + DefaultExceptionAttributeResolver.getInstance()); } private SdkSpan createTestSpan(SpanLimits config) { return createTestSpan( - SpanKind.INTERNAL, config, parentSpanId, null, Collections.singletonList(link)); + SpanKind.INTERNAL, + config, + parentSpanId, + null, + Collections.singletonList(link), + DefaultExceptionAttributeResolver.getInstance()); } private SdkSpan createTestSpan( @@ -1451,7 +1502,8 @@ private SdkSpan createTestSpan( SpanLimits config, @Nullable String parentSpanId, @Nullable AttributesMap attributes, - @Nullable List links) { + @Nullable List links, + ExceptionAttributeResolver exceptionAttributeResolver) { List linksCopy = links == null ? new ArrayList<>() : new ArrayList<>(links); SdkSpan span = @@ -1468,6 +1520,7 @@ private SdkSpan createTestSpan( Context.root(), config, spanProcessor, + exceptionAttributeResolver, testClock, resource, attributes, @@ -1555,6 +1608,7 @@ void testAsSpanData() { Context.root(), spanLimits, spanProcessor, + DefaultExceptionAttributeResolver.getInstance(), clock, resource, attributesWithCapacity, diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java index 578fd9099ee..345f97d52c0 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkTracerProviderTest.java @@ -8,7 +8,11 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import io.opentelemetry.api.common.Attributes; @@ -18,6 +22,8 @@ import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.internal.DefaultExceptionAttributeResolver; +import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; @@ -282,4 +288,22 @@ void suppliesDefaultTracerForEmptyName() { assertThat(tracer.getInstrumentationScopeInfo().getName()) .isEqualTo(SdkTracerProvider.DEFAULT_TRACER_NAME); } + + @Test + void exceptionAttributeResolver() { + int maxAttributeLength = 5; + SdkTracerProviderBuilder builder = + SdkTracerProvider.builder() + .addSpanProcessor(spanProcessor) + .setSpanLimits( + SpanLimits.builder().setMaxAttributeValueLength(maxAttributeLength).build()); + ExceptionAttributeResolver exceptionAttributeResolver = + spy(DefaultExceptionAttributeResolver.getInstance()); + SdkTracerProviderUtil.setExceptionAttributeResolver(builder, exceptionAttributeResolver); + + Exception exception = new Exception("error"); + builder.build().get("tracer").spanBuilder("span").startSpan().recordException(exception).end(); + + verify(exceptionAttributeResolver).setExceptionAttributes(any(), any(), eq(maxAttributeLength)); + } }