span stacktrace refactor + autoconfig (#1499)
This commit is contained in:
parent
193f1f53eb
commit
ef0caea27e
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue