Skip to content

Commit 151b744

Browse files
SylvainJugejackshirazi
authored andcommitted
add span stacktrace config option (open-telemetry#1414)
Co-authored-by: jackshirazi <[email protected]>
1 parent 9ee5fb1 commit 151b744

File tree

4 files changed

+106
-34
lines changed

4 files changed

+106
-34
lines changed

span-stacktrace/README.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,38 @@ section below to configure it.
2020
### Manual SDK setup
2121

2222
Here is an example registration of `StackTraceSpanProcessor` to capture stack trace for all
23-
the spans that have a duration >= 1000 ns. The spans that have an `ignorespan` string attribute
23+
the spans that have a duration >= 1 ms. The spans that have an `ignorespan` string attribute
2424
will be ignored.
2525

2626
```java
2727
InMemorySpanExporter spansExporter = InMemorySpanExporter.create();
2828
SpanProcessor exportProcessor = SimpleSpanProcessor.create(spansExporter);
2929

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+
3034
Predicate<ReadableSpan> filterPredicate = readableSpan -> {
3135
if(readableSpan.getAttribute(AttributeKey.stringKey("ignorespan")) != null){
3236
return false;
3337
}
3438
return true;
3539
};
3640
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
37-
.addSpanProcessor(new StackTraceSpanProcessor(exportProcessor, 1000, filterPredicate))
41+
.addSpanProcessor(new StackTraceSpanProcessor(exportProcessor, config, filterPredicate))
3842
.build();
3943

4044
OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
4145
```
4246

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.
54+
4355
## Component owners
4456

4557
- [Jack Shirazi](https://github.com/jackshirazi), Elastic

span-stacktrace/build.gradle.kts

+5
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,10 @@ dependencies {
1010
api("io.opentelemetry:opentelemetry-sdk")
1111
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
1212

13+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
14+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
15+
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
16+
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
17+
1318
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
1419
}

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

+28-5
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,22 @@
88
import io.opentelemetry.api.common.AttributeKey;
99
import io.opentelemetry.contrib.stacktrace.internal.AbstractSimpleChainingSpanProcessor;
1010
import io.opentelemetry.contrib.stacktrace.internal.MutableSpan;
11+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
1112
import io.opentelemetry.sdk.trace.ReadableSpan;
1213
import io.opentelemetry.sdk.trace.SpanProcessor;
1314
import java.io.PrintWriter;
1415
import java.io.StringWriter;
16+
import java.time.Duration;
1517
import java.util.function.Predicate;
1618
import java.util.logging.Level;
1719
import java.util.logging.Logger;
1820

1921
public class StackTraceSpanProcessor extends AbstractSimpleChainingSpanProcessor {
2022

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);
26+
2127
// inlined incubating attribute to prevent direct dependency on incubating semconv
2228
private static final AttributeKey<String> SPAN_STACKTRACE =
2329
AttributeKey.stringKey("code.stacktrace");
@@ -38,10 +44,27 @@ public StackTraceSpanProcessor(
3844
super(next);
3945
this.minSpanDurationNanos = minSpanDurationNanos;
4046
this.filterPredicate = filterPredicate;
41-
logger.log(
42-
Level.FINE,
43-
"Stack traces will be added to spans with a minimum duration of {0} nanos",
44-
minSpanDurationNanos);
47+
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);
54+
}
55+
}
56+
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);
4568
}
4669

4770
@Override
@@ -56,7 +79,7 @@ protected boolean requiresEnd() {
5679

5780
@Override
5881
protected ReadableSpan doOnEnd(ReadableSpan span) {
59-
if (span.getLatencyNanos() < minSpanDurationNanos) {
82+
if (minSpanDurationNanos < 0 || span.getLatencyNanos() < minSpanDurationNanos) {
6083
return span;
6184
}
6285
if (span.getAttribute(SPAN_STACKTRACE) != null) {

span-stacktrace/src/test/java/io/opentelemetry/contrib/stacktrace/StackTraceSpanProcessorTest.java

+59-27
Original file line numberDiff line numberDiff line change
@@ -13,73 +13,107 @@
1313
import io.opentelemetry.context.Scope;
1414
import io.opentelemetry.contrib.stacktrace.internal.TestUtils;
1515
import io.opentelemetry.sdk.OpenTelemetrySdk;
16+
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
1617
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
1718
import io.opentelemetry.sdk.trace.ReadableSpan;
1819
import io.opentelemetry.sdk.trace.SpanProcessor;
1920
import io.opentelemetry.sdk.trace.data.SpanData;
2021
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
2122
import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes;
23+
import java.time.Duration;
2224
import java.time.Instant;
25+
import java.util.HashMap;
2326
import java.util.List;
27+
import java.util.Map;
2428
import java.util.function.Consumer;
2529
import java.util.function.Function;
2630
import java.util.function.Predicate;
27-
import org.junit.jupiter.api.BeforeEach;
2831
import org.junit.jupiter.api.Test;
2932

3033
class StackTraceSpanProcessorTest {
3134

32-
private InMemorySpanExporter spansExporter;
33-
private SpanProcessor exportProcessor;
34-
35-
@BeforeEach
36-
public void setup() {
37-
spansExporter = InMemorySpanExporter.create();
38-
exportProcessor = SimpleSpanProcessor.create(spansExporter);
35+
private static long msToNs(int ms) {
36+
return Duration.ofMillis(ms).toNanos();
3937
}
4038

4139
@Test
4240
void durationAndFiltering() {
41+
// on duration threshold
42+
checkSpanWithStackTrace(span -> true, "1ms", msToNs(1));
4343
// over duration threshold
44-
testSpan(span -> true, 11, 1);
44+
checkSpanWithStackTrace(span -> true, "1ms", msToNs(2));
4545
// under duration threshold
46-
testSpan(span -> true, 9, 0);
46+
checkSpanWithoutStackTrace(span -> true, "2ms", msToNs(1));
4747

4848
// filtering out span
49-
testSpan(span -> false, 20, 0);
49+
checkSpanWithoutStackTrace(span -> false, "1ms", 20);
50+
}
51+
52+
@Test
53+
void defaultConfig() {
54+
long expectedDefault = msToNs(5);
55+
checkSpanWithStackTrace(span -> true, null, expectedDefault);
56+
checkSpanWithStackTrace(span -> true, null, expectedDefault + 1);
57+
checkSpanWithoutStackTrace(span -> true, null, expectedDefault - 1);
58+
}
59+
60+
@Test
61+
void disabledConfig() {
62+
checkSpanWithoutStackTrace(span -> true, "-1", 5);
5063
}
5164

5265
@Test
5366
void spanWithExistingStackTrace() {
54-
testSpan(
67+
checkSpan(
5568
span -> true,
56-
20,
57-
1,
69+
"1ms",
70+
Duration.ofMillis(1).toNanos(),
5871
sb -> sb.setAttribute(CodeIncubatingAttributes.CODE_STACKTRACE, "hello"),
5972
stacktrace -> assertThat(stacktrace).isEqualTo("hello"));
6073
}
6174

62-
private void testSpan(
63-
Predicate<ReadableSpan> filterPredicate, long spanDurationNanos, int expectedSpansCount) {
64-
testSpan(
75+
private static void checkSpanWithStackTrace(
76+
Predicate<ReadableSpan> filterPredicate, String configString, long spanDurationNanos) {
77+
checkSpan(
6578
filterPredicate,
79+
configString,
6680
spanDurationNanos,
67-
expectedSpansCount,
6881
Function.identity(),
6982
(stackTrace) ->
7083
assertThat(stackTrace)
7184
.describedAs("span stack trace should contain caller class name")
7285
.contains(StackTraceSpanProcessorTest.class.getCanonicalName()));
7386
}
7487

75-
private void testSpan(
88+
private static void checkSpanWithoutStackTrace(
89+
Predicate<ReadableSpan> filterPredicate, String configString, long spanDurationNanos) {
90+
checkSpan(
91+
filterPredicate,
92+
configString,
93+
spanDurationNanos,
94+
Function.identity(),
95+
(stackTrace) -> assertThat(stackTrace).describedAs("no stack trace expected").isNull());
96+
}
97+
98+
private static void checkSpan(
7699
Predicate<ReadableSpan> filterPredicate,
100+
String configString,
77101
long spanDurationNanos,
78-
int expectedSpansCount,
79102
Function<SpanBuilder, SpanBuilder> customizeSpanBuilder,
80103
Consumer<String> stackTraceCheck) {
104+
105+
// they must be re-created as they are shutdown when the span processor is closed
106+
InMemorySpanExporter spansExporter = InMemorySpanExporter.create();
107+
SpanProcessor exportProcessor = SimpleSpanProcessor.create(spansExporter);
108+
109+
Map<String, String> configMap = new HashMap<>();
110+
if (configString != null) {
111+
configMap.put("otel.java.experimental.span-stacktrace.min.duration", configString);
112+
}
113+
81114
try (SpanProcessor processor =
82-
new StackTraceSpanProcessor(exportProcessor, 10, filterPredicate)) {
115+
new StackTraceSpanProcessor(
116+
exportProcessor, DefaultConfigProperties.createFromMap(configMap), filterPredicate)) {
83117

84118
OpenTelemetrySdk sdk = TestUtils.sdkWith(processor);
85119
Tracer tracer = sdk.getTracer("test");
@@ -96,14 +130,12 @@ private void testSpan(
96130
}
97131

98132
List<SpanData> finishedSpans = spansExporter.getFinishedSpanItems();
99-
assertThat(finishedSpans).hasSize(expectedSpansCount);
133+
assertThat(finishedSpans).hasSize(1);
100134

101-
if (!finishedSpans.isEmpty()) {
102-
String stackTrace =
103-
finishedSpans.get(0).getAttributes().get(CodeIncubatingAttributes.CODE_STACKTRACE);
135+
String stackTrace =
136+
finishedSpans.get(0).getAttributes().get(CodeIncubatingAttributes.CODE_STACKTRACE);
104137

105-
stackTraceCheck.accept(stackTrace);
106-
}
138+
stackTraceCheck.accept(stackTrace);
107139
}
108140
}
109141
}

0 commit comments

Comments
 (0)