Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

span stacktrace refactor + autoconfig #1499

Merged
merged 12 commits into from
Oct 17, 2024
53 changes: 13 additions & 40 deletions span-stacktrace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions span-stacktrace/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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")
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
this.minSpanDurationNanos = minSpanDurationNanos;
this.filterPredicate = filterPredicate;
long minSpanDurationNanos, Predicate<ReadableSpan> 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);
throw new IllegalArgumentException("minimal span duration must be positive or zero");
}
}

/**
* @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);
this.minSpanDurationNanos = minSpanDurationNanos;
this.filterPredicate = 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);
span.setAttribute(SPAN_STACKTRACE, generateSpanEndStacktrace());
}

String stacktrace = generateSpanEndStacktrace();
mutableSpan.setAttribute(SPAN_STACKTRACE, stacktrace);
return mutableSpan;
@Override
public boolean isEndRequired() {
return false;
}

@Override
public void onEnd(ReadableSpan readableSpan) {}

private static String generateSpanEndStacktrace() {
Throwable exception = new Throwable();
StringWriter stringWriter = new StringWriter();
Expand Down
Loading
Loading