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()),