Skip to content

Commit ef0caea

Browse files
authored
span stacktrace refactor + autoconfig (#1499)
1 parent 193f1f5 commit ef0caea

File tree

12 files changed

+315
-809
lines changed

12 files changed

+315
-809
lines changed

span-stacktrace/README.md

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,24 @@ This module provides a `SpanProcessor` that captures the [`code.stacktrace`](htt
66
Capturing the stack trace is an expensive operation and does not provide any value on short-lived spans.
77
As a consequence it should only be used when the span duration is known, thus on span end.
88

9-
However, the current SDK API does not allow to modify span attributes on span end, so we have to
10-
introduce other components to make it work as expected.
9+
## Usage and configuration
1110

12-
## Usage
11+
This extension supports autoconfiguration, so it will be automatically enabled by OpenTelemetry
12+
SDK when included in the application runtime dependencies.
1313

14-
This extension does not support autoconfiguration because it needs to wrap the `SimpleSpanExporter`
15-
or `BatchingSpanProcessor` that invokes the `SpanExporter`.
14+
`otel.java.experimental.span-stacktrace.min.duration`
1615

17-
As a consequence you have to use [Manual SDK setup](#manual-sdk-setup)
18-
section below to configure it.
16+
- allows to configure the minimal duration for which spans have a stacktrace captured
17+
- defaults to 5ms
18+
- a value of zero will include all spans
19+
- a negative value will disable the feature
1920

20-
### Manual SDK setup
21+
`otel.java.experimental.span-stacktrace.filter`
2122

22-
Here is an example registration of `StackTraceSpanProcessor` to capture stack trace for all
23-
the spans that have a duration >= 1 ms. The spans that have an `ignorespan` string attribute
24-
will be ignored.
25-
26-
```java
27-
InMemorySpanExporter spansExporter = InMemorySpanExporter.create();
28-
SpanProcessor exportProcessor = SimpleSpanProcessor.create(spansExporter);
29-
30-
Map<String, String> configMap = new HashMap<>();
31-
configMap.put("otel.java.experimental.span-stacktrace.min.duration", "1ms");
32-
ConfigProperties config = DefaultConfigProperties.createFromMap(configMap);
33-
34-
Predicate<ReadableSpan> filterPredicate = readableSpan -> {
35-
if(readableSpan.getAttribute(AttributeKey.stringKey("ignorespan")) != null){
36-
return false;
37-
}
38-
return true;
39-
};
40-
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
41-
.addSpanProcessor(new StackTraceSpanProcessor(exportProcessor, config, filterPredicate))
42-
.build();
43-
44-
OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
45-
```
46-
47-
### Configuration
48-
49-
The `otel.java.experimental.span-stacktrace.min.duration` configuration option (defaults to 5ms) allows configuring
50-
the minimal duration for which spans should have a stacktrace captured.
51-
52-
Setting `otel.java.experimental.span-stacktrace.min.duration` to zero will include all spans, and using a negative
53-
value will disable the feature.
23+
- allows to filter spans to be excluded from stacktrace capture
24+
- defaults to include all spans.
25+
- value is the class name of a class implementing `java.util.function.Predicate<ReadableSpan>`
26+
- filter class must be publicly accessible and provide a no-arg constructor
5427

5528
## Component owners
5629

span-stacktrace/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ description = "OpenTelemetry Java span stacktrace capture module"
77
otelJava.moduleName.set("io.opentelemetry.contrib.stacktrace")
88

99
dependencies {
10+
annotationProcessor("com.google.auto.service:auto-service")
11+
compileOnly("com.google.auto.service:auto-service-annotations")
12+
1013
api("io.opentelemetry:opentelemetry-sdk")
1114
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
1215

@@ -16,4 +19,9 @@ dependencies {
1619
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
1720

1821
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
22+
23+
testAnnotationProcessor("com.google.auto.service:auto-service")
24+
testCompileOnly("com.google.auto.service:auto-service-annotations")
25+
26+
testImplementation("io.opentelemetry:opentelemetry-exporter-logging")
1927
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.stacktrace;
7+
8+
import com.google.auto.service.AutoService;
9+
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
10+
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
11+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
12+
import io.opentelemetry.sdk.trace.ReadableSpan;
13+
import java.lang.reflect.Constructor;
14+
import java.lang.reflect.InvocationTargetException;
15+
import java.time.Duration;
16+
import java.util.function.Predicate;
17+
import java.util.logging.Level;
18+
import java.util.logging.Logger;
19+
import javax.annotation.Nullable;
20+
21+
@AutoService(AutoConfigurationCustomizerProvider.class)
22+
public class StackTraceAutoConfig implements AutoConfigurationCustomizerProvider {
23+
24+
private static final Logger log = Logger.getLogger(StackTraceAutoConfig.class.getName());
25+
26+
private static final String CONFIG_MIN_DURATION =
27+
"otel.java.experimental.span-stacktrace.min.duration";
28+
private static final Duration CONFIG_MIN_DURATION_DEFAULT = Duration.ofMillis(5);
29+
30+
private static final String CONFIG_FILTER = "otel.java.experimental.span-stacktrace.filter";
31+
32+
@Override
33+
public void customize(AutoConfigurationCustomizer config) {
34+
config.addTracerProviderCustomizer(
35+
(providerBuilder, properties) -> {
36+
long minDuration = getMinDuration(properties);
37+
if (minDuration >= 0) {
38+
Predicate<ReadableSpan> filter = getFilterPredicate(properties);
39+
providerBuilder.addSpanProcessor(new StackTraceSpanProcessor(minDuration, filter));
40+
}
41+
return providerBuilder;
42+
});
43+
}
44+
45+
// package-private for testing
46+
static long getMinDuration(ConfigProperties properties) {
47+
long minDuration =
48+
properties.getDuration(CONFIG_MIN_DURATION, CONFIG_MIN_DURATION_DEFAULT).toNanos();
49+
if (minDuration < 0) {
50+
log.fine("Stack traces capture is disabled");
51+
} else {
52+
log.log(
53+
Level.FINE,
54+
"Stack traces will be added to spans with a minimum duration of {0} nanos",
55+
minDuration);
56+
}
57+
return minDuration;
58+
}
59+
60+
// package private for testing
61+
static Predicate<ReadableSpan> getFilterPredicate(ConfigProperties properties) {
62+
String filterClass = properties.getString(CONFIG_FILTER);
63+
Predicate<ReadableSpan> filter = null;
64+
if (filterClass != null) {
65+
Class<?> filterType = getFilterType(filterClass);
66+
if (filterType != null) {
67+
filter = getFilterInstance(filterType);
68+
}
69+
}
70+
71+
if (filter == null) {
72+
// if value is set, lack of filtering is likely an error and must be reported
73+
Level disabledLogLevel = filterClass != null ? Level.SEVERE : Level.FINE;
74+
log.log(disabledLogLevel, "Span stacktrace filtering disabled");
75+
return span -> true;
76+
} else {
77+
log.fine("Span stacktrace filtering enabled with: " + filterClass);
78+
return filter;
79+
}
80+
}
81+
82+
@Nullable
83+
private static Class<?> getFilterType(String filterClass) {
84+
try {
85+
Class<?> filterType = Class.forName(filterClass);
86+
if (!Predicate.class.isAssignableFrom(filterType)) {
87+
log.severe("Filter must be a subclass of java.util.function.Predicate");
88+
return null;
89+
}
90+
return filterType;
91+
} catch (ClassNotFoundException e) {
92+
log.severe("Unable to load filter class: " + filterClass);
93+
return null;
94+
}
95+
}
96+
97+
@Nullable
98+
@SuppressWarnings("unchecked")
99+
private static Predicate<ReadableSpan> getFilterInstance(Class<?> filterType) {
100+
try {
101+
Constructor<?> constructor = filterType.getConstructor();
102+
return (Predicate<ReadableSpan>) constructor.newInstance();
103+
} catch (NoSuchMethodException
104+
| InstantiationException
105+
| IllegalAccessException
106+
| InvocationTargetException e) {
107+
log.severe("Unable to create filter instance with no-arg constructor: " + filterType);
108+
return null;
109+
}
110+
}
111+
}

span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessor.java

Lines changed: 26 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,96 +6,74 @@
66
package io.opentelemetry.contrib.stacktrace;
77

88
import io.opentelemetry.api.common.AttributeKey;
9-
import io.opentelemetry.contrib.stacktrace.internal.AbstractSimpleChainingSpanProcessor;
10-
import io.opentelemetry.contrib.stacktrace.internal.MutableSpan;
11-
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
9+
import io.opentelemetry.context.Context;
10+
import io.opentelemetry.sdk.trace.ReadWriteSpan;
1211
import io.opentelemetry.sdk.trace.ReadableSpan;
13-
import io.opentelemetry.sdk.trace.SpanProcessor;
12+
import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor;
1413
import java.io.PrintWriter;
1514
import java.io.StringWriter;
16-
import java.time.Duration;
1715
import java.util.function.Predicate;
18-
import java.util.logging.Level;
19-
import java.util.logging.Logger;
2016

21-
public class StackTraceSpanProcessor extends AbstractSimpleChainingSpanProcessor {
22-
23-
private static final String CONFIG_MIN_DURATION =
24-
"otel.java.experimental.span-stacktrace.min.duration";
25-
private static final Duration CONFIG_MIN_DURATION_DEFAULT = Duration.ofMillis(5);
17+
public class StackTraceSpanProcessor implements ExtendedSpanProcessor {
2618

2719
// inlined incubating attribute to prevent direct dependency on incubating semconv
2820
private static final AttributeKey<String> SPAN_STACKTRACE =
2921
AttributeKey.stringKey("code.stacktrace");
3022

31-
private static final Logger logger = Logger.getLogger(StackTraceSpanProcessor.class.getName());
32-
3323
private final long minSpanDurationNanos;
3424

3525
private final Predicate<ReadableSpan> filterPredicate;
3626

3727
/**
38-
* @param next next span processor to invoke
3928
* @param minSpanDurationNanos minimum span duration in ns for stacktrace capture
4029
* @param filterPredicate extra filter function to exclude spans if needed
4130
*/
4231
public StackTraceSpanProcessor(
43-
SpanProcessor next, long minSpanDurationNanos, Predicate<ReadableSpan> filterPredicate) {
44-
super(next);
45-
this.minSpanDurationNanos = minSpanDurationNanos;
46-
this.filterPredicate = filterPredicate;
32+
long minSpanDurationNanos, Predicate<ReadableSpan> filterPredicate) {
4733
if (minSpanDurationNanos < 0) {
48-
logger.log(Level.FINE, "Stack traces capture is disabled");
49-
} else {
50-
logger.log(
51-
Level.FINE,
52-
"Stack traces will be added to spans with a minimum duration of {0} nanos",
53-
minSpanDurationNanos);
34+
throw new IllegalArgumentException("minimal span duration must be positive or zero");
5435
}
55-
}
5636

57-
/**
58-
* @param next next span processor to invoke
59-
* @param config configuration
60-
* @param filterPredicate extra filter function to exclude spans if needed
61-
*/
62-
public StackTraceSpanProcessor(
63-
SpanProcessor next, ConfigProperties config, Predicate<ReadableSpan> filterPredicate) {
64-
this(
65-
next,
66-
config.getDuration(CONFIG_MIN_DURATION, CONFIG_MIN_DURATION_DEFAULT).toNanos(),
67-
filterPredicate);
37+
this.minSpanDurationNanos = minSpanDurationNanos;
38+
this.filterPredicate = filterPredicate;
6839
}
6940

7041
@Override
71-
protected boolean requiresStart() {
42+
public boolean isStartRequired() {
7243
return false;
7344
}
7445

7546
@Override
76-
protected boolean requiresEnd() {
47+
public void onStart(Context context, ReadWriteSpan readWriteSpan) {}
48+
49+
@Override
50+
public boolean isOnEndingRequired() {
7751
return true;
7852
}
7953

8054
@Override
81-
protected ReadableSpan doOnEnd(ReadableSpan span) {
82-
if (minSpanDurationNanos < 0 || span.getLatencyNanos() < minSpanDurationNanos) {
83-
return span;
55+
public void onEnding(ReadWriteSpan span) {
56+
if (span.getLatencyNanos() < minSpanDurationNanos) {
57+
return;
8458
}
8559
if (span.getAttribute(SPAN_STACKTRACE) != null) {
8660
// Span already has a stacktrace, do not override
87-
return span;
61+
return;
8862
}
8963
if (!filterPredicate.test(span)) {
90-
return span;
64+
return;
9165
}
92-
MutableSpan mutableSpan = MutableSpan.makeMutable(span);
66+
span.setAttribute(SPAN_STACKTRACE, generateSpanEndStacktrace());
67+
}
9368

94-
String stacktrace = generateSpanEndStacktrace();
95-
mutableSpan.setAttribute(SPAN_STACKTRACE, stacktrace);
96-
return mutableSpan;
69+
@Override
70+
public boolean isEndRequired() {
71+
return false;
9772
}
9873

74+
@Override
75+
public void onEnd(ReadableSpan readableSpan) {}
76+
9977
private static String generateSpanEndStacktrace() {
10078
Throwable exception = new Throwable();
10179
StringWriter stringWriter = new StringWriter();

0 commit comments

Comments
 (0)