diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/190_failure_store_redirection.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/190_failure_store_redirection.yml
index 54ce32eb13207..0b3007021cad8 100644
--- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/190_failure_store_redirection.yml
+++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/190_failure_store_redirection.yml
@@ -123,7 +123,7 @@ teardown:
- match: { hits.hits.0._source.document.source.foo: 'bar' }
- match: { hits.hits.0._source.error.type: 'fail_processor_exception' }
- match: { hits.hits.0._source.error.message: 'error_message' }
- - contains: { hits.hits.0._source.error.stack_trace: 'org.elasticsearch.ingest.common.FailProcessorException: error_message' }
+ - contains: { hits.hits.0._source.error.stack_trace: 'error_message' }
- length: { hits.hits.0._source.error.pipeline_trace: 2 }
- match: { hits.hits.0._source.error.pipeline_trace.0: 'parent_failing_pipeline' }
- match: { hits.hits.0._source.error.pipeline_trace.1: 'failing_pipeline' }
@@ -207,7 +207,6 @@ teardown:
- match: { hits.hits.0._source.error.type: 'document_parsing_exception' }
- contains: { hits.hits.0._source.error.message: "failed to parse field [count] of type [long] in document with id " }
- contains: { hits.hits.0._source.error.message: "Preview of field's value: 'invalid value'" }
- - contains: { hits.hits.0._source.error.stack_trace: "org.elasticsearch.index.mapper.DocumentParsingException: " }
- contains: { hits.hits.0._source.error.stack_trace: "failed to parse field [count] of type [long] in document with id" }
- contains: { hits.hits.0._source.error.stack_trace: "Preview of field's value: 'invalid value'" }
diff --git a/server/src/main/java/org/elasticsearch/ExceptionsHelper.java b/server/src/main/java/org/elasticsearch/ExceptionsHelper.java
index b67b59aeee076..3e109fb1600b9 100644
--- a/server/src/main/java/org/elasticsearch/ExceptionsHelper.java
+++ b/server/src/main/java/org/elasticsearch/ExceptionsHelper.java
@@ -97,6 +97,193 @@ public static String stackTrace(Throwable e) {
return stackTraceStringWriter.toString();
}
+ /**
+ * Constructs a limited and compressed stack trace string. Each exception printed as part of the full stack trace will have its printed
+ * stack frames capped at the given trace depth. Stack traces that are longer than the given trace depth will summarize the count of the
+ * remaining frames at the end of the trace. Each stack frame omits the module information and limits the package names to single
+ * characters per part.
+ *
+ * An example result when using a trace depth of 2 and one nested cause:
+ *
+ * o.e.s.GenericException: some generic exception!
+ * at o.e.s.SomeClass.method(SomeClass.java:100)
+ * at o.e.s.SomeOtherClass.earlierMethod(SomeOtherClass.java:24)
+ * ... 5 more
+ * Caused by: o.e.s.GenericException: some other generic exception!
+ * at o.e.s.SomeClass.method(SomeClass.java:115)
+ * at o.e.s.SomeOtherClass.earlierMethod(SomeOtherClass.java:16)
+ * ... 12 more
+ *
+ *
+ * @param e Throwable object to construct a printed stack trace for
+ * @param traceDepth The maximum number of stack trace elements to display per exception referenced
+ * @return A string containing a limited and compressed stack trace.
+ */
+ public static String limitedStackTrace(Throwable e, int traceDepth) {
+ assert traceDepth >= 0 : "Cannot print stacktraces with negative trace depths";
+ StringWriter stackTraceStringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stackTraceStringWriter);
+ printLimitedStackTrace(e, printWriter, traceDepth);
+ return stackTraceStringWriter.toString();
+ }
+
+ /** Caption for labeling causative exception stack traces */
+ private static final String CAUSE_CAPTION = "Caused by: ";
+ /** Caption for labeling suppressed exception stack traces */
+ private static final String SUPPRESSED_CAPTION = "Suppressed: ";
+
+ private static void printLimitedStackTrace(Throwable e, PrintWriter s, int maxLines) {
+ // Guard against malicious overrides of Throwable.equals by
+ // using a Set with identity equality semantics.
+ Set dejaVu = Collections.newSetFromMap(new IdentityHashMap<>());
+ dejaVu.add(e);
+
+ // Print our stack trace
+ s.println(compressExceptionMessage(e));
+ StackTraceElement[] trace = e.getStackTrace();
+ int linesPrinted = 0;
+ for (StackTraceElement traceElement : trace) {
+ if (linesPrinted >= maxLines) {
+ break;
+ } else {
+ s.println(compressStackTraceElement(new StringBuilder("\tat "), traceElement));
+ linesPrinted++;
+ }
+ }
+ if (trace.length > linesPrinted) {
+ s.println("\t... " + (trace.length - linesPrinted) + " more");
+ }
+
+ // Print suppressed exceptions, if any
+ for (Throwable se : e.getSuppressed()) {
+ limitAndPrintEnclosedStackTrace(se, s, trace, SUPPRESSED_CAPTION, "\t", maxLines, dejaVu);
+ }
+
+ // Print cause, if any
+ Throwable ourCause = e.getCause();
+ if (ourCause != null) {
+ limitAndPrintEnclosedStackTrace(ourCause, s, trace, CAUSE_CAPTION, "", maxLines, dejaVu);
+ }
+ }
+
+ private static void limitAndPrintEnclosedStackTrace(
+ Throwable e,
+ PrintWriter s,
+ StackTraceElement[] enclosingTrace,
+ String caption,
+ String prefix,
+ int maxLines,
+ Set dejaVu
+ ) {
+ if (dejaVu.contains(e)) {
+ s.println(prefix + caption + "[CIRCULAR REFERENCE: " + compressExceptionMessage(e) + "]");
+ } else {
+ dejaVu.add(e);
+ // Compute number of frames in common between this and enclosing trace
+ StackTraceElement[] trace = e.getStackTrace();
+ int m = trace.length - 1;
+ int n = enclosingTrace.length - 1;
+ while (m >= 0 && n >= 0 && trace[m].equals(enclosingTrace[n])) {
+ m--;
+ n--;
+ }
+ int framesInCommon = trace.length - 1 - m;
+
+ // Instead of breaking out of the print loop below when it reaches the maximum
+ // print lines, we simply cap how many frames we plan on printing here.
+ int linesToPrint = m + 1;
+ if (linesToPrint > maxLines) {
+ // The print loop below is "<=" based instead of "<", so subtract
+ // one from the max lines to convert a count value to an array index
+ // value and avoid an off by one error.
+ m = maxLines - 1;
+ framesInCommon = trace.length - 1 - m;
+ }
+
+ // Print our stack trace
+ s.println(prefix + caption + compressExceptionMessage(e));
+ for (int i = 0; i <= m; i++) {
+ s.println(compressStackTraceElement(new StringBuilder(prefix).append("\tat "), trace[i]));
+ }
+ if (framesInCommon != 0) {
+ s.println(prefix + "\t... " + framesInCommon + " more");
+ }
+
+ // Print suppressed exceptions, if any
+ for (Throwable se : e.getSuppressed()) {
+ limitAndPrintEnclosedStackTrace(se, s, trace, SUPPRESSED_CAPTION, prefix + "\t", maxLines, dejaVu);
+ }
+
+ // Print cause, if any
+ Throwable ourCause = e.getCause();
+ if (ourCause != null) {
+ limitAndPrintEnclosedStackTrace(ourCause, s, trace, CAUSE_CAPTION, prefix, maxLines, dejaVu);
+ }
+ }
+ }
+
+ private static String compressExceptionMessage(Throwable e) {
+ StringBuilder msg = new StringBuilder();
+ compressPackages(msg, e.getClass().getName());
+ String message = e.getLocalizedMessage();
+ if (message != null) {
+ msg.append(": ").append(message);
+ }
+ return msg.toString();
+ }
+
+ private static StringBuilder compressStackTraceElement(StringBuilder s, final StackTraceElement stackTraceElement) {
+ String declaringClass = stackTraceElement.getClassName();
+ compressPackages(s, declaringClass);
+
+ String methodName = stackTraceElement.getMethodName();
+ s.append(".").append(methodName).append("(");
+
+ if (stackTraceElement.isNativeMethod()) {
+ s.append("Native Method)");
+ } else {
+ String fileName = stackTraceElement.getFileName();
+ int lineNumber = stackTraceElement.getLineNumber();
+ if (fileName != null && lineNumber >= 0) {
+ s.append(fileName).append(":").append(lineNumber).append(")");
+ } else if (fileName != null) {
+ s.append(fileName).append(")");
+ } else {
+ s.append("Unknown Source)");
+ }
+ }
+ return s;
+ }
+
+ // Visible for testing
+ static void compressPackages(StringBuilder s, String className) {
+ assert s != null : "s cannot be null";
+ assert className != null : "className cannot be null";
+ int finalDot = className.lastIndexOf('.');
+ if (finalDot < 0) {
+ s.append(className);
+ return;
+ }
+ int lastPackageName = className.lastIndexOf('.', finalDot - 1);
+ if (lastPackageName < 0) {
+ if (finalDot >= 1) {
+ s.append(className.charAt(0)).append('.');
+ }
+ s.append(className.substring(finalDot + 1));
+ return;
+ }
+ boolean firstChar = true;
+ char[] charArray = className.toCharArray();
+ for (int idx = 0; idx <= lastPackageName + 1; idx++) {
+ char c = charArray[idx];
+ if (firstChar && '.' != c) {
+ s.append(c).append('.');
+ }
+ firstChar = '.' == c;
+ }
+ s.append(className.substring(finalDot + 1));
+ }
+
public static String formatStackTrace(final StackTraceElement[] stackTrace) {
return Arrays.stream(stackTrace).skip(1).map(e -> "\tat " + e).collect(Collectors.joining("\n"));
}
diff --git a/server/src/main/java/org/elasticsearch/action/bulk/FailureStoreDocumentConverter.java b/server/src/main/java/org/elasticsearch/action/bulk/FailureStoreDocumentConverter.java
index cc9f9b8ee1ce7..962e844529125 100644
--- a/server/src/main/java/org/elasticsearch/action/bulk/FailureStoreDocumentConverter.java
+++ b/server/src/main/java/org/elasticsearch/action/bulk/FailureStoreDocumentConverter.java
@@ -32,6 +32,8 @@
*/
public class FailureStoreDocumentConverter {
+ private static final int STACKTRACE_PRINT_DEPTH = 2;
+
private static final Set INGEST_EXCEPTION_HEADERS = Set.of(
PIPELINE_ORIGIN_EXCEPTION_HEADER,
PROCESSOR_TAG_EXCEPTION_HEADER,
@@ -109,7 +111,7 @@ private static XContentBuilder createSource(
{
builder.field("type", ElasticsearchException.getExceptionName(unwrapped));
builder.field("message", unwrapped.getMessage());
- builder.field("stack_trace", ExceptionsHelper.stackTrace(unwrapped));
+ builder.field("stack_trace", ExceptionsHelper.limitedStackTrace(unwrapped, STACKTRACE_PRINT_DEPTH));
// Try to find the IngestProcessorException somewhere in the stack trace. Since IngestProcessorException is package-private,
// we can't instantiate it in tests, so we'll have to check for the headers directly.
var ingestException = ExceptionsHelper.unwrapCausesAndSuppressed(
diff --git a/server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java b/server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java
index 744c0fafedb46..8f3d84b5e90da 100644
--- a/server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java
+++ b/server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java
@@ -221,4 +221,276 @@ public void testCauseCycle() {
ExceptionsHelper.unwrap(e1, IOException.class);
ExceptionsHelper.unwrapCorruption(e1);
}
+
+ public void testLimitedStackTrace() {
+ // A normal exception is thrown several stack frames down
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ int expectedLength = 1 + maxTraces + 1; // Exception message, traces, then the count of remaining traces
+ assertThat(limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceShortened() {
+ // An exception has a smaller trace than is requested
+ // Set max traces very, very high, since this test is sensitive to the number of method calls on the thread's stack.
+ int maxTraces = 5000;
+ RuntimeException exception = new RuntimeException("Regular Exception");
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ String fullTrace = ExceptionsHelper.stackTrace(exception);
+ int expectedLength = fullTrace.split("\n").length; // The resulting line count should not be reduced
+ assertThat(limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceWrappedExceptions() {
+ // An exception is thrown and is then wrapped several stack frames later
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(
+ randomIntBetween(10, 15),
+ () -> throwExceptionCausedBy(recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException))
+ );
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ // (1) Exception message, (n) traces, (1) remaining traces, (1) caused by, (n) caused by traces, (1) remaining traces
+ int expectedLength = 4 + (2 * maxTraces);
+ assertThat(limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceSuppressingAnException() {
+ // A normal exception is thrown several stack frames down and then suppresses a new exception on the way back up
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(randomIntBetween(10, 15), () -> {
+ RuntimeException original = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ recurseUntil(randomIntBetween(10, 15), () -> suppressNewExceptionUnder(original));
+ });
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ // (1) Exception message, (n) traces, (1) remaining traces, then
+ // (1) suppressed, (n) suppressed by traces, (1) remaining lines
+ int expectedLength = 4 + (2 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceSuppressedByAnException() {
+ // A normal exception is thrown several stack frames down and then gets suppressed on the way back up by a new exception
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(randomIntBetween(10, 15), () -> {
+ RuntimeException original = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ recurseUntil(randomIntBetween(10, 15), () -> throwNewExceptionThatSuppresses(original));
+ });
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ // (1) Exception message, (n) traces, (1) remaining traces, then
+ // (1) suppressed original exception, (n) suppressed traces, (1) remaining traces
+ int expectedLength = 4 + (2 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceSuppressingAnExceptionWithACause() {
+ // A normal exception is thrown several stack frames down. On the way back up, a new exception with a nested cause is
+ // suppressed by it.
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(randomIntBetween(10, 15), () -> {
+ RuntimeException original = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ RuntimeException causedBy = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ recurseUntil(randomIntBetween(10, 15), () -> suppressNewExceptionWithCauseUnder(original, causedBy));
+ });
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ // (1) Exception message, (n) traces, (1) remaining traces, then
+ // (1) suppressed exception, (n) suppressed traces, (1) remaining traces
+ // (1) suppressed caused by exception, (n) traces, (1) remaining traces
+ int expectedLength = 6 + (3 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceSuppressedByAnExceptionWithACause() {
+ // A normal exception is thrown several stack frames down. On the way back up, a new exception with a nested cause
+ // suppresses it.
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(randomIntBetween(10, 15), () -> {
+ RuntimeException original = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ RuntimeException causedBy = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ recurseUntil(randomIntBetween(10, 15), () -> throwNewExceptionWithCauseThatSuppresses(original, causedBy));
+ });
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ // (1) Exception message, (n) traces, (1) remaining traces, then
+ // (1) suppressed original exception, (n) traces, (1) remaining traces, then
+ // (1) caused by exception, (n) traces, (1) remaining traces
+ int expectedLength = 6 + (3 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceWrappedAndSuppressingAWrappedException() {
+ // A normal exception is thrown several stack frames down. It gets wrapped on the way back up.
+ // Some "recovery" code runs and a new exception is thrown. It also gets wrapped on the way back up.
+ // The first chain of exceptions suppresses the second.
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(randomIntBetween(10, 15), () -> {
+ RuntimeException original = recurseAndCatchRuntime(
+ randomIntBetween(10, 15),
+ () -> throwExceptionCausedBy(recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException))
+ );
+ RuntimeException causedBy = recurseAndCatchRuntime(
+ randomIntBetween(10, 15),
+ () -> throwExceptionCausedBy(recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException))
+ );
+ recurseUntil(randomIntBetween(10, 15), () -> suppressNewExceptionWithCauseUnder(original, causedBy));
+ });
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ // (1) wrapped exception message, (n) traces, (1) remaining traces, then
+ // (1) suppressed exception, (n) suppressed traces, (1) remaining traces, then
+ // (1) wrapped exception under suppressed exception, (n) traces, (1) remaining traces, then
+ // (1) root cause of suppressed exception chain, (n) traces, (1) remaining traces, then
+ // (1) root cause of the suppressing exception chain, (n) traces, (1) remaining traces
+ int expectedLength = 10 + (5 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceWrappedAndSuppressedByAWrappedException() {
+ // A normal exception is thrown several stack frames down. It gets wrapped on the way back up.
+ // Some "recovery" code runs and a new exception is thrown. It also gets wrapped on the way back up.
+ // The first chain of exceptions suppresses the second.
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(randomIntBetween(10, 15), () -> {
+ RuntimeException original = recurseAndCatchRuntime(
+ randomIntBetween(10, 15),
+ () -> throwExceptionCausedBy(recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException))
+ );
+ RuntimeException causedBy = recurseAndCatchRuntime(
+ randomIntBetween(10, 15),
+ () -> throwExceptionCausedBy(recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException))
+ );
+ throwNewExceptionWithCauseThatSuppresses(original, causedBy);
+ });
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ // (1) wrapped exception message, (n) traces, (1) remaining traces, then
+ // (1) suppressed wrapped exception, (n) traces, (1) remaining traces, then
+ // (1) root cause of suppressed exception chain, (n) traces, (1) remaining traces, then
+ // (1) wrapped exception, (n) traces, (1) remaining traces, then
+ // (1) root cause of exception chain, (n) traces, (1) remaining traces
+ int expectedLength = 10 + (5 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceNestedSuppression() {
+ // The original exception is thrown and is then repeatedly suppressed by new exceptions
+ int maxTraces = between(0, 5);
+ RuntimeException exception = recurseAndCatchRuntime(
+ randomIntBetween(10, 15),
+ () -> throwNewExceptionWithCauseThatSuppresses(
+ recurseAndCatchRuntime(
+ randomIntBetween(10, 15),
+ () -> throwNewExceptionWithCauseThatSuppresses(
+ recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException),
+ recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException)
+ )
+ ),
+ recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException)
+ )
+ );
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception, maxTraces);
+ // (1) first suppressing exception message, (n) traces, (1) remaining traces, then
+ // (1) second suppressing exception message, (n) traces, (1) remaining traces, then
+ // (1) suppressed original exception message, (n) traces, (1) remaining traces, then
+ // (1) cause of second suppressed exception, (n) traces, (1) remaining traces, then
+ // (1) cause of first suppressed exception, (n) traces, (1) remaining traces, then
+ int expectedLength = 10 + (5 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceCircularCause() {
+ // An exception is thrown and then suppresses itself
+ int maxTraces = between(0, 5);
+ RuntimeException exception1 = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ RuntimeException exception2 = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ exception1.initCause(exception2);
+ exception2.initCause(exception1);
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception1, maxTraces);
+ // (1) first exception message, (n) traces, (1) remaining traces, then
+ // (1) caused by second exception, (n) traces, (1) remaining traces, then
+ // (1) caused by first exception message again, but no further traces
+ int expectedLength = 5 + (2 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ public void testLimitedStackTraceCircularSuppression() {
+ // An exception is thrown and then suppresses itself
+ int maxTraces = between(0, 5);
+ RuntimeException exception1 = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ RuntimeException exception2 = recurseAndCatchRuntime(randomIntBetween(10, 15), ExceptionsHelperTests::throwRegularException);
+ exception1.addSuppressed(exception2);
+ exception2.addSuppressed(exception1);
+ String limitedTrace = ExceptionsHelper.limitedStackTrace(exception1, maxTraces);
+ // (1) first exception message, (n) traces, (1) remaining traces, then
+ // (1) suppressed second exception, (n) traces, (1) remaining traces, then
+ // (1) suppressed first exception message again, but no further traces
+ int expectedLength = 5 + (2 * maxTraces);
+ assertThat(limitedTrace, limitedTrace.split("\n").length, equalTo(expectedLength));
+ }
+
+ private static void throwRegularException() {
+ throw new RuntimeException("Regular Exception");
+ }
+
+ private static void throwExceptionCausedBy(RuntimeException causedBy) {
+ throw new RuntimeException("Wrapping Exception", causedBy);
+ }
+
+ private static void suppressNewExceptionUnder(RuntimeException suppressor) {
+ suppressor.addSuppressed(new RuntimeException("Suppressed Exception"));
+ throw suppressor;
+ }
+
+ private static void throwNewExceptionThatSuppresses(RuntimeException suppressed) {
+ RuntimeException priority = new RuntimeException("Priority Exception");
+ priority.addSuppressed(suppressed);
+ throw priority;
+ }
+
+ private static void suppressNewExceptionWithCauseUnder(RuntimeException suppressor, RuntimeException suppressedCause) {
+ suppressor.addSuppressed(new RuntimeException("Suppressed Exception", suppressedCause));
+ throw suppressor;
+ }
+
+ private static void throwNewExceptionWithCauseThatSuppresses(RuntimeException suppressed, RuntimeException suppressorCause) {
+ RuntimeException priority = new RuntimeException("Priority Exception", suppressorCause);
+ priority.addSuppressed(suppressed);
+ throw priority;
+ };
+
+ private static RuntimeException recurseAndCatchRuntime(int depth, Runnable op) {
+ return expectThrows(RuntimeException.class, () -> doRecurse(depth, 0, op));
+ }
+
+ private static void recurseUntil(int depth, Runnable op) {
+ doRecurse(depth, 0, op);
+ }
+
+ private static void doRecurse(int depth, int current, Runnable op) {
+ if (depth == current) {
+ op.run();
+ } else {
+ doRecurse(depth, current + 1, op);
+ }
+ }
+
+ public void testCompressStackTraceElement() {
+ assertThat(compressPackages(""), equalTo(""));
+ assertThat(compressPackages("."), equalTo(""));
+ assertThat(compressPackages("ClassName"), equalTo("ClassName"));
+ assertThat(compressPackages("alfa.ClassName"), equalTo("a.ClassName"));
+ assertThat(compressPackages("alfa.bravo.ClassName"), equalTo("a.b.ClassName"));
+ assertThat(compressPackages(".ClassName"), equalTo("ClassName"));
+ assertThat(compressPackages(".alfa.ClassName"), equalTo("a.ClassName"));
+ assertThat(compressPackages(".alfa.bravo.ClassName"), equalTo("a.b.ClassName"));
+ assertThat(compressPackages("...alfa.....ClassName"), equalTo("a.ClassName"));
+ assertThat(compressPackages("...alfa....bravo.ClassName"), equalTo("a.b.ClassName"));
+ assertThat(compressPackages("...alfa....bravo.charliepackagenameisreallyreallylong.ClassName"), equalTo("a.b.c.ClassName"));
+ assertThat(compressPackages("alfa.bravo.charlie.OuterClassName.InnerClassName"), equalTo("a.b.c.O.InnerClassName"));
+
+ expectThrows(AssertionError.class, () -> compressPackages(null));
+ }
+
+ private static String compressPackages(String className) {
+ StringBuilder s = new StringBuilder();
+ ExceptionsHelper.compressPackages(s, className);
+ return s.toString();
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/action/bulk/FailureStoreDocumentConverterTests.java b/server/src/test/java/org/elasticsearch/action/bulk/FailureStoreDocumentConverterTests.java
index 85cedff9145be..c03d5e16b287b 100644
--- a/server/src/test/java/org/elasticsearch/action/bulk/FailureStoreDocumentConverterTests.java
+++ b/server/src/test/java/org/elasticsearch/action/bulk/FailureStoreDocumentConverterTests.java
@@ -90,11 +90,11 @@ public void testFailureStoreDocumentConversion() throws Exception {
assertThat(ObjectPath.eval("error.message", convertedRequest.sourceAsMap()), is(equalTo("Test exception please ignore")));
assertThat(
ObjectPath.eval("error.stack_trace", convertedRequest.sourceAsMap()),
- startsWith("org.elasticsearch.ElasticsearchException: Test exception please ignore")
+ startsWith("o.e.ElasticsearchException: Test exception please ignore")
);
assertThat(
ObjectPath.eval("error.stack_trace", convertedRequest.sourceAsMap()),
- containsString("at org.elasticsearch.action.bulk.FailureStoreDocumentConverterTests.testFailureStoreDocumentConversion")
+ containsString("at o.e.a.b.FailureStoreDocumentConverterTests.testFailureStoreDocumentConversion")
);
assertThat(
ObjectPath.eval("error.pipeline_trace", convertedRequest.sourceAsMap()),