span stacktrace refactor + autoconfig (#1499)

This commit is contained in:
SylvainJuge 2024-10-17 16:40:30 +02:00 committed by GitHub
parent 193f1f53eb
commit ef0caea27e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 317 additions and 811 deletions

View File

@ -6,51 +6,24 @@ This module provides a `SpanProcessor` that captures the [`code.stacktrace`](htt
Capturing the stack trace is an expensive operation and does not provide any value on short-lived spans.
As a consequence it should only be used when the span duration is known, thus on span end.
However, the current SDK API does not allow to modify span attributes on span end, so we have to
introduce other components to make it work as expected.
## Usage and configuration
## Usage
This extension supports autoconfiguration, so it will be automatically enabled by OpenTelemetry
SDK when included in the application runtime dependencies.
This extension does not support autoconfiguration because it needs to wrap the `SimpleSpanExporter`
or `BatchingSpanProcessor` that invokes the `SpanExporter`.
`otel.java.experimental.span-stacktrace.min.duration`
As a consequence you have to use [Manual SDK setup](#manual-sdk-setup)
section below to configure it.
- allows to configure the minimal duration for which spans have a stacktrace captured
- defaults to 5ms
- a value of zero will include all spans
- a negative value will disable the feature
### Manual SDK setup
`otel.java.experimental.span-stacktrace.filter`
Here is an example registration of `StackTraceSpanProcessor` to capture stack trace for all
the spans that have a duration >= 1 ms. The spans that have an `ignorespan` string attribute
will be ignored.
```java
InMemorySpanExporter spansExporter = InMemorySpanExporter.create();
SpanProcessor exportProcessor = SimpleSpanProcessor.create(spansExporter);
Map<String, String> configMap = new HashMap<>();
configMap.put("otel.java.experimental.span-stacktrace.min.duration", "1ms");
ConfigProperties config = DefaultConfigProperties.createFromMap(configMap);
Predicate<ReadableSpan> filterPredicate = readableSpan -> {
if(readableSpan.getAttribute(AttributeKey.stringKey("ignorespan")) != null){
return false;
}
return true;
};
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(new StackTraceSpanProcessor(exportProcessor, config, filterPredicate))
.build();
OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
```
### Configuration
The `otel.java.experimental.span-stacktrace.min.duration` configuration option (defaults to 5ms) allows configuring
the minimal duration for which spans should have a stacktrace captured.
Setting `otel.java.experimental.span-stacktrace.min.duration` to zero will include all spans, and using a negative
value will disable the feature.
- allows to filter spans to be excluded from stacktrace capture
- defaults to include all spans.
- value is the class name of a class implementing `java.util.function.Predicate<ReadableSpan>`
- filter class must be publicly accessible and provide a no-arg constructor
## Component owners

View File

@ -7,6 +7,9 @@ description = "OpenTelemetry Java span stacktrace capture module"
otelJava.moduleName.set("io.opentelemetry.contrib.stacktrace")
dependencies {
annotationProcessor("com.google.auto.service:auto-service")
compileOnly("com.google.auto.service:auto-service-annotations")
api("io.opentelemetry:opentelemetry-sdk")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
@ -16,4 +19,9 @@ dependencies {
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
testAnnotationProcessor("com.google.auto.service:auto-service")
testCompileOnly("com.google.auto.service:auto-service-annotations")
testImplementation("io.opentelemetry:opentelemetry-exporter-logging")
}

View File

@ -0,0 +1,111 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.stacktrace;
import com.google.auto.service.AutoService;
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.trace.ReadableSpan;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@AutoService(AutoConfigurationCustomizerProvider.class)
public class StackTraceAutoConfig implements AutoConfigurationCustomizerProvider {
private static final Logger log = Logger.getLogger(StackTraceAutoConfig.class.getName());
private static final String CONFIG_MIN_DURATION =
"otel.java.experimental.span-stacktrace.min.duration";
private static final Duration CONFIG_MIN_DURATION_DEFAULT = Duration.ofMillis(5);
private static final String CONFIG_FILTER = "otel.java.experimental.span-stacktrace.filter";
@Override
public void customize(AutoConfigurationCustomizer config) {
config.addTracerProviderCustomizer(
(providerBuilder, properties) -> {
long minDuration = getMinDuration(properties);
if (minDuration >= 0) {
Predicate<ReadableSpan> filter = getFilterPredicate(properties);
providerBuilder.addSpanProcessor(new StackTraceSpanProcessor(minDuration, filter));
}
return providerBuilder;
});
}
// package-private for testing
static long getMinDuration(ConfigProperties properties) {
long minDuration =
properties.getDuration(CONFIG_MIN_DURATION, CONFIG_MIN_DURATION_DEFAULT).toNanos();
if (minDuration < 0) {
log.fine("Stack traces capture is disabled");
} else {
log.log(
Level.FINE,
"Stack traces will be added to spans with a minimum duration of {0} nanos",
minDuration);
}
return minDuration;
}
// package private for testing
static Predicate<ReadableSpan> getFilterPredicate(ConfigProperties properties) {
String filterClass = properties.getString(CONFIG_FILTER);
Predicate<ReadableSpan> filter = null;
if (filterClass != null) {
Class<?> filterType = getFilterType(filterClass);
if (filterType != null) {
filter = getFilterInstance(filterType);
}
}
if (filter == null) {
// if value is set, lack of filtering is likely an error and must be reported
Level disabledLogLevel = filterClass != null ? Level.SEVERE : Level.FINE;
log.log(disabledLogLevel, "Span stacktrace filtering disabled");
return span -> true;
} else {
log.fine("Span stacktrace filtering enabled with: " + filterClass);
return filter;
}
}
@Nullable
private static Class<?> getFilterType(String filterClass) {
try {
Class<?> filterType = Class.forName(filterClass);
if (!Predicate.class.isAssignableFrom(filterType)) {
log.severe("Filter must be a subclass of java.util.function.Predicate");
return null;
}
return filterType;
} catch (ClassNotFoundException e) {
log.severe("Unable to load filter class: " + filterClass);
return null;
}
}
@Nullable
@SuppressWarnings("unchecked")
private static Predicate<ReadableSpan> getFilterInstance(Class<?> filterType) {
try {
Constructor<?> constructor = filterType.getConstructor();
return (Predicate<ReadableSpan>) constructor.newInstance();
} catch (NoSuchMethodException
| InstantiationException
| IllegalAccessException
| InvocationTargetException e) {
log.severe("Unable to create filter instance with no-arg constructor: " + filterType);
return null;
}
}
}

View File

@ -6,96 +6,74 @@
package io.opentelemetry.contrib.stacktrace;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.contrib.stacktrace.internal.AbstractSimpleChainingSpanProcessor;
import io.opentelemetry.contrib.stacktrace.internal.MutableSpan;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
public class StackTraceSpanProcessor extends AbstractSimpleChainingSpanProcessor {
private static final String CONFIG_MIN_DURATION =
"otel.java.experimental.span-stacktrace.min.duration";
private static final Duration CONFIG_MIN_DURATION_DEFAULT = Duration.ofMillis(5);
public class StackTraceSpanProcessor implements ExtendedSpanProcessor {
// inlined incubating attribute to prevent direct dependency on incubating semconv
private static final AttributeKey<String> SPAN_STACKTRACE =
AttributeKey.stringKey("code.stacktrace");
private static final Logger logger = Logger.getLogger(StackTraceSpanProcessor.class.getName());
private final long minSpanDurationNanos;
private final Predicate<ReadableSpan> filterPredicate;
/**
* @param next next span processor to invoke
* @param minSpanDurationNanos minimum span duration in ns for stacktrace capture
* @param filterPredicate extra filter function to exclude spans if needed
*/
public StackTraceSpanProcessor(
SpanProcessor next, long minSpanDurationNanos, Predicate<ReadableSpan> filterPredicate) {
super(next);
long minSpanDurationNanos, Predicate<ReadableSpan> filterPredicate) {
if (minSpanDurationNanos < 0) {
throw new IllegalArgumentException("minimal span duration must be positive or zero");
}
this.minSpanDurationNanos = minSpanDurationNanos;
this.filterPredicate = filterPredicate;
if (minSpanDurationNanos < 0) {
logger.log(Level.FINE, "Stack traces capture is disabled");
} else {
logger.log(
Level.FINE,
"Stack traces will be added to spans with a minimum duration of {0} nanos",
minSpanDurationNanos);
}
}
/**
* @param next next span processor to invoke
* @param config configuration
* @param filterPredicate extra filter function to exclude spans if needed
*/
public StackTraceSpanProcessor(
SpanProcessor next, ConfigProperties config, Predicate<ReadableSpan> filterPredicate) {
this(
next,
config.getDuration(CONFIG_MIN_DURATION, CONFIG_MIN_DURATION_DEFAULT).toNanos(),
filterPredicate);
}
@Override
protected boolean requiresStart() {
public boolean isStartRequired() {
return false;
}
@Override
protected boolean requiresEnd() {
public void onStart(Context context, ReadWriteSpan readWriteSpan) {}
@Override
public boolean isOnEndingRequired() {
return true;
}
@Override
protected ReadableSpan doOnEnd(ReadableSpan span) {
if (minSpanDurationNanos < 0 || span.getLatencyNanos() < minSpanDurationNanos) {
return span;
public void onEnding(ReadWriteSpan span) {
if (span.getLatencyNanos() < minSpanDurationNanos) {
return;
}
if (span.getAttribute(SPAN_STACKTRACE) != null) {
// Span already has a stacktrace, do not override
return span;
return;
}
if (!filterPredicate.test(span)) {
return span;
return;
}
MutableSpan mutableSpan = MutableSpan.makeMutable(span);
String stacktrace = generateSpanEndStacktrace();
mutableSpan.setAttribute(SPAN_STACKTRACE, stacktrace);
return mutableSpan;
span.setAttribute(SPAN_STACKTRACE, generateSpanEndStacktrace());
}
@Override
public boolean isEndRequired() {
return false;
}
@Override
public void onEnd(ReadableSpan readableSpan) {}
private static String generateSpanEndStacktrace() {
Throwable exception = new Throwable();
StringWriter stringWriter = new StringWriter();

View File

@ -1,139 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.stacktrace.internal;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import java.util.Arrays;
/**
* A @{@link SpanProcessor} which in addition to all standard operations is capable of modifying and
* optionally filtering spans in the end-callback.
*
* <p>This is done by chaining processors and registering only the first processor with the SDK.
* Mutations can be performed in {@link #doOnEnd(ReadableSpan)} by wrapping the span in a {@link
* MutableSpan}
*/
public abstract class AbstractSimpleChainingSpanProcessor implements SpanProcessor {
protected final SpanProcessor next;
private final boolean nextRequiresStart;
private final boolean nextRequiresEnd;
/**
* @param next the next processor to be invoked after the one being constructed.
*/
public AbstractSimpleChainingSpanProcessor(SpanProcessor next) {
this.next = next;
nextRequiresStart = next.isStartRequired();
nextRequiresEnd = next.isEndRequired();
}
/**
* Equivalent of {@link SpanProcessor#onStart(Context, ReadWriteSpan)}. The onStart callback of
* the next processor must not be invoked from this method, this is already handled by the
* implementation of {@link #onStart(Context, ReadWriteSpan)}.
*/
protected void doOnStart(Context context, ReadWriteSpan readWriteSpan) {}
/**
* Equivalent of {@link SpanProcessor#onEnd(ReadableSpan)}}.
*
* <p>If this method returns null, the provided span will be dropped and not passed to the next
* processor. If anything non-null is returned, the returned instance is passed to the next
* processor.
*
* <p>So in order to mutate the span, simply use {@link MutableSpan#makeMutable(ReadableSpan)} on
* the provided argument and return the {@link MutableSpan} from this method.
*/
@CanIgnoreReturnValue
protected ReadableSpan doOnEnd(ReadableSpan readableSpan) {
return readableSpan;
}
/**
* Indicates if span processor needs to be called on span start
*
* @return true, if this implementation would like {@link #doOnStart(Context, ReadWriteSpan)} to
* be invoked.
*/
protected boolean requiresStart() {
return true;
}
/**
* Indicates if span processor needs to be called on span end
*
* @return true, if this implementation would like {@link #doOnEnd(ReadableSpan)} to be invoked.
*/
protected boolean requiresEnd() {
return true;
}
protected CompletableResultCode doForceFlush() {
return CompletableResultCode.ofSuccess();
}
protected CompletableResultCode doShutdown() {
return CompletableResultCode.ofSuccess();
}
@Override
public final void onStart(Context context, ReadWriteSpan readWriteSpan) {
try {
if (requiresStart()) {
doOnStart(context, readWriteSpan);
}
} finally {
if (nextRequiresStart) {
next.onStart(context, readWriteSpan);
}
}
}
@Override
public final void onEnd(ReadableSpan readableSpan) {
ReadableSpan mappedTo = readableSpan;
try {
if (requiresEnd()) {
mappedTo = doOnEnd(readableSpan);
}
} finally {
if (mappedTo != null && nextRequiresEnd) {
next.onEnd(mappedTo);
}
}
}
@Override
public final boolean isStartRequired() {
return requiresStart() || nextRequiresStart;
}
@Override
public final boolean isEndRequired() {
return requiresEnd() || nextRequiresEnd;
}
@Override
public final CompletableResultCode shutdown() {
return CompletableResultCode.ofAll(Arrays.asList(doShutdown(), next.shutdown()));
}
@Override
public final CompletableResultCode forceFlush() {
return CompletableResultCode.ofAll(Arrays.asList(doForceFlush(), next.forceFlush()));
}
@Override
public final void close() {
SpanProcessor.super.close();
}
}

View File

@ -1,153 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.stacktrace.internal;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.data.SpanData;
import javax.annotation.Nullable;
/**
* A wrapper around an ended {@link ReadableSpan}, which allows mutation. This is done by wrapping
* the {@link SpanData} of the provided span and returning a mutated wrapper when {@link
* #toSpanData()} is called.
*
* <p>This class is not thread-safe.Note that after {@link #toSpanData()} has been called, no more
* mutation are allowed. This guarantees that the returned SpanData is safe to use across threads.
*/
public class MutableSpan implements ReadableSpan {
private final ReadableSpan delegate;
@Nullable private MutableSpanData mutableSpanData = null;
@Nullable private SpanData cachedDelegateSpanData = null;
private boolean frozen;
private MutableSpan(ReadableSpan delegate) {
if (!delegate.hasEnded()) {
throw new IllegalArgumentException("The provided span has not ended yet!");
}
this.delegate = delegate;
}
/**
* If the provided span is already mutable, it is casted and returned. Otherwise, it is wrapped in
* a new MutableSpan instance and returned.
*
* @param span the span to make mutable
*/
public static MutableSpan makeMutable(ReadableSpan span) {
if (span instanceof MutableSpan && !((MutableSpan) span).frozen) {
return (MutableSpan) span;
} else {
return new MutableSpan(span);
}
}
public ReadableSpan getOriginalSpan() {
return delegate;
}
private SpanData getDelegateSpanData() {
if (cachedDelegateSpanData == null) {
cachedDelegateSpanData = delegate.toSpanData();
}
return cachedDelegateSpanData;
}
@Override
public SpanData toSpanData() {
frozen = true;
if (mutableSpanData != null) {
return mutableSpanData;
}
return getDelegateSpanData();
}
private MutableSpanData mutate() {
if (frozen) {
throw new IllegalStateException(
"toSpanData() has already been called on this span, it is no longer mutable!");
}
if (mutableSpanData == null) {
mutableSpanData = new MutableSpanData(getDelegateSpanData());
}
return mutableSpanData;
}
@Nullable
@Override
public <T> T getAttribute(AttributeKey<T> key) {
if (mutableSpanData != null) {
return mutableSpanData.getAttribute(key);
} else {
return delegate.getAttribute(key);
}
}
public <T> void removeAttribute(AttributeKey<T> key) {
mutate().setAttribute(key, null);
}
public <T> void setAttribute(AttributeKey<T> key, @Nullable T value) {
mutate().setAttribute(key, value);
}
@Override
public String getName() {
if (mutableSpanData != null) {
return mutableSpanData.getName();
}
return delegate.getName();
}
public void setName(String name) {
if (name == null) {
throw new IllegalArgumentException("name must not be null");
}
mutate().setName(name);
}
@Override
public SpanContext getSpanContext() {
return delegate.getSpanContext();
}
@Override
public SpanContext getParentSpanContext() {
return delegate.getParentSpanContext();
}
@Override
@Deprecated
public io.opentelemetry.sdk.common.InstrumentationLibraryInfo getInstrumentationLibraryInfo() {
return delegate.getInstrumentationLibraryInfo();
}
@Override
public InstrumentationScopeInfo getInstrumentationScopeInfo() {
return delegate.getInstrumentationScopeInfo();
}
@Override
public boolean hasEnded() {
return delegate.hasEnded();
}
@Override
public long getLatencyNanos() {
return delegate.getLatencyNanos();
}
@Override
public SpanKind getKind() {
return delegate.getKind();
}
}

View File

@ -1,94 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.stacktrace.internal;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.trace.data.DelegatingSpanData;
import io.opentelemetry.sdk.trace.data.SpanData;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
public class MutableSpanData extends DelegatingSpanData {
@Nullable private Map<AttributeKey<?>, Object> attributeOverrides = null;
@Nullable private Attributes cachedMutatedAttributes = null;
@Nullable private String nameOverride = null;
protected MutableSpanData(SpanData delegate) {
super(delegate);
}
public <T> void setAttribute(AttributeKey<T> key, @Nullable T value) {
if (attributeOverrides != null
&& attributeOverrides.containsKey(key)
&& Objects.equals(attributeOverrides.get(key), value)) {
return;
}
T originalValue = super.getAttributes().get(key);
if (Objects.equals(originalValue, value)) {
if (attributeOverrides != null) {
cachedMutatedAttributes = null;
attributeOverrides.remove(key);
}
return;
}
if (attributeOverrides == null) {
attributeOverrides = new HashMap<>();
}
cachedMutatedAttributes = null;
attributeOverrides.put(key, value);
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Attributes getAttributes() {
Attributes original = super.getAttributes();
if (attributeOverrides == null || attributeOverrides.isEmpty()) {
return original;
}
if (cachedMutatedAttributes == null) {
AttributesBuilder attributesBuilder = Attributes.builder().putAll(original);
for (AttributeKey overrideKey : attributeOverrides.keySet()) {
Object value = attributeOverrides.get(overrideKey);
if (value == null) {
attributesBuilder.remove(overrideKey);
} else {
attributesBuilder.put(overrideKey, value);
}
}
cachedMutatedAttributes = attributesBuilder.build();
}
return cachedMutatedAttributes;
}
@SuppressWarnings("unchecked")
@Nullable
public <T> T getAttribute(AttributeKey<T> key) {
if (attributeOverrides != null && attributeOverrides.containsKey(key)) {
return (T) attributeOverrides.get(key);
}
return super.getAttributes().get(key);
}
public void setName(String name) {
nameOverride = name;
}
@Override
public String getName() {
if (nameOverride != null) {
return nameOverride;
}
return super.getName();
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.stacktrace;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.trace.ReadableSpan;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
public class StackTraceAutoConfigTest {
@Test
void defaultConfig() {
DefaultConfigProperties config = DefaultConfigProperties.createFromMap(Collections.emptyMap());
assertThat(StackTraceAutoConfig.getMinDuration(config)).isEqualTo(5000000L);
Predicate<ReadableSpan> filterPredicate = StackTraceAutoConfig.getFilterPredicate(config);
assertThat(filterPredicate).isNotNull();
}
@Test
void minDurationValue() {
Map<String, String> configMap = new HashMap<>();
configMap.put("otel.java.experimental.span-stacktrace.min.duration", "42ms");
DefaultConfigProperties config = DefaultConfigProperties.createFromMap(configMap);
assertThat(StackTraceAutoConfig.getMinDuration(config)).isEqualTo(42000000L);
}
@Test
void negativeMinDuration() {
Map<String, String> configMap = new HashMap<>();
configMap.put("otel.java.experimental.span-stacktrace.min.duration", "-1");
DefaultConfigProperties config = DefaultConfigProperties.createFromMap(configMap);
assertThat(StackTraceAutoConfig.getMinDuration(config)).isNegative();
}
@Test
void customFilter() {
Map<String, String> configMap = new HashMap<>();
configMap.put("otel.java.experimental.span-stacktrace.filter", MyFilter.class.getName());
DefaultConfigProperties config = DefaultConfigProperties.createFromMap(configMap);
Predicate<ReadableSpan> filterPredicate = StackTraceAutoConfig.getFilterPredicate(config);
assertThat(filterPredicate).isInstanceOf(MyFilter.class);
// default does not filter, so any negative value means we use the test filter
assertThat(filterPredicate.test(null)).isFalse();
}
public static class MyFilter implements Predicate<ReadableSpan> {
@Override
public boolean test(ReadableSpan readableSpan) {
return false;
}
}
@Test
void brokenFilter_classVisibility() {
testBrokenFilter(BrokenFilter.class.getName());
}
@Test
void brokenFilter_type() {
testBrokenFilter(Object.class.getName());
}
@Test
void brokenFilter_missingType() {
testBrokenFilter("missing.class.name");
}
private static void testBrokenFilter(String filterName) {
Map<String, String> configMap = new HashMap<>();
configMap.put("otel.java.experimental.span-stacktrace.filter", filterName);
DefaultConfigProperties config = DefaultConfigProperties.createFromMap(configMap);
Predicate<ReadableSpan> filterPredicate = StackTraceAutoConfig.getFilterPredicate(config);
assertThat(filterPredicate).isNotNull();
assertThat(filterPredicate.test(null)).isTrue();
}
private static class BrokenFilter extends MyFilter {}
}

View File

@ -6,19 +6,19 @@
package io.opentelemetry.contrib.stacktrace;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.contrib.stacktrace.internal.TestUtils;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder;
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes;
import java.time.Duration;
import java.time.Instant;
@ -36,47 +36,67 @@ class StackTraceSpanProcessorTest {
return Duration.ofMillis(ms).toNanos();
}
@Test
void tryInvalidMinDuration() {
assertThatCode(() -> new StackTraceSpanProcessor(-1, null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void durationAndFiltering() {
// on duration threshold
checkSpanWithStackTrace(span -> true, "1ms", msToNs(1));
checkSpanWithStackTrace("1ms", msToNs(1));
// over duration threshold
checkSpanWithStackTrace(span -> true, "1ms", msToNs(2));
checkSpanWithStackTrace("1ms", msToNs(2));
// under duration threshold
checkSpanWithoutStackTrace(span -> true, "2ms", msToNs(1));
checkSpanWithoutStackTrace(YesPredicate.class, "2ms", msToNs(1));
// filtering out span
checkSpanWithoutStackTrace(span -> false, "1ms", 20);
checkSpanWithoutStackTrace(NoPredicate.class, "1ms", msToNs(20));
}
public static class YesPredicate implements Predicate<ReadableSpan> {
@Override
public boolean test(ReadableSpan readableSpan) {
return true;
}
}
public static class NoPredicate implements Predicate<ReadableSpan> {
@Override
public boolean test(ReadableSpan readableSpan) {
return false;
}
}
@Test
void defaultConfig() {
long expectedDefault = msToNs(5);
checkSpanWithStackTrace(span -> true, null, expectedDefault);
checkSpanWithStackTrace(span -> true, null, expectedDefault + 1);
checkSpanWithoutStackTrace(span -> true, null, expectedDefault - 1);
checkSpanWithStackTrace(null, expectedDefault);
checkSpanWithStackTrace(null, expectedDefault + 1);
checkSpanWithoutStackTrace(YesPredicate.class, null, expectedDefault - 1);
}
@Test
void disabledConfig() {
checkSpanWithoutStackTrace(span -> true, "-1", 5);
checkSpanWithoutStackTrace(YesPredicate.class, "-1", 5);
}
@Test
void spanWithExistingStackTrace() {
checkSpan(
span -> true,
YesPredicate.class,
"1ms",
Duration.ofMillis(1).toNanos(),
sb -> sb.setAttribute(CodeIncubatingAttributes.CODE_STACKTRACE, "hello"),
stacktrace -> assertThat(stacktrace).isEqualTo("hello"));
}
private static void checkSpanWithStackTrace(
Predicate<ReadableSpan> filterPredicate, String configString, long spanDurationNanos) {
private static void checkSpanWithStackTrace(String minDurationString, long spanDurationNanos) {
checkSpan(
filterPredicate,
configString,
YesPredicate.class,
minDurationString,
spanDurationNanos,
Function.identity(),
(stackTrace) ->
@ -86,36 +106,53 @@ class StackTraceSpanProcessorTest {
}
private static void checkSpanWithoutStackTrace(
Predicate<ReadableSpan> filterPredicate, String configString, long spanDurationNanos) {
Class<? extends Predicate<?>> predicateClass,
String minDurationString,
long spanDurationNanos) {
checkSpan(
filterPredicate,
configString,
predicateClass,
minDurationString,
spanDurationNanos,
Function.identity(),
(stackTrace) -> assertThat(stackTrace).describedAs("no stack trace expected").isNull());
}
private static void checkSpan(
Predicate<ReadableSpan> filterPredicate,
String configString,
Class<? extends Predicate<?>> predicateClass,
String minDurationString,
long spanDurationNanos,
Function<SpanBuilder, SpanBuilder> customizeSpanBuilder,
Consumer<String> stackTraceCheck) {
// they must be re-created as they are shutdown when the span processor is closed
// must be re-created on every test as exporter is shut down on span processor close
InMemorySpanExporter spansExporter = InMemorySpanExporter.create();
SpanProcessor exportProcessor = SimpleSpanProcessor.create(spansExporter);
Map<String, String> configMap = new HashMap<>();
if (configString != null) {
configMap.put("otel.java.experimental.span-stacktrace.min.duration", configString);
}
AutoConfiguredOpenTelemetrySdkBuilder sdkBuilder = AutoConfiguredOpenTelemetrySdk.builder();
sdkBuilder.addPropertiesSupplier(
() -> {
Map<String, String> configMap = new HashMap<>();
try (SpanProcessor processor =
new StackTraceSpanProcessor(
exportProcessor, DefaultConfigProperties.createFromMap(configMap), filterPredicate)) {
configMap.put("otel.metrics.exporter", "none");
configMap.put("otel.traces.exporter", "logging");
configMap.put("otel.logs.exporter", "none");
if (minDurationString != null) {
configMap.put("otel.java.experimental.span-stacktrace.min.duration", minDurationString);
}
if (predicateClass != null) {
configMap.put(
"otel.java.experimental.span-stacktrace.filter", predicateClass.getName());
}
return configMap;
});
// duplicate export to our in-memory span exporter
sdkBuilder.addSpanExporterCustomizer(
(exporter, config) -> SpanExporter.composite(exporter, spansExporter));
new StackTraceAutoConfig().customize(sdkBuilder);
try (OpenTelemetrySdk sdk = sdkBuilder.build().getOpenTelemetrySdk()) {
OpenTelemetrySdk sdk = TestUtils.sdkWith(processor);
Tracer tracer = sdk.getTracer("test");
Instant start = Instant.now();

View File

@ -1,109 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.stacktrace.internal;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class AbstractSimpleChainingSpanProcessorTest {
private InMemorySpanExporter spans;
private SpanProcessor exportProcessor;
@BeforeEach
public void setup() {
spans = InMemorySpanExporter.create();
exportProcessor = SimpleSpanProcessor.create(spans);
}
@Test
public void testSpanDropping() {
SpanProcessor processor =
new AbstractSimpleChainingSpanProcessor(exportProcessor) {
@Override
protected ReadableSpan doOnEnd(ReadableSpan readableSpan) {
if (readableSpan.getName().startsWith("dropMe")) {
return null;
} else {
return readableSpan;
}
}
};
try (OpenTelemetrySdk sdk = TestUtils.sdkWith(processor)) {
Tracer tracer = sdk.getTracer("dummy-tracer");
tracer.spanBuilder("dropMe1").startSpan().end();
tracer.spanBuilder("sendMe").startSpan().end();
tracer.spanBuilder("dropMe2").startSpan().end();
assertThat(spans.getFinishedSpanItems())
.hasSize(1)
.anySatisfy(span -> assertThat(span).hasName("sendMe"));
}
}
@Test
public void testAttributeUpdate() {
AttributeKey<String> keepMeKey = AttributeKey.stringKey("keepMe");
AttributeKey<String> updateMeKey = AttributeKey.stringKey("updateMe");
AttributeKey<String> addMeKey = AttributeKey.stringKey("addMe");
AttributeKey<String> removeMeKey = AttributeKey.stringKey("removeMe");
SpanProcessor second =
new AbstractSimpleChainingSpanProcessor(exportProcessor) {
@Override
protected ReadableSpan doOnEnd(ReadableSpan readableSpan) {
MutableSpan span = MutableSpan.makeMutable(readableSpan);
span.setAttribute(addMeKey, "added");
return span;
}
};
SpanProcessor first =
new AbstractSimpleChainingSpanProcessor(second) {
@Override
protected ReadableSpan doOnEnd(ReadableSpan readableSpan) {
MutableSpan span = MutableSpan.makeMutable(readableSpan);
span.setAttribute(updateMeKey, "updated");
span.removeAttribute(removeMeKey);
return span;
}
};
try (OpenTelemetrySdk sdk = TestUtils.sdkWith(first)) {
Tracer tracer = sdk.getTracer("dummy-tracer");
tracer
.spanBuilder("dropMe1")
.startSpan()
.setAttribute(keepMeKey, "keep-me-original")
.setAttribute(removeMeKey, "remove-me-original")
.setAttribute(updateMeKey, "foo")
.end();
assertThat(spans.getFinishedSpanItems())
.hasSize(1)
.anySatisfy(
span -> {
Attributes attribs = span.getAttributes();
assertThat(attribs)
.hasSize(3)
.containsEntry(keepMeKey, "keep-me-original")
.containsEntry(updateMeKey, "updated")
.containsEntry(addMeKey, "added");
});
}
}
}

View File

@ -1,173 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.stacktrace.internal;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.data.SpanData;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
public class MutableSpanTest {
@Test
public void noSpanDataCopyWithoutMutation() {
ReadableSpan original = createSpan("foo", builder -> {});
MutableSpan mutable = MutableSpan.makeMutable(original);
SpanData first = mutable.toSpanData();
SpanData second = mutable.toSpanData();
assertThat(first.getClass().getName())
.isEqualTo("io.opentelemetry.sdk.trace.AutoValue_SpanWrapper");
assertThat(first).isSameAs(second);
}
@Test
public void freezeAfterMutation() {
ReadableSpan original = createSpan("foo", builder -> {});
MutableSpan mutable1 = MutableSpan.makeMutable(original);
mutable1.setName("updated");
mutable1.toSpanData();
assertThatThrownBy(() -> mutable1.setName("should not be allowed"))
.isInstanceOf(IllegalStateException.class);
// it should be okay to wrap again and then mutate
MutableSpan mutable2 = MutableSpan.makeMutable(mutable1);
mutable2.setName("updated again");
assertThat(mutable1.toSpanData()).hasName("updated");
assertThat(mutable2.toSpanData()).hasName("updated again");
assertThat(mutable1.getOriginalSpan()).isSameAs(original);
assertThat(mutable2.getOriginalSpan()).isSameAs(mutable1);
}
@Test
public void testAttributesMutations() {
AttributeKey<String> keep = AttributeKey.stringKey("keep-me");
AttributeKey<String> update = AttributeKey.stringKey("update-me");
AttributeKey<String> remove = AttributeKey.stringKey("remove-me");
AttributeKey<String> add = AttributeKey.stringKey("add-me");
ReadableSpan original =
createSpan(
"foo",
builder -> {
builder.setAttribute(keep, "keep-original");
builder.setAttribute(update, "update-original");
builder.setAttribute(remove, "remove-original");
});
MutableSpan mutable = MutableSpan.makeMutable(original);
assertThat(mutable.getAttribute(keep)).isEqualTo("keep-original");
assertThat(mutable.getAttribute(update)).isEqualTo("update-original");
assertThat(mutable.getAttribute(remove)).isEqualTo("remove-original");
mutable.setAttribute(add, "added");
mutable.removeAttribute(remove);
mutable.setAttribute(update, "updated");
assertThat(mutable.getAttribute(keep)).isEqualTo("keep-original");
assertThat(mutable.getAttribute(update)).isEqualTo("updated");
assertThat(mutable.getAttribute(remove)).isNull();
assertThat(mutable.getAttribute(add)).isEqualTo("added");
// check again after the MutableSpan has been frozen due ot the toSpanData() call
assertThat(mutable.toSpanData().getAttributes())
.hasSize(3)
.containsEntry(keep, "keep-original")
.containsEntry(update, "updated")
.containsEntry(add, "added");
assertThat(mutable.getAttribute(keep)).isEqualTo("keep-original");
assertThat(mutable.getAttribute(update)).isEqualTo("updated");
assertThat(mutable.getAttribute(remove)).isNull();
assertThat(mutable.getAttribute(add)).isEqualTo("added");
// Ensure attributes are cached
assertThat(mutable.toSpanData().getAttributes()).isSameAs(mutable.toSpanData().getAttributes());
}
@Test
public void testAttributesReusedIfNotMutated() {
AttributeKey<String> key = AttributeKey.stringKey("first-key");
AttributeKey<String> cancelledKey = AttributeKey.stringKey("second-key");
ReadableSpan original =
createSpan(
"foo",
builder -> {
builder.setAttribute(key, "original");
});
MutableSpan mutable1 = MutableSpan.makeMutable(original);
mutable1.setAttribute(key, "updated");
mutable1.setAttribute(cancelledKey, "removed later");
mutable1.setAttribute(key, "original");
mutable1.removeAttribute(cancelledKey);
SpanData mutatedSpanData = mutable1.toSpanData();
assertThat(mutatedSpanData.getAttributes()).isSameAs(original.toSpanData().getAttributes());
}
@Test
public void noDoubleWrapping() {
ReadableSpan original = createSpan("foo", builder -> {});
MutableSpan mutable = MutableSpan.makeMutable(original);
assertThat(MutableSpan.makeMutable(mutable)).isSameAs(mutable);
mutable.setName("updated");
assertThat(MutableSpan.makeMutable(mutable)).isSameAs(mutable);
}
private static ReadableSpan createSpan(String name, Consumer<SpanBuilder> spanCustomizer) {
AtomicReference<ReadableSpan> resultSpan = new AtomicReference<>();
SpanProcessor collecting =
new SpanProcessor() {
@Override
public void onStart(Context parentContext, ReadWriteSpan span) {}
@Override
public boolean isStartRequired() {
return false;
}
@Override
public void onEnd(ReadableSpan span) {
resultSpan.set(span);
}
@Override
public boolean isEndRequired() {
return true;
}
};
try (OpenTelemetrySdk sdk = TestUtils.sdkWith(collecting)) {
SpanBuilder builder = sdk.getTracer("my-tracer").spanBuilder(name);
spanCustomizer.accept(builder);
builder.startSpan().end();
return resultSpan.get();
}
}
}

View File

@ -1,21 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.stacktrace.internal;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SpanProcessor;
public class TestUtils {
private TestUtils() {}
public static OpenTelemetrySdk sdkWith(SpanProcessor processor) {
return OpenTelemetrySdk.builder()
.setTracerProvider(SdkTracerProvider.builder().addSpanProcessor(processor).build())
.build();
}
}