Browse Source

Implement custom JUL bridge (#96872)

The log4j JUL bridge turned out to have issues because it relied on java
beans. This commit implements a custom bridge between JUL and Log4j.

closes #94613
Ryan Ernst 2 years ago
parent
commit
7d8aac3a3e

+ 1 - 1
qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerConfigurationTests.java

@@ -144,7 +144,7 @@ public class EvilLoggerConfigurationTests extends ESTestCase {
         final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
         final Configuration config = ctx.getConfiguration();
         final Map<String, LoggerConfig> loggerConfigs = config.getLoggers();
-        assertThat(loggerConfigs.size(), equalTo(3));
+        assertThat(loggerConfigs.size(), equalTo(5));
         assertThat(loggerConfigs, hasKey(""));
         assertThat(loggerConfigs.get("").getLevel(), equalTo(rootLevel));
         assertThat(loggerConfigs, hasKey("foo"));

+ 1 - 0
server/build.gradle

@@ -248,6 +248,7 @@ tasks.named("thirdPartyAudit").configure {
 
 tasks.named("dependencyLicenses").configure {
     mapping from: /lucene-.*/, to: 'lucene'
+    mapping from: /log4j-.*/, to: 'log4j'
     dependencies = project.configurations.runtimeClasspath.fileCollection {
         it.group.startsWith('org.elasticsearch') == false ||
                 // keep the following org.elasticsearch jars in

+ 0 - 0
server/licenses/log4j-api-LICENSE.txt → server/licenses/log4j-LICENSE.txt


+ 0 - 0
server/licenses/log4j-api-NOTICE.txt → server/licenses/log4j-NOTICE.txt


+ 0 - 202
server/licenses/log4j-core-LICENSE.txt

@@ -1,202 +0,0 @@
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright 1999-2005 The Apache Software Foundation
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.

+ 0 - 20
server/licenses/log4j-core-NOTICE.txt

@@ -1,20 +0,0 @@
-Apache Log4j
-Copyright 1999-2023 Apache Software Foundation
-
-This product includes software developed at
-The Apache Software Foundation (http://www.apache.org/).
-
-ResolverUtil.java
-Copyright 2005-2006 Tim Fennell
-
-Dumbster SMTP test server
-Copyright 2004 Jason Paul Kitchen
-
-TypeUtil.java
-Copyright 2002-2012 Ramnivas Laddad, Juergen Hoeller, Chris Beams
-
-picocli (http://picocli.info)
-Copyright 2017 Remko Popma
-
-TimeoutBlockingWaitStrategy.java and parts of Util.java
-Copyright 2011 LMAX Ltd.

+ 97 - 0
server/src/main/java/org/elasticsearch/common/logging/JULBridge.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.common.logging;
+
+import org.elasticsearch.common.util.Maps;
+import org.elasticsearch.logging.Level;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+
+/**
+ * A Java Util Logging handler that writes log messages to the Elasticsearch logging framework.
+ */
+class JULBridge extends Handler {
+
+    private static final Map<java.util.logging.Level, Level> levelMap = Map.of(
+        java.util.logging.Level.OFF,
+        Level.OFF,
+        java.util.logging.Level.SEVERE,
+        Level.ERROR,
+        java.util.logging.Level.WARNING,
+        Level.WARN,
+        java.util.logging.Level.INFO,
+        Level.INFO,
+        java.util.logging.Level.FINE,
+        Level.DEBUG,
+        java.util.logging.Level.FINEST,
+        Level.TRACE,
+        java.util.logging.Level.ALL,
+        Level.ALL
+    );
+
+    private static final NavigableMap<Integer, Level> sortedLevelMap = levelMap.entrySet()
+        .stream()
+        .collect(Maps.toUnmodifiableSortedMap(e -> e.getKey().intValue(), Map.Entry::getValue));
+
+    public static void install() {
+        var rootJulLogger = java.util.logging.LogManager.getLogManager().getLogger("");
+        // clear out any other handlers, so eg we don't also print to stdout
+        for (var existingHandler : rootJulLogger.getHandlers()) {
+            rootJulLogger.removeHandler(existingHandler);
+        }
+        rootJulLogger.addHandler(new JULBridge());
+    }
+
+    private JULBridge() {}
+
+    @Override
+    public void publish(LogRecord record) {
+        Logger logger = LogManager.getLogger(record.getLoggerName());
+        Level level = translateJulLevel(record.getLevel());
+        Throwable thrown = record.getThrown();
+
+        String rawMessage = record.getMessage();
+        final String message;
+        if (rawMessage == null) {
+            message = "<null message>";
+        } else {
+            message = new MessageFormat(rawMessage, Locale.ROOT).format(record.getParameters());
+        }
+
+        if (thrown == null) {
+            logger.log(level, message);
+        } else {
+            logger.log(level, () -> message, thrown);
+        }
+    }
+
+    private Level translateJulLevel(java.util.logging.Level julLevel) {
+        Level esLevel = levelMap.get(julLevel);
+        if (esLevel != null) {
+            return esLevel;
+        }
+        // no matching known level, so find the closest level by int value
+        var closestEntry = sortedLevelMap.lowerEntry(julLevel.intValue());
+        assert closestEntry != null; // not possible since ALL is min int
+        return closestEntry.getValue();
+    }
+
+    @Override
+    public void flush() {}
+
+    @Override
+    public void close() {}
+}

+ 32 - 17
server/src/main/java/org/elasticsearch/common/logging/LogConfigurator.java

@@ -12,6 +12,7 @@ import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.appender.ConsoleAppender;
 import org.apache.logging.log4j.core.config.AbstractConfiguration;
@@ -21,6 +22,7 @@ import org.apache.logging.log4j.core.config.Configurator;
 import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
 import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
 import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
+import org.apache.logging.log4j.core.config.builder.impl.DefaultConfigurationBuilder;
 import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
 import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
 import org.apache.logging.log4j.core.config.properties.PropertiesConfiguration;
@@ -222,6 +224,7 @@ public class LogConfigurator {
                         properties.setProperty(name, value.replace("%marker", "[%node_name]%marker "));
                     }
                 }
+
                 // end hack
                 return new PropertiesConfigurationBuilder().setConfigurationSource(source)
                     .setRootProperties(properties)
@@ -241,6 +244,8 @@ public class LogConfigurator {
         });
         assert configurations.isEmpty() == false;
 
+        configurations.add(createStaticConfiguration(context));
+
         context.start(new CompositeConfiguration(configurations));
 
         configureLoggerLevels(settings);
@@ -257,26 +262,13 @@ public class LogConfigurator {
                 );
         }
 
+        JULBridge.install();
+
         // Redirect stdout/stderr to log4j. While we ensure Elasticsearch code does not write to those streams,
         // third party libraries may do that. Note that we do NOT close the streams because other code may have
         // grabbed a handle to the streams and intend to write to it, eg log4j for writing to the console
-        System.setOut(
-            new PrintStream(new LoggingOutputStream(LogManager.getLogger("stdout"), Level.INFO, List.of()), false, StandardCharsets.UTF_8)
-        );
-        System.setErr(
-            new PrintStream(
-                new LoggingOutputStream(
-                    LogManager.getLogger("stderr"),
-                    Level.WARN,
-                    // MMapDirectory messages come from Lucene, suggesting to users as a warning that they should enable preview features in
-                    // the JDK. Vector logs come from Lucene too, but only if the used explicitly disables the Vector API - no point warning
-                    // in this case.
-                    List.of("MMapDirectory", "VectorUtilProvider", "WARNING: Java vector incubator module is not readable")
-                ),
-                false,
-                StandardCharsets.UTF_8
-            )
-        );
+        System.setOut(new PrintStream(new LoggingOutputStream(LogManager.getLogger("stdout"), Level.INFO), false, StandardCharsets.UTF_8));
+        System.setErr(new PrintStream(new LoggingOutputStream(LogManager.getLogger("stderr"), Level.WARN), false, StandardCharsets.UTF_8));
 
         final Logger rootLogger = LogManager.getRootLogger();
         Appender appender = Loggers.findAppender(rootLogger, ConsoleAppender.class);
@@ -289,6 +281,29 @@ public class LogConfigurator {
         }
     }
 
+    /**
+     * Creates a log4j configuration that is not changeable by users.
+     */
+    private static AbstractConfiguration createStaticConfiguration(LoggerContext context) {
+        var builder = new DefaultConfigurationBuilder<>();
+        builder.setConfigurationSource(ConfigurationSource.NULL_SOURCE);
+        builder.setLoggerContext(context);
+
+        // adding filters for confusing Lucene messages
+        addRegexFilter(builder, "org.apache.lucene.store.MemorySegmentIndexInputProvider", "Using MemorySegmentIndexInput.*");
+        addRegexFilter(builder, "org.apache.lucene.util.VectorUtilProvider", ".* incubator module is not readable.*");
+
+        return builder.build();
+    }
+
+    private static void addRegexFilter(DefaultConfigurationBuilder<BuiltConfiguration> builder, String loggerName, String pattern) {
+        var filterBuilder = builder.newFilter("RegexFilter", Filter.Result.DENY, Filter.Result.NEUTRAL);
+        filterBuilder.addAttribute("regex", pattern);
+        var loggerBuilder = builder.newLogger(loggerName);
+        loggerBuilder.add(filterBuilder);
+        builder.add(loggerBuilder);
+    }
+
     /**
      * Removes the appender for the console, if one exists.
      */

+ 2 - 15
server/src/main/java/org/elasticsearch/common/logging/LoggingOutputStream.java

@@ -15,7 +15,6 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
-import java.util.List;
 
 /**
  * A stream whose output is sent to the configured logger, line by line.
@@ -43,12 +42,9 @@ class LoggingOutputStream extends OutputStream {
 
     private final Level level;
 
-    private final List<String> messageFilters;
-
-    LoggingOutputStream(Logger logger, Level level, List<String> messageFilters) {
+    LoggingOutputStream(Logger logger, Level level) {
         this.logger = logger;
         this.level = level;
-        this.messageFilters = messageFilters;
     }
 
     @Override
@@ -107,17 +103,8 @@ class LoggingOutputStream extends OutputStream {
         threadLocal = null;
     }
 
-    private void log(String msg) {
-        for (String filter : messageFilters) {
-            if (msg.contains(filter)) {
-                return;
-            }
-        }
-        this.log0(msg);
-    }
-
     // pkg private for testing
-    protected void log0(String msg) {
+    protected void log(String msg) {
         logger.log(level, msg);
     }
 }

+ 154 - 0
server/src/test/java/org/elasticsearch/common/logging/JULBridgeTests.java

@@ -0,0 +1,154 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.common.logging;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.LogEvent;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.MockLogAppender;
+import org.elasticsearch.test.MockLogAppender.LoggingExpectation;
+import org.elasticsearch.test.MockLogAppender.SeenEventExpectation;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import java.util.logging.ConsoleHandler;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.arrayContaining;
+import static org.hamcrest.Matchers.instanceOf;
+
+public class JULBridgeTests extends ESTestCase {
+
+    private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger("");
+    private static java.util.logging.Level savedLevel;
+    private static java.util.logging.Handler[] savedHandlers;
+
+    @BeforeClass
+    public static void saveLoggerState() {
+        savedLevel = logger.getLevel();
+        savedHandlers = logger.getHandlers();
+    }
+
+    @Before
+    public void resetLogger() {
+        logger.setLevel(java.util.logging.Level.ALL);
+        for (var existingHandler : logger.getHandlers()) {
+            logger.removeHandler(existingHandler);
+        }
+    }
+
+    @AfterClass
+    public static void restoreLoggerState() {
+        logger.setLevel(savedLevel);
+        for (var existingHandler : logger.getHandlers()) {
+            logger.removeHandler(existingHandler);
+        }
+        for (var savedHandler : savedHandlers) {
+            logger.addHandler(savedHandler);
+        }
+    }
+
+    private void assertLogged(Runnable loggingCode, LoggingExpectation... expectations) {
+        Logger testLogger = LogManager.getLogger("");
+        Loggers.setLevel(testLogger, Level.ALL);
+        MockLogAppender mockAppender = new MockLogAppender();
+        mockAppender.start();
+        try {
+            Loggers.addAppender(testLogger, mockAppender);
+            for (var expectation : expectations) {
+                mockAppender.addExpectation(expectation);
+            }
+            loggingCode.run();
+            mockAppender.assertAllExpectationsMatched();
+        } finally {
+            Loggers.removeAppender(testLogger, mockAppender);
+            mockAppender.stop();
+        }
+    }
+
+    private void assertMessage(String msg, java.util.logging.Level julLevel, Level expectedLevel) {
+        assertLogged(() -> logger.log(julLevel, msg), new SeenEventExpectation(msg, "", expectedLevel, msg));
+    }
+
+    private static java.util.logging.Level julLevel(int value) {
+        return java.util.logging.Level.parse(Integer.toString(value));
+    }
+
+    public void testInstallRemovesExistingHandlers() {
+        logger.addHandler(new ConsoleHandler());
+        JULBridge.install();
+        assertThat(logger.getHandlers(), arrayContaining(instanceOf(JULBridge.class)));
+    }
+
+    public void testKnownLevels() {
+        JULBridge.install();
+        assertMessage("off msg", java.util.logging.Level.OFF, Level.OFF);
+        assertMessage("severe msg", java.util.logging.Level.SEVERE, Level.ERROR);
+        assertMessage("warning msg", java.util.logging.Level.WARNING, Level.WARN);
+        assertMessage("info msg", java.util.logging.Level.INFO, Level.INFO);
+        assertMessage("fine msg", java.util.logging.Level.FINE, Level.DEBUG);
+        assertMessage("finest msg", java.util.logging.Level.FINEST, Level.TRACE);
+    }
+
+    public void testCustomLevels() {
+        JULBridge.install();
+        assertMessage("smallest level", julLevel(Integer.MIN_VALUE), Level.ALL);
+        assertMessage("largest level", julLevel(Integer.MAX_VALUE), Level.OFF);
+        assertMessage("above severe", julLevel(java.util.logging.Level.SEVERE.intValue() + 1), Level.ERROR);
+        assertMessage("above warning", julLevel(java.util.logging.Level.WARNING.intValue() + 1), Level.WARN);
+        assertMessage("above info", julLevel(java.util.logging.Level.INFO.intValue() + 1), Level.INFO);
+        assertMessage("above fine", julLevel(java.util.logging.Level.FINE.intValue() + 1), Level.DEBUG);
+        assertMessage("above finest", julLevel(java.util.logging.Level.FINEST.intValue() + 1), Level.TRACE);
+    }
+
+    public void testThrowable() {
+        JULBridge.install();
+        java.util.logging.Logger logger = java.util.logging.Logger.getLogger("");
+        assertLogged(() -> logger.log(java.util.logging.Level.SEVERE, "error msg", new Exception("some error")), new LoggingExpectation() {
+            boolean matched = false;
+
+            @Override
+            public void match(LogEvent event) {
+                Throwable thrown = event.getThrown();
+                matched = event.getLoggerName().equals("")
+                    && event.getMessage().getFormattedMessage().equals("error msg")
+                    && thrown != null
+                    && thrown.getMessage().equals("some error");
+            }
+
+            @Override
+            public void assertMatched() {
+                assertThat("expected to see error message but did not", matched, equalTo(true));
+            }
+        });
+    }
+
+    public void testChildLogger() {
+        JULBridge.install();
+        java.util.logging.Logger childLogger = java.util.logging.Logger.getLogger("foo");
+        assertLogged(() -> childLogger.info("child msg"), new SeenEventExpectation("child msg", "foo", Level.INFO, "child msg"));
+    }
+
+    public void testNullMessage() {
+        JULBridge.install();
+        assertLogged(() -> logger.info((String) null), new SeenEventExpectation("null msg", "", Level.INFO, "<null message>"));
+    }
+
+    public void testFormattedMessage() {
+        JULBridge.install();
+        assertLogged(
+            () -> logger.log(java.util.logging.Level.INFO, "{0}", "a var"),
+            new SeenEventExpectation("formatted msg", "", Level.INFO, "a var")
+        );
+    }
+}

+ 2 - 12
server/src/test/java/org/elasticsearch/common/logging/LoggingOutputStreamTests.java

@@ -29,22 +29,20 @@ public class LoggingOutputStreamTests extends ESTestCase {
         List<String> lines = new ArrayList<>();
 
         TestLoggingOutputStream() {
-            super(null, null, messageFilters);
+            super(null, null);
         }
 
         @Override
-        protected void log0(String msg) {
+        protected void log(String msg) {
             lines.add(msg);
         }
     }
 
-    List<String> messageFilters = new ArrayList<>();
     TestLoggingOutputStream loggingStream;
     PrintStream printStream;
 
     @Before
     public void createStream() {
-        messageFilters.clear();
         loggingStream = new TestLoggingOutputStream();
         printStream = new PrintStream(loggingStream, false, StandardCharsets.UTF_8);
     }
@@ -117,12 +115,4 @@ public class LoggingOutputStreamTests extends ESTestCase {
         printStream.flush();
         assertThat(loggingStream.lines, contains("from thread 2", "from thread 1"));
     }
-
-    public void testMessageFilters() throws Exception {
-        messageFilters.add("foo bar");
-        printStream.println("prefix foo bar suffix");
-        printStream.println("non-filtered message");
-        printStream.flush();
-        assertThat(loggingStream.lines, contains("non-filtered message"));
-    }
 }