diff --git a/docs/changelog/88297.yaml b/docs/changelog/88297.yaml new file mode 100644 index 0000000000000..5a759223df69b --- /dev/null +++ b/docs/changelog/88297.yaml @@ -0,0 +1,5 @@ +pr: 88297 +summary: Print full exception when console is non-interactive +area: Infra/Core +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java b/server/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java index 9f9e404ba4b90..0e565f22ba763 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java @@ -9,6 +9,7 @@ package org.elasticsearch.bootstrap; import org.apache.lucene.util.SetOnce; +import org.elasticsearch.core.Nullable; import org.elasticsearch.core.SuppressForbidden; import java.util.Dictionary; @@ -50,8 +51,10 @@ public static boolean isSystemCallFilterInstalled() { } /** - * Returns a reference to a stream attached to Standard Output, iff we have determined that stdout is a console (tty) + * Returns information about the console (tty) attached to the server process, or {@code null} + * if no console is attached. */ + @Nullable public static ConsoleLoader.Console getConsole() { return console.get(); } @@ -143,7 +146,7 @@ public static Dictionary getSystemProperties() { public static void init() {} - static void setConsole(ConsoleLoader.Console console) { + static void setConsole(@Nullable ConsoleLoader.Console console) { BootstrapInfo.console.set(console); } diff --git a/server/src/main/java/org/elasticsearch/common/logging/ConsoleThrowablePatternConverter.java b/server/src/main/java/org/elasticsearch/common/logging/ConsoleThrowablePatternConverter.java index 3d77ee3299083..7e86cbacd980e 100644 --- a/server/src/main/java/org/elasticsearch/common/logging/ConsoleThrowablePatternConverter.java +++ b/server/src/main/java/org/elasticsearch/common/logging/ConsoleThrowablePatternConverter.java @@ -14,17 +14,25 @@ import org.apache.logging.log4j.core.pattern.ConverterKeys; import org.apache.logging.log4j.core.pattern.PatternConverter; import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter; +import org.elasticsearch.bootstrap.BootstrapInfo; import org.elasticsearch.bootstrap.StartupException; import org.elasticsearch.common.inject.CreationException; /** - * Outputs a very short version of exceptions for the console, pointing to full log for details. + * Outputs a very short version of exceptions for an interactive console, pointing to full log for details. + * + *

If a non-interactive console is attached, the full exception is always printed. */ @Plugin(name = "consoleException", category = PatternConverter.CATEGORY) @ConverterKeys({ "consoleException" }) public class ConsoleThrowablePatternConverter extends ThrowablePatternConverter { - private ConsoleThrowablePatternConverter(String[] options, Configuration config) { + + // true if exceptions should be truncated, false if they should be delegated to the super class + private final boolean enabled; + + private ConsoleThrowablePatternConverter(String[] options, Configuration config, boolean enabled) { super("ConsoleThrowablePatternConverter", "throwable", options, config); + this.enabled = enabled; } /** @@ -34,13 +42,18 @@ private ConsoleThrowablePatternConverter(String[] options, Configuration config) * @return instance of class. */ public static ConsoleThrowablePatternConverter newInstance(final Configuration config, final String[] options) { - return new ConsoleThrowablePatternConverter(options, config); + return newInstance(config, options, BootstrapInfo.getConsole() != null); + } + + // package private for tests + static ConsoleThrowablePatternConverter newInstance(final Configuration config, final String[] options, boolean enabled) { + return new ConsoleThrowablePatternConverter(options, config, enabled); } @Override public void format(final LogEvent event, final StringBuilder toAppendTo) { Throwable error = event.getThrown(); - if (error == null) { + if (enabled == false | error == null) { super.format(event, toAppendTo); return; } @@ -48,6 +61,7 @@ public void format(final LogEvent event, final StringBuilder toAppendTo) { error = e.getCause(); toAppendTo.append("\n\nElasticsearch failed to startup normally.\n\n"); } + appendShortStacktrace(error, toAppendTo); if (error instanceof CreationException) { toAppendTo.append("There were problems initializing Guice. See log for more details."); diff --git a/server/src/test/java/org/elasticsearch/common/logging/ConsoleThrowablePatternConverterTests.java b/server/src/test/java/org/elasticsearch/common/logging/ConsoleThrowablePatternConverterTests.java index 8e55414289cfe..897aa31763ab8 100644 --- a/server/src/test/java/org/elasticsearch/common/logging/ConsoleThrowablePatternConverterTests.java +++ b/server/src/test/java/org/elasticsearch/common/logging/ConsoleThrowablePatternConverterTests.java @@ -26,19 +26,31 @@ import static org.hamcrest.Matchers.not; public class ConsoleThrowablePatternConverterTests extends ESTestCase { - static final ConsoleThrowablePatternConverter converter = ConsoleThrowablePatternConverter.newInstance(null, null); + static final ConsoleThrowablePatternConverter converter = ConsoleThrowablePatternConverter.newInstance(null, null, true); - String format(Throwable e) { + String format(ConsoleThrowablePatternConverter converter, Throwable e) { LogEvent event = Log4jLogEvent.newBuilder().setThrown(e).build(); var builder = new StringBuilder(); converter.format(event, builder); return builder.toString(); } + String format(Throwable e) { + return format(converter, e); + } + public void testNoException() { assertThat(format(null), emptyString()); } + public void testDisabledPassthrough() { + // mimic no interactive console + var converter = ConsoleThrowablePatternConverter.newInstance(null, null, false); + var e = new StartupException(new RuntimeException("a cause")); + // the cause of StartupException is not extracted by the parent pattern converter + assertThat(format(converter, e), allOf(containsString("StartupException: "), containsString("RuntimeException: a cause"))); + } + public void testStartupExceptionUnwrapped() { var e = new StartupException(new RuntimeException("an error")); assertThat(