Skip to content

Commit 425bfb9

Browse files
authored
stacktrace span processor (#1255)
1 parent 0909264 commit 425bfb9

File tree

12 files changed

+929
-0
lines changed

12 files changed

+929
-0
lines changed

.github/component_owners.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,7 @@ components:
6666
kafka-exporter:
6767
- spockz
6868
- vincentfree
69+
span-stacktrace:
70+
- jackshirazi
71+
- jonaskunz
72+
- sylvainjuge

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ include(":static-instrumenter:bootstrap")
8686
include(":static-instrumenter:test-app")
8787
include(":kafka-exporter")
8888
include(":gcp-resources")
89+
include(":span-stacktrace")

span-stacktrace/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
# Span stacktrace capture
3+
4+
This module provides a `SpanProcessor` that captures the [`code.stacktrace`](https://opentelemetry.io/docs/specs/semconv/attributes-registry/code/).
5+
6+
Capturing the stack trace is an expensive operation and does not provide any value on short-lived spans.
7+
As a consequence it should only be used when the span duration is known, thus on span end.
8+
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.
11+
12+
## Component owners
13+
14+
- [Jack Shirazi](https://github.com/jackshirazi), Elastic
15+
- [Jonas Kunz](https://github.com/jonaskunz), Elastic
16+
- [Sylvain Juge](https://github.com/sylvainjuge), Elastic
17+
18+
Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).

span-stacktrace/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
plugins {
2+
id("otel.java-conventions")
3+
}
4+
5+
description = "OpenTelemetry Java span stacktrace capture module"
6+
7+
dependencies {
8+
api("io.opentelemetry:opentelemetry-sdk")
9+
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
10+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.stacktrace;
7+
8+
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.trace.ReadableSpan;
12+
import io.opentelemetry.sdk.trace.SpanProcessor;
13+
import java.io.PrintWriter;
14+
import java.io.StringWriter;
15+
import java.util.function.Predicate;
16+
import java.util.logging.Level;
17+
import java.util.logging.Logger;
18+
19+
public class StackTraceSpanProcessor extends AbstractSimpleChainingSpanProcessor {
20+
21+
// TODO : remove this once semconv 1.24.0 is available
22+
static final AttributeKey<String> SPAN_STACKTRACE = AttributeKey.stringKey("code.stacktrace");
23+
24+
private static final Logger logger = Logger.getLogger(StackTraceSpanProcessor.class.getName());
25+
26+
private final long minSpanDurationNanos;
27+
28+
private final Predicate<ReadableSpan> filterPredicate;
29+
30+
/**
31+
* @param next next span processor to invoke
32+
* @param minSpanDurationNanos minimum span duration in ns for stacktrace capture
33+
* @param filterPredicate extra filter function to exclude spans if needed
34+
*/
35+
public StackTraceSpanProcessor(
36+
SpanProcessor next, long minSpanDurationNanos, Predicate<ReadableSpan> filterPredicate) {
37+
super(next);
38+
this.minSpanDurationNanos = minSpanDurationNanos;
39+
this.filterPredicate = filterPredicate;
40+
logger.log(
41+
Level.FINE,
42+
"Stack traces will be added to spans with a minimum duration of {0} nanos",
43+
minSpanDurationNanos);
44+
}
45+
46+
@Override
47+
protected boolean requiresStart() {
48+
return false;
49+
}
50+
51+
@Override
52+
protected boolean requiresEnd() {
53+
return true;
54+
}
55+
56+
@Override
57+
protected ReadableSpan doOnEnd(ReadableSpan span) {
58+
if (span.getLatencyNanos() < minSpanDurationNanos) {
59+
return span;
60+
}
61+
if (span.getAttribute(SPAN_STACKTRACE) != null) {
62+
// Span already has a stacktrace, do not override
63+
return span;
64+
}
65+
if (!filterPredicate.test(span)) {
66+
return span;
67+
}
68+
MutableSpan mutableSpan = MutableSpan.makeMutable(span);
69+
70+
String stacktrace = generateSpanEndStacktrace();
71+
mutableSpan.setAttribute(SPAN_STACKTRACE, stacktrace);
72+
return mutableSpan;
73+
}
74+
75+
private static String generateSpanEndStacktrace() {
76+
Throwable exception = new Throwable();
77+
StringWriter stringWriter = new StringWriter();
78+
try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
79+
exception.printStackTrace(printWriter);
80+
}
81+
return removeInternalFrames(stringWriter.toString());
82+
}
83+
84+
private static String removeInternalFrames(String stackTrace) {
85+
String lastInternal = "at io.opentelemetry.sdk.trace.SdkSpan.end";
86+
87+
int idx = stackTrace.lastIndexOf(lastInternal);
88+
if (idx == -1) {
89+
// should usually not happen, this means that the span processor was called from somewhere
90+
// else
91+
return stackTrace;
92+
}
93+
int nextNewLine = stackTrace.indexOf('\n', idx);
94+
if (nextNewLine == -1) {
95+
nextNewLine = stackTrace.length() - 1;
96+
}
97+
return stackTrace.substring(nextNewLine + 1);
98+
}
99+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.stacktrace.internal;
7+
8+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
9+
import io.opentelemetry.context.Context;
10+
import io.opentelemetry.sdk.common.CompletableResultCode;
11+
import io.opentelemetry.sdk.trace.ReadWriteSpan;
12+
import io.opentelemetry.sdk.trace.ReadableSpan;
13+
import io.opentelemetry.sdk.trace.SpanProcessor;
14+
import java.util.Arrays;
15+
16+
/**
17+
* A @{@link SpanProcessor} which in addition to all standard operations is capable of modifying and
18+
* optionally filtering spans in the end-callback.
19+
*
20+
* <p>This is done by chaining processors and registering only the first processor with the SDK.
21+
* Mutations can be performed in {@link #doOnEnd(ReadableSpan)} by wrapping the span in a {@link
22+
* MutableSpan}
23+
*/
24+
public abstract class AbstractSimpleChainingSpanProcessor implements SpanProcessor {
25+
26+
protected final SpanProcessor next;
27+
private final boolean nextRequiresStart;
28+
private final boolean nextRequiresEnd;
29+
30+
/**
31+
* @param next the next processor to be invoked after the one being constructed.
32+
*/
33+
public AbstractSimpleChainingSpanProcessor(SpanProcessor next) {
34+
this.next = next;
35+
nextRequiresStart = next.isStartRequired();
36+
nextRequiresEnd = next.isEndRequired();
37+
}
38+
39+
/**
40+
* Equivalent of {@link SpanProcessor#onStart(Context, ReadWriteSpan)}. The onStart callback of
41+
* the next processor must not be invoked from this method, this is already handled by the
42+
* implementation of {@link #onStart(Context, ReadWriteSpan)}.
43+
*/
44+
protected void doOnStart(Context context, ReadWriteSpan readWriteSpan) {}
45+
46+
/**
47+
* Equivalent of {@link SpanProcessor#onEnd(ReadableSpan)}}.
48+
*
49+
* <p>If this method returns null, the provided span will be dropped and not passed to the next
50+
* processor. If anything non-null is returned, the returned instance is passed to the next
51+
* processor.
52+
*
53+
* <p>So in order to mutate the span, simply use {@link MutableSpan#makeMutable(ReadableSpan)} on
54+
* the provided argument and return the {@link MutableSpan} from this method.
55+
*/
56+
@CanIgnoreReturnValue
57+
protected ReadableSpan doOnEnd(ReadableSpan readableSpan) {
58+
return readableSpan;
59+
}
60+
61+
/**
62+
* Indicates if span processor needs to be called on span start
63+
*
64+
* @return true, if this implementation would like {@link #doOnStart(Context, ReadWriteSpan)} to
65+
* be invoked.
66+
*/
67+
protected boolean requiresStart() {
68+
return true;
69+
}
70+
71+
/**
72+
* Indicates if span processor needs to be called on span end
73+
*
74+
* @return true, if this implementation would like {@link #doOnEnd(ReadableSpan)} to be invoked.
75+
*/
76+
protected boolean requiresEnd() {
77+
return true;
78+
}
79+
80+
protected CompletableResultCode doForceFlush() {
81+
return CompletableResultCode.ofSuccess();
82+
}
83+
84+
protected CompletableResultCode doShutdown() {
85+
return CompletableResultCode.ofSuccess();
86+
}
87+
88+
@Override
89+
public final void onStart(Context context, ReadWriteSpan readWriteSpan) {
90+
try {
91+
if (requiresStart()) {
92+
doOnStart(context, readWriteSpan);
93+
}
94+
} finally {
95+
if (nextRequiresStart) {
96+
next.onStart(context, readWriteSpan);
97+
}
98+
}
99+
}
100+
101+
@Override
102+
public final void onEnd(ReadableSpan readableSpan) {
103+
ReadableSpan mappedTo = readableSpan;
104+
try {
105+
if (requiresEnd()) {
106+
mappedTo = doOnEnd(readableSpan);
107+
}
108+
} finally {
109+
if (mappedTo != null && nextRequiresEnd) {
110+
next.onEnd(mappedTo);
111+
}
112+
}
113+
}
114+
115+
@Override
116+
public final boolean isStartRequired() {
117+
return requiresStart() || nextRequiresStart;
118+
}
119+
120+
@Override
121+
public final boolean isEndRequired() {
122+
return requiresEnd() || nextRequiresEnd;
123+
}
124+
125+
@Override
126+
public final CompletableResultCode shutdown() {
127+
return CompletableResultCode.ofAll(Arrays.asList(doShutdown(), next.shutdown()));
128+
}
129+
130+
@Override
131+
public final CompletableResultCode forceFlush() {
132+
return CompletableResultCode.ofAll(Arrays.asList(doForceFlush(), next.forceFlush()));
133+
}
134+
135+
@Override
136+
public final void close() {
137+
SpanProcessor.super.close();
138+
}
139+
}

0 commit comments

Comments
 (0)