Skip to content

Commit 4821c37

Browse files
authored
Add custom stacktrace renderer which is length limit aware (#7281)
1 parent cecfb83 commit 4821c37

File tree

12 files changed

+501
-29
lines changed

12 files changed

+501
-29
lines changed

sdk/common/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ plugins {
44
id("otel.java-conventions")
55
id("otel.publish-conventions")
66
id("otel.animalsniffer-conventions")
7+
id("otel.jmh-conventions")
78
}
89
apply<OtelVersionClassPlugin>()
910

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.internal;
7+
8+
import java.io.PrintStream;
9+
import java.io.PrintWriter;
10+
import java.io.StringWriter;
11+
import java.util.concurrent.TimeUnit;
12+
import java.util.function.BiFunction;
13+
import org.openjdk.jmh.annotations.Benchmark;
14+
import org.openjdk.jmh.annotations.BenchmarkMode;
15+
import org.openjdk.jmh.annotations.Fork;
16+
import org.openjdk.jmh.annotations.Measurement;
17+
import org.openjdk.jmh.annotations.Mode;
18+
import org.openjdk.jmh.annotations.OutputTimeUnit;
19+
import org.openjdk.jmh.annotations.Param;
20+
import org.openjdk.jmh.annotations.Scope;
21+
import org.openjdk.jmh.annotations.State;
22+
import org.openjdk.jmh.annotations.Threads;
23+
import org.openjdk.jmh.annotations.Warmup;
24+
25+
/**
26+
* This benchmark compares the performance of {@link StackTraceRenderer}, the custom length limit
27+
* aware exception render, to the built-in JDK stacktrace renderer {@link
28+
* Throwable#printStackTrace(PrintStream)}.
29+
*/
30+
@BenchmarkMode({Mode.AverageTime})
31+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
32+
@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
33+
@Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
34+
@Fork(1)
35+
@SuppressWarnings("StaticAssignmentOfThrowable")
36+
public class StacktraceRenderBenchmark {
37+
38+
private static final Exception simple = new Exception("error");
39+
private static final Exception complex =
40+
new Exception("error", new Exception("cause1", new Exception("cause2")));
41+
42+
static {
43+
complex.addSuppressed(new Exception("suppressed1"));
44+
complex.addSuppressed(new Exception("suppressed2", new Exception("cause")));
45+
}
46+
47+
@State(Scope.Benchmark)
48+
public static class BenchmarkState {
49+
50+
@Param Renderer renderer;
51+
@Param ExceptionParam exceptionParam;
52+
53+
@Param({"10", "1000", "100000"})
54+
int lengthLimit;
55+
}
56+
57+
@SuppressWarnings("ImmutableEnumChecker")
58+
public enum Renderer {
59+
JDK(
60+
(throwable, limit) -> {
61+
StringWriter stringWriter = new StringWriter();
62+
try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
63+
throwable.printStackTrace(printWriter);
64+
}
65+
String stacktrace = stringWriter.toString();
66+
return stacktrace.substring(0, Math.min(stacktrace.length(), limit));
67+
}),
68+
CUSTOM((throwable, limit) -> new StackTraceRenderer(throwable, limit).render());
69+
70+
private final BiFunction<Throwable, Integer, String> renderer;
71+
72+
Renderer(BiFunction<Throwable, Integer, String> renderer) {
73+
this.renderer = renderer;
74+
}
75+
76+
BiFunction<Throwable, Integer, String> renderer() {
77+
return renderer;
78+
}
79+
}
80+
81+
@SuppressWarnings("ImmutableEnumChecker")
82+
public enum ExceptionParam {
83+
SIMPLE(simple),
84+
COMPLEX(complex);
85+
86+
private final Throwable throwable;
87+
88+
ExceptionParam(Throwable throwable) {
89+
this.throwable = throwable;
90+
}
91+
92+
Throwable throwable() {
93+
return throwable;
94+
}
95+
}
96+
97+
@Benchmark
98+
@Threads(1)
99+
@SuppressWarnings("ReturnValueIgnored")
100+
public void render(BenchmarkState benchmarkState) {
101+
BiFunction<Throwable, Integer, String> renderer = benchmarkState.renderer.renderer();
102+
Throwable throwable = benchmarkState.exceptionParam.throwable();
103+
int limit = benchmarkState.lengthLimit;
104+
105+
renderer.apply(throwable, limit);
106+
}
107+
}

sdk/common/src/main/java/io/opentelemetry/sdk/internal/DefaultExceptionAttributeResolver.java

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@
99
import java.io.StringWriter;
1010

1111
/**
12-
* This class is internal and experimental. Its APIs are unstable and can change at any time. Its
12+
* The default {@link ExceptionAttributeResolver}, populating standard {@code exception.*} as
13+
* defined by the semantic conventions.
14+
*
15+
* <p>This class is internal and experimental. Its APIs are unstable and can change at any time. Its
1316
* APIs (or a version of them) may be promoted to the public stable API in the future, but no
1417
* guarantees are made.
18+
*
19+
* @see ExceptionAttributeResolver#getDefault()
20+
* @see ExceptionAttributeResolver#getDefault(boolean) ()
1521
*/
16-
public final class DefaultExceptionAttributeResolver implements ExceptionAttributeResolver {
22+
final class DefaultExceptionAttributeResolver implements ExceptionAttributeResolver {
1723

18-
private static final DefaultExceptionAttributeResolver INSTANCE =
19-
new DefaultExceptionAttributeResolver();
24+
static final String ENABLE_JVM_STACKTRACE_PROPERTY = "otel.experimental.sdk.jvm_stacktrace";
2025

21-
private DefaultExceptionAttributeResolver() {}
26+
private final boolean jvmStacktraceEnabled;
2227

23-
public static ExceptionAttributeResolver getInstance() {
24-
return INSTANCE;
28+
DefaultExceptionAttributeResolver(boolean jvmStacktraceEnabled) {
29+
this.jvmStacktraceEnabled = jvmStacktraceEnabled;
2530
}
2631

2732
@Override
@@ -37,14 +42,23 @@ public void setExceptionAttributes(
3742
attributeSetter.setAttribute(ExceptionAttributeResolver.EXCEPTION_MESSAGE, exceptionMessage);
3843
}
3944

45+
String exceptionStacktrace =
46+
jvmStacktraceEnabled
47+
? jvmStacktrace(throwable)
48+
: limitsAwareStacktrace(throwable, maxAttributeLength);
49+
attributeSetter.setAttribute(
50+
ExceptionAttributeResolver.EXCEPTION_STACKTRACE, exceptionStacktrace);
51+
}
52+
53+
private static String jvmStacktrace(Throwable throwable) {
4054
StringWriter stringWriter = new StringWriter();
4155
try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
4256
throwable.printStackTrace(printWriter);
4357
}
44-
String exceptionStacktrace = stringWriter.toString();
45-
if (exceptionStacktrace != null) {
46-
attributeSetter.setAttribute(
47-
ExceptionAttributeResolver.EXCEPTION_STACKTRACE, exceptionStacktrace);
48-
}
58+
return stringWriter.toString();
59+
}
60+
61+
private static String limitsAwareStacktrace(Throwable throwable, int maxAttributeLength) {
62+
return new StackTraceRenderer(throwable, maxAttributeLength).render();
4963
}
5064
}

sdk/common/src/main/java/io/opentelemetry/sdk/internal/ExceptionAttributeResolver.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
package io.opentelemetry.sdk.internal;
77

8+
import static io.opentelemetry.sdk.internal.DefaultExceptionAttributeResolver.ENABLE_JVM_STACKTRACE_PROPERTY;
9+
810
import io.opentelemetry.api.common.AttributeKey;
11+
import io.opentelemetry.api.internal.ConfigUtil;
912
import javax.annotation.Nullable;
1013

1114
/**
@@ -24,6 +27,26 @@ public interface ExceptionAttributeResolver {
2427
void setExceptionAttributes(
2528
AttributeSetter attributeSetter, Throwable throwable, int maxAttributeLength);
2629

30+
/**
31+
* Return the default exception attribute resolver, setting {@code jvmStacktraceEnabled} based on
32+
* {@link DefaultExceptionAttributeResolver#ENABLE_JVM_STACKTRACE_PROPERTY}.
33+
*/
34+
static ExceptionAttributeResolver getDefault() {
35+
return getDefault(
36+
Boolean.parseBoolean(ConfigUtil.getString(ENABLE_JVM_STACKTRACE_PROPERTY, "false")));
37+
}
38+
39+
/**
40+
* Return the default exception attribute resolver.
41+
*
42+
* @param jvmStacktraceEnabled if true, resolve stacktrace using the stacktrace renderer built
43+
* into the JVM. This built in JVM renderer is not attribute limits aware, and may utilize
44+
* more CPU / memory than is needed. Most users will prefer to set this to {@code false}.
45+
*/
46+
static ExceptionAttributeResolver getDefault(boolean jvmStacktraceEnabled) {
47+
return new DefaultExceptionAttributeResolver(jvmStacktraceEnabled);
48+
}
49+
2750
/**
2851
* This class is internal and experimental. Its APIs are unstable and can change at any time. Its
2952
* APIs (or a version of them) may be promoted to the public stable API in the future, but no
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.internal;
7+
8+
import java.io.PrintStream;
9+
import java.util.Collections;
10+
import java.util.IdentityHashMap;
11+
import java.util.Set;
12+
13+
/**
14+
* An alternative to exception stacktrace renderer that replicates the behavior of {@link
15+
* Throwable#printStackTrace(PrintStream)}, but which is aware of a maximum stacktrace length limit,
16+
* and exits early when the length limit has been exceeded to avoid unnecessary computation.
17+
*
18+
* <p>Instances should only be used once.
19+
*/
20+
class StackTraceRenderer {
21+
22+
private static final String CAUSED_BY = "Caused by: ";
23+
private static final String SUPPRESSED = "Suppressed: ";
24+
25+
private final Throwable throwable;
26+
private final int lengthLimit;
27+
private final StringBuilder builder = new StringBuilder();
28+
29+
StackTraceRenderer(Throwable throwable, int lengthLimit) {
30+
this.throwable = throwable;
31+
this.lengthLimit = lengthLimit;
32+
}
33+
34+
String render() {
35+
if (builder.length() == 0) {
36+
appendStackTrace();
37+
}
38+
39+
return builder.substring(0, Math.min(builder.length(), lengthLimit));
40+
}
41+
42+
private void appendStackTrace() {
43+
builder.append(throwable).append(System.lineSeparator());
44+
if (isOverLimit()) {
45+
return;
46+
}
47+
48+
StackTraceElement[] stackTraceElements = throwable.getStackTrace();
49+
for (StackTraceElement stackTraceElement : stackTraceElements) {
50+
builder.append("\tat ").append(stackTraceElement).append(System.lineSeparator());
51+
if (isOverLimit()) {
52+
return;
53+
}
54+
}
55+
56+
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
57+
seen.add(throwable);
58+
59+
for (Throwable suppressed : throwable.getSuppressed()) {
60+
appendInnerStacktrace(stackTraceElements, suppressed, "\t", SUPPRESSED, seen);
61+
}
62+
63+
Throwable cause = throwable.getCause();
64+
if (cause != null) {
65+
appendInnerStacktrace(stackTraceElements, cause, "", CAUSED_BY, seen);
66+
}
67+
}
68+
69+
/**
70+
* Append the {@code innerThrowable} to the {@link #builder}, returning {@code true} if the
71+
* builder now exceeds the length limit.
72+
*/
73+
private boolean appendInnerStacktrace(
74+
StackTraceElement[] parentElements,
75+
Throwable innerThrowable,
76+
String prefix,
77+
String caption,
78+
Set<Throwable> seen) {
79+
if (seen.contains(innerThrowable)) {
80+
builder
81+
.append(prefix)
82+
.append(caption)
83+
.append("[CIRCULAR REFERENCE: ")
84+
.append(innerThrowable)
85+
.append("]")
86+
.append(System.lineSeparator());
87+
return true;
88+
}
89+
seen.add(innerThrowable);
90+
91+
// Iterating back to front, compute the lastSharedFrameIndex, which tracks the point at which
92+
// this exception's stacktrace elements start repeating the parent's elements
93+
StackTraceElement[] currentElements = innerThrowable.getStackTrace();
94+
int parentIndex = parentElements.length - 1;
95+
int lastSharedFrameIndex = currentElements.length - 1;
96+
while (true) {
97+
if (parentIndex < 0 || lastSharedFrameIndex < 0) {
98+
break;
99+
}
100+
if (!parentElements[parentIndex].equals(currentElements[lastSharedFrameIndex])) {
101+
break;
102+
}
103+
parentIndex--;
104+
lastSharedFrameIndex--;
105+
}
106+
107+
builder.append(prefix).append(caption).append(innerThrowable).append(System.lineSeparator());
108+
if (isOverLimit()) {
109+
return true;
110+
}
111+
112+
for (int i = 0; i <= lastSharedFrameIndex; i++) {
113+
StackTraceElement stackTraceElement = currentElements[i];
114+
builder
115+
.append(prefix)
116+
.append("\tat ")
117+
.append(stackTraceElement)
118+
.append(System.lineSeparator());
119+
if (isOverLimit()) {
120+
return true;
121+
}
122+
}
123+
124+
int duplicateFrames = currentElements.length - 1 - lastSharedFrameIndex;
125+
if (duplicateFrames != 0) {
126+
builder
127+
.append(prefix)
128+
.append("\t... ")
129+
.append(duplicateFrames)
130+
.append(" more")
131+
.append(System.lineSeparator());
132+
if (isOverLimit()) {
133+
return true;
134+
}
135+
}
136+
137+
for (Throwable suppressed : innerThrowable.getSuppressed()) {
138+
if (appendInnerStacktrace(currentElements, suppressed, prefix + "\t", SUPPRESSED, seen)) {
139+
return true;
140+
}
141+
}
142+
143+
Throwable cause = innerThrowable.getCause();
144+
if (cause != null) {
145+
return appendInnerStacktrace(currentElements, cause, prefix, CAUSED_BY, seen);
146+
}
147+
148+
return false;
149+
}
150+
151+
private boolean isOverLimit() {
152+
return builder.length() >= lengthLimit;
153+
}
154+
}

0 commit comments

Comments
 (0)