diff --git a/dd-trace-core/src/main/java/datadog/trace/core/util/StackTraces.java b/dd-trace-core/src/main/java/datadog/trace/core/util/StackTraces.java index c39d3fba63b..47299f034f6 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/util/StackTraces.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/util/StackTraces.java @@ -32,7 +32,7 @@ public static String safeGetMessage(Throwable t) { } try { return t.getMessage(); - } catch (Exception e) { + } catch (Exception | StackOverflowError e) { return "(Exception message unavailable for " + t.getClass().getSimpleName() + ": getMessage() threw " @@ -60,8 +60,9 @@ public static String getStackTrace(Throwable t, int maxChars) { StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); trace = sw.toString(); - } catch (Exception ignored) { - // printStackTrace() failed (e.g. getMessage() throws inside toString()). + } catch (Exception | StackOverflowError ignored) { + // printStackTrace() failed (e.g. getMessage() throws inside toString(), or a + // StackOverflowError while formatting an already stack-constrained throwable). // Reconstruct from getStackTrace() so the call site is still locatable. try { trace = @@ -70,14 +71,19 @@ public static String getStackTrace(Throwable t, int maxChars) { + Arrays.stream(t.getStackTrace()) .map(f -> "\tat " + f) .collect(Collectors.joining(System.lineSeparator())); - } catch (Exception ignored2) { - trace = t.getClass().getName(); + } catch (Exception | StackOverflowError ignored2) { + try { + trace = t.getClass().getName() + ": " + t.getMessage(); + } catch (Exception | StackOverflowError ignored3) { + trace = t.getClass().getName(); + } } } try { return truncate(trace, maxChars); - } catch (Exception e) { - // If something goes wrong, return the original trace + } catch (Exception | StackOverflowError e) { + // If something goes wrong (including a further StackOverflowError while the stack is + // still constrained), return the untruncated trace rather than propagate. return trace; } } diff --git a/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java b/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java index ba4bc51d4f2..d7d9b045735 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java @@ -83,6 +83,29 @@ void safeGetMessageReturnsNullForNullInput() { assertNull(StackTraces.safeGetMessage(null)); } + @Test + void getStackTraceFallsBackWhenPrintStackTraceThrowsStackOverflowError() { + String trace = + StackTraces.getStackTrace(TestThrowables.throwingStackOverflowOnPrintStackTrace(), 1000); + assertTrue(trace.contains("TestThrowables"), "must fall back to a locatable trace"); + } + + @Test + void getStackTraceFallsBackToClassNameAndMessageWhenEverythingElseThrows() { + String trace = + StackTraces.getStackTrace( + TestThrowables.throwingStackOverflowEverywhereExceptGetMessage(), 1000); + assertTrue(trace.contains("TestThrowables"), "must include the throwable's class name"); + assertTrue(trace.contains("still readable"), "must include the throwable's message"); + } + + @Test + void getStackTraceFallsBackToClassNameWhenGetMessageAlsoThrows() { + String trace = + StackTraces.getStackTrace(TestThrowables.throwingStackOverflowEverywhere(), 1000); + assertEquals(trace, TestThrowables.throwingStackOverflowEverywhere().getClass().getName()); + } + // --- getStackTrace with broken getMessage --- @ParameterizedTest(name = "truncation limit {0}") diff --git a/dd-trace-core/src/test/java/datadog/trace/core/util/TestThrowables.java b/dd-trace-core/src/test/java/datadog/trace/core/util/TestThrowables.java index 42885a4af45..c18ac746940 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/util/TestThrowables.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/util/TestThrowables.java @@ -20,4 +20,64 @@ public String getMessage() { } }; } + + /** + * Returns a {@link RuntimeException} whose {@link Throwable#printStackTrace(java.io.PrintWriter)} + * throws a {@link StackOverflowError} — simulating a second overflow while formatting a throwable + * that was itself caught with little remaining stack margin. + */ + public static RuntimeException throwingStackOverflowOnPrintStackTrace() { + return new RuntimeException() { + @Override + public void printStackTrace(java.io.PrintWriter s) { + throw new StackOverflowError(); + } + }; + } + + /** + * Returns a {@link RuntimeException} whose {@link Throwable#printStackTrace(java.io.PrintWriter)} + * and {@link Throwable#getStackTrace()} both throw {@link StackOverflowError} — simulating a + * throwable caught with essentially no remaining stack margin, where even the array-based + * fallback in {@link StackTraces#getStackTrace} fails and only {@link Throwable#getMessage()} + * remains callable. + */ + public static RuntimeException throwingStackOverflowEverywhereExceptGetMessage() { + return new RuntimeException("still readable") { + @Override + public void printStackTrace(java.io.PrintWriter s) { + throw new StackOverflowError(); + } + + @Override + public StackTraceElement[] getStackTrace() { + throw new StackOverflowError(); + } + }; + } + + /** + * Returns a {@link RuntimeException} whose {@link + * Throwable#printStackTrace(java.io.PrintWriter)}, {@link Throwable#getStackTrace()}, and {@link + * Throwable#getMessage()} all throw {@link StackOverflowError} — the worst case, where {@link + * StackTraces#getStackTrace} must fall back to just the throwable's class name. + */ + public static RuntimeException throwingStackOverflowEverywhere() { + return new RuntimeException() { + @Override + public void printStackTrace(java.io.PrintWriter s) { + throw new StackOverflowError(); + } + + @Override + public StackTraceElement[] getStackTrace() { + throw new StackOverflowError(); + } + + @Override + public String getMessage() { + throw new StackOverflowError(); + } + }; + } }