Skip to content

Commit 090859b

Browse files
authored
Don't close stderr under --quiet (#47208)
Closes #46900. When running ES with `--quiet`, if ES then exits abnormally, a user has to go hunting in the logs for the error. Instead, never close System.err, and print more information to it if ES encounters a fatal error e.g. config validation, or some fatal runtime exception. This is useful when running under e.g. systemd, since the error will go into the journal. Note that stderr is still closed in daemon (`-d`) mode.
1 parent b76c4d8 commit 090859b

File tree

8 files changed

+156
-68
lines changed

8 files changed

+156
-68
lines changed

libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ public final boolean promptYesNo(String prompt, boolean defaultYes) {
151151
}
152152
}
153153

154+
public void flush() {
155+
this.getWriter().flush();
156+
this.getErrorWriter().flush();
157+
}
158+
154159
private static class ConsoleTerminal extends Terminal {
155160

156161
private static final Console CONSOLE = System.console();

qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import org.apache.http.client.fluent.Request;
2424
import org.elasticsearch.packaging.util.FileUtils;
2525
import org.elasticsearch.packaging.util.Shell.Result;
26-
import org.hamcrest.CoreMatchers;
2726
import org.junit.BeforeClass;
2827

2928
import java.nio.charset.StandardCharsets;
@@ -47,10 +46,12 @@
4746
import static org.elasticsearch.packaging.util.Packages.SYSTEMD_SERVICE;
4847
import static org.elasticsearch.packaging.util.Packages.assertInstalled;
4948
import static org.elasticsearch.packaging.util.Packages.assertRemoved;
49+
import static org.elasticsearch.packaging.util.Packages.clearJournal;
5050
import static org.elasticsearch.packaging.util.Packages.installPackage;
5151
import static org.elasticsearch.packaging.util.Packages.remove;
5252
import static org.elasticsearch.packaging.util.Packages.restartElasticsearch;
5353
import static org.elasticsearch.packaging.util.Packages.startElasticsearch;
54+
import static org.elasticsearch.packaging.util.Packages.startElasticsearchIgnoringFailure;
5455
import static org.elasticsearch.packaging.util.Packages.stopElasticsearch;
5556
import static org.elasticsearch.packaging.util.Packages.verifyPackageInstallation;
5657
import static org.elasticsearch.packaging.util.Platforms.getOsRelease;
@@ -60,7 +61,7 @@
6061
import static org.hamcrest.CoreMatchers.equalTo;
6162
import static org.hamcrest.CoreMatchers.not;
6263
import static org.hamcrest.Matchers.containsString;
63-
import static org.hamcrest.Matchers.isEmptyString;
64+
import static org.hamcrest.Matchers.emptyString;
6465
import static org.hamcrest.core.Is.is;
6566
import static org.junit.Assume.assumeThat;
6667
import static org.junit.Assume.assumeTrue;
@@ -79,8 +80,8 @@ public void test10InstallPackage() throws Exception {
7980
verifyPackageInstallation(installation, distribution(), sh);
8081
}
8182

82-
public void test20PluginsCommandWhenNoPlugins() throws Exception {
83-
assertThat(sh.run(installation.bin("elasticsearch-plugin") + " list").stdout, isEmptyString());
83+
public void test20PluginsCommandWhenNoPlugins() {
84+
assertThat(sh.run(installation.bin("elasticsearch-plugin") + " list").stdout, is(emptyString()));
8485
}
8586

8687
public void test30DaemonIsNotEnabledOnRestart() {
@@ -95,7 +96,7 @@ public void test31InstallDoesNotStartServer() {
9596
assertThat(sh.run("ps aux").stdout, not(containsString("org.elasticsearch.bootstrap.Elasticsearch")));
9697
}
9798

98-
public void assertRunsWithJavaHome() throws Exception {
99+
private void assertRunsWithJavaHome() throws Exception {
99100
byte[] originalEnvFile = Files.readAllBytes(installation.envFile);
100101
try {
101102
Files.write(installation.envFile, ("JAVA_HOME=" + systemJavaHome + "\n").getBytes(StandardCharsets.UTF_8),
@@ -286,53 +287,17 @@ public void test80DeletePID_DIRandRestart() throws Exception {
286287
}
287288

288289
public void test81CustomPathConfAndJvmOptions() throws Exception {
289-
assumeTrue(isSystemd());
290-
291-
assertPathsExist(installation.envFile);
292-
293-
stopElasticsearch(sh);
294-
295-
// The custom config directory is not under /tmp or /var/tmp because
296-
// systemd's private temp directory functionally means different
297-
// processes can have different views of what's in these directories
298-
String randomName = RandomStrings.randomAsciiAlphanumOfLength(getRandom(), 10);
299-
sh.run("mkdir /etc/"+randomName);
300-
final Path tempConf = Paths.get("/etc/"+randomName);
301-
302-
try {
303-
mkdir(tempConf);
304-
cp(installation.config("elasticsearch.yml"), tempConf.resolve("elasticsearch.yml"));
305-
cp(installation.config("log4j2.properties"), tempConf.resolve("log4j2.properties"));
306-
307-
// we have to disable Log4j from using JMX lest it will hit a security
308-
// manager exception before we have configured logging; this will fail
309-
// startup since we detect usages of logging before it is configured
310-
final String jvmOptions =
311-
"-Xms512m\n" +
312-
"-Xmx512m\n" +
313-
"-Dlog4j2.disable.jmx=true\n";
314-
append(tempConf.resolve("jvm.options"), jvmOptions);
315-
316-
sh.runIgnoreExitCode("chown -R elasticsearch:elasticsearch " + tempConf);
317-
318-
cp(installation.envFile, tempConf.resolve("elasticsearch.bk"));//backup
319-
append(installation.envFile, "ES_PATH_CONF=" + tempConf + "\n");
290+
withCustomConfig(tempConf -> {
320291
append(installation.envFile, "ES_JAVA_OPTS=-XX:-UseCompressedOops");
321292

322293
startElasticsearch(sh, installation);
323294

324295
final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes"));
325-
assertThat(nodesResponse, CoreMatchers.containsString("\"heap_init_in_bytes\":536870912"));
326-
assertThat(nodesResponse, CoreMatchers.containsString("\"using_compressed_ordinary_object_pointers\":\"false\""));
296+
assertThat(nodesResponse, containsString("\"heap_init_in_bytes\":536870912"));
297+
assertThat(nodesResponse, containsString("\"using_compressed_ordinary_object_pointers\":\"false\""));
327298

328299
stopElasticsearch(sh);
329-
330-
} finally {
331-
rm(installation.envFile);
332-
cp(tempConf.resolve("elasticsearch.bk"), installation.envFile);
333-
rm(tempConf);
334-
cleanup();
335-
}
300+
});
336301
}
337302

338303
public void test82SystemdMask() throws Exception {
@@ -374,4 +339,63 @@ public void test83serviceFileSetsLimits() throws Exception {
374339

375340
stopElasticsearch(sh);
376341
}
342+
343+
public void test90DoNotCloseStderrWhenQuiet() throws Exception {
344+
withCustomConfig(tempConf -> {
345+
// Create a startup problem by adding an invalid YAML line to the config
346+
append(tempConf.resolve("elasticsearch.yml"), "discovery.zen.ping.unicast.hosts:15172.30.5.3416172.30.5.35, 172.30.5.17]\n");
347+
348+
// Make sure we don't pick up the journal entries for previous ES instances.
349+
clearJournal(sh);
350+
startElasticsearchIgnoringFailure(sh);
351+
352+
final Result logs = sh.run("journalctl -u elasticsearch.service");
353+
354+
assertThat(logs.stdout, containsString("Failed to load settings from [elasticsearch.yml]"));
355+
});
356+
}
357+
358+
@FunctionalInterface
359+
private interface CustomConfigConsumer {
360+
void accept(Path path) throws Exception;
361+
}
362+
363+
private void withCustomConfig(CustomConfigConsumer pathConsumer) throws Exception {
364+
assumeTrue(isSystemd());
365+
366+
assertPathsExist(installation.envFile);
367+
368+
stopElasticsearch(sh);
369+
370+
// The custom config directory is not under /tmp or /var/tmp because
371+
// systemd's private temp directory functionally means different
372+
// processes can have different views of what's in these directories
373+
String randomName = RandomStrings.randomAsciiAlphanumOfLength(getRandom(), 10);
374+
sh.run("mkdir /etc/" + randomName);
375+
final Path tempConf = Paths.get("/etc/" + randomName);
376+
377+
try {
378+
mkdir(tempConf);
379+
cp(installation.config("elasticsearch.yml"), tempConf.resolve("elasticsearch.yml"));
380+
cp(installation.config("log4j2.properties"), tempConf.resolve("log4j2.properties"));
381+
382+
// we have to disable Log4j from using JMX lest it will hit a security
383+
// manager exception before we have configured logging; this will fail
384+
// startup since we detect usages of logging before it is configured
385+
final String jvmOptions = "-Xms512m\n-Xmx512m\n-Dlog4j2.disable.jmx=true\n";
386+
append(tempConf.resolve("jvm.options"), jvmOptions);
387+
388+
sh.runIgnoreExitCode("chown -R elasticsearch:elasticsearch " + tempConf);
389+
390+
cp(installation.envFile, tempConf.resolve("elasticsearch.bk"));// backup
391+
append(installation.envFile, "ES_PATH_CONF=" + tempConf + "\n");
392+
393+
pathConsumer.accept(tempConf);
394+
} finally {
395+
rm(installation.envFile);
396+
cp(tempConf.resolve("elasticsearch.bk"), installation.envFile);
397+
rm(tempConf);
398+
cleanup();
399+
}
400+
}
377401
}

qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.commons.logging.Log;
2323
import org.apache.commons.logging.LogFactory;
24+
import org.elasticsearch.common.CheckedRunnable;
2425

2526
import java.nio.file.Path;
2627
import java.nio.file.attribute.PosixFilePermission;
@@ -409,7 +410,7 @@ public static void waitForElasticsearch(String status, String index, Installatio
409410
withLogging(() -> ServerUtils.waitForElasticsearch(status, index, installation, username, password));
410411
}
411412

412-
private static void withLogging(ThrowingRunnable r) throws Exception {
413+
private static <E extends Exception> void withLogging(CheckedRunnable<E> r) throws Exception {
413414
try {
414415
r.run();
415416
} catch (Exception e) {
@@ -418,8 +419,4 @@ private static void withLogging(ThrowingRunnable r) throws Exception {
418419
throw e;
419420
}
420421
}
421-
422-
private interface ThrowingRunnable {
423-
void run() throws Exception;
424-
}
425422
}

qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353
public class Packages {
5454

55-
protected static final Logger logger = LogManager.getLogger(Packages.class);
55+
private static final Logger logger = LogManager.getLogger(Packages.class);
5656

5757
public static final Path SYSVINIT_SCRIPT = Paths.get("/etc/init.d/elasticsearch");
5858
public static final Path SYSTEMD_SERVICE = Paths.get("/usr/lib/systemd/system/elasticsearch.service");
@@ -114,7 +114,7 @@ public static Installation installPackage(Distribution distribution) throws IOEx
114114
return installation;
115115
}
116116

117-
public static Result runInstallCommand(Distribution distribution, Shell sh) {
117+
private static Result runInstallCommand(Distribution distribution, Shell sh) {
118118
final Path distributionFile = distribution.path;
119119

120120
if (Platforms.isRPM()) {
@@ -280,7 +280,33 @@ public static void startElasticsearch(Shell sh, Installation installation) throw
280280
assertElasticsearchStarted(sh, installation);
281281
}
282282

283-
public static void assertElasticsearchStarted(Shell sh, Installation installation) throws IOException {
283+
/**
284+
* Starts Elasticsearch, without checking that startup is successful. To also check
285+
* that Elasticsearch has started, call {@link #startElasticsearch(Shell, Installation)}.
286+
*/
287+
public static void startElasticsearchIgnoringFailure(Shell sh) {
288+
if (isSystemd()) {
289+
sh.runIgnoreExitCode("systemctl daemon-reload");
290+
sh.runIgnoreExitCode("systemctl enable elasticsearch.service");
291+
sh.runIgnoreExitCode("systemctl is-enabled elasticsearch.service");
292+
sh.runIgnoreExitCode("systemctl start elasticsearch.service");
293+
} else {
294+
sh.runIgnoreExitCode("service elasticsearch start");
295+
}
296+
}
297+
298+
/**
299+
* Clears the systemd journal. This is intended to clear the <code>journalctl</code> output
300+
* before a test that checks the journal output.
301+
*/
302+
public static void clearJournal(Shell sh) {
303+
if (isSystemd()) {
304+
sh.run("rm -rf /run/log/journal/");
305+
sh.run("systemctl restart systemd-journald");
306+
}
307+
}
308+
309+
private static void assertElasticsearchStarted(Shell sh, Installation installation) throws IOException {
284310
waitForElasticsearch(installation);
285311

286312
if (isSystemd()) {
@@ -291,7 +317,7 @@ public static void assertElasticsearchStarted(Shell sh, Installation installatio
291317
}
292318
}
293319

294-
public static void stopElasticsearch(Shell sh) throws IOException {
320+
public static void stopElasticsearch(Shell sh) {
295321
if (isSystemd()) {
296322
sh.run("systemctl stop elasticsearch.service");
297323
} else {

server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,12 @@ static void init(
347347

348348
INSTANCE.start();
349349

350-
if (closeStandardStreams) {
350+
// We don't close stderr if `--quiet` is passed, because that
351+
// hides fatal startup errors. For example, if Elasticsearch is
352+
// running via systemd, the init script only specifies
353+
// `--quiet`, not `-d`, so we want users to be able to see
354+
// startup errors via journalctl.
355+
if (foreground == false) {
351356
closeSysError();
352357
}
353358
} catch (NodeValidationException | RuntimeException e) {

server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ public void checkPermission(Permission perm) {
9191
final Elasticsearch elasticsearch = new Elasticsearch();
9292
int status = main(args, elasticsearch, Terminal.DEFAULT);
9393
if (status != ExitCodes.OK) {
94+
final String basePath = System.getProperty("es.logs.base_path");
95+
// It's possible to fail before logging has been configured, in which case there's no point
96+
// suggesting that the user look in the log file.
97+
if (basePath != null) {
98+
Terminal.DEFAULT.errorPrintln(
99+
"ERROR: Elasticsearch did not exit normally - check the logs at "
100+
+ basePath
101+
+ System.getProperty("file.separator")
102+
+ System.getProperty("es.logs.cluster_name") + ".log"
103+
);
104+
}
94105
exit(status);
95106
}
96107
}

server/src/main/java/org/elasticsearch/bootstrap/ElasticsearchUncaughtExceptionHandler.java

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import org.apache.logging.log4j.LogManager;
2323
import org.apache.logging.log4j.Logger;
24-
import org.apache.logging.log4j.message.ParameterizedMessage;
24+
import org.elasticsearch.cli.Terminal;
2525
import org.elasticsearch.common.SuppressForbidden;
2626

2727
import java.io.IOError;
@@ -32,29 +32,29 @@ class ElasticsearchUncaughtExceptionHandler implements Thread.UncaughtExceptionH
3232
private static final Logger logger = LogManager.getLogger(ElasticsearchUncaughtExceptionHandler.class);
3333

3434
@Override
35-
public void uncaughtException(Thread t, Throwable e) {
36-
if (isFatalUncaught(e)) {
35+
public void uncaughtException(Thread thread, Throwable t) {
36+
if (isFatalUncaught(t)) {
3737
try {
38-
onFatalUncaught(t.getName(), e);
38+
onFatalUncaught(thread.getName(), t);
3939
} finally {
4040
// we use specific error codes in case the above notification failed, at least we
4141
// will have some indication of the error bringing us down
42-
if (e instanceof InternalError) {
42+
if (t instanceof InternalError) {
4343
halt(128);
44-
} else if (e instanceof OutOfMemoryError) {
44+
} else if (t instanceof OutOfMemoryError) {
4545
halt(127);
46-
} else if (e instanceof StackOverflowError) {
46+
} else if (t instanceof StackOverflowError) {
4747
halt(126);
48-
} else if (e instanceof UnknownError) {
48+
} else if (t instanceof UnknownError) {
4949
halt(125);
50-
} else if (e instanceof IOError) {
50+
} else if (t instanceof IOError) {
5151
halt(124);
5252
} else {
5353
halt(1);
5454
}
5555
}
5656
} else {
57-
onNonFatalUncaught(t.getName(), e);
57+
onNonFatalUncaught(thread.getName(), t);
5858
}
5959
}
6060

@@ -63,11 +63,21 @@ static boolean isFatalUncaught(Throwable e) {
6363
}
6464

6565
void onFatalUncaught(final String threadName, final Throwable t) {
66-
logger.error(() -> new ParameterizedMessage("fatal error in thread [{}], exiting", threadName), t);
66+
final String message = "fatal error in thread [" + threadName + "], exiting";
67+
logger.error(message, t);
68+
Terminal.DEFAULT.errorPrintln(message);
69+
t.printStackTrace(Terminal.DEFAULT.getErrorWriter());
70+
// Without a final flush, the stacktrace may not be shown before ES exits
71+
Terminal.DEFAULT.flush();
6772
}
6873

6974
void onNonFatalUncaught(final String threadName, final Throwable t) {
70-
logger.warn(() -> new ParameterizedMessage("uncaught exception in thread [{}]", threadName), t);
75+
final String message = "uncaught exception in thread [" + threadName + "]";
76+
logger.error(message, t);
77+
Terminal.DEFAULT.errorPrintln(message);
78+
t.printStackTrace(Terminal.DEFAULT.getErrorWriter());
79+
// Without a final flush, the stacktrace may not be shown if ES goes on to exit
80+
Terminal.DEFAULT.flush();
7181
}
7282

7383
void halt(int status) {

server/src/main/java/org/elasticsearch/bootstrap/StartupException.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,17 @@ private void printStackTrace(Consumer<String> consumer) {
111111
// if its a guice exception, the whole thing really will not be in the log, its megabytes.
112112
// refer to the hack in bootstrap, where we don't log it
113113
if (originalCause instanceof CreationException == false) {
114-
consumer.accept("Refer to the log for complete error details.");
114+
final String basePath = System.getProperty("es.logs.base_path");
115+
// It's possible to fail before logging has been configured, in which case there's no point
116+
// suggested that the user look in the log file.
117+
if (basePath != null) {
118+
final String logPath = System.getProperty("es.logs.base_path")
119+
+ System.getProperty("file.separator")
120+
+ System.getProperty("es.logs.cluster_name")
121+
+ ".log";
122+
123+
consumer.accept("For complete error details, refer to the log at " + logPath);
124+
}
115125
}
116126
}
117127

0 commit comments

Comments
 (0)