diff --git a/core/src/main/java/io/jstach/rainbowgum/LogRouter.java b/core/src/main/java/io/jstach/rainbowgum/LogRouter.java index 8de09eca..36ca1e0b 100644 --- a/core/src/main/java/io/jstach/rainbowgum/LogRouter.java +++ b/core/src/main/java/io/jstach/rainbowgum/LogRouter.java @@ -17,6 +17,7 @@ import org.eclipse.jdt.annotation.Nullable; +import io.jstach.rainbowgum.LogConfig.ChangePublisher.ChangeType; import io.jstach.rainbowgum.LogProperty.Property; import io.jstach.rainbowgum.LogPublisher.PublisherFactory; import io.jstach.rainbowgum.LogRouter.RootRouter; @@ -154,6 +155,13 @@ sealed interface RootRouter extends LogRouter permits InternalRootRouter { */ public void onChange(Consumer router); + /** + * If a logger name configuration is changeable. + * @param loggerName logger name. + * @return true if config changes are allowed. + */ + public boolean isChangeable(String loggerName); + } /** @@ -567,7 +575,7 @@ static void setRouter(RootRouter router) { GlobalLogRouter.INSTANCE.drain((InternalRootRouter) router); } - static InternalRootRouter of(List routes) { + static InternalRootRouter of(List routes, LogConfig config) { if (routes.isEmpty()) { throw new IllegalArgumentException("atleast one route is required"); @@ -598,11 +606,14 @@ static InternalRootRouter of(List routes) { Router[] array = routes.toArray(new Router[] {}); + RouteChangePublisher changePublisher = new RouteChangePublisher( + s -> config.changePublisher().allowedChanges(s).contains(ChangeType.LEVEL)); if (array.length == 1) { var r = array[0]; - return r.synchronous() ? new SingleSyncRootRouter(r) : new SingleAsyncRootRouter(r); + return r.synchronous() ? new SingleSyncRootRouter(r, changePublisher) + : new SingleAsyncRootRouter(r, changePublisher); } - return new CompositeLogRouter(array, globalLevelResolver); + return new CompositeLogRouter(array, globalLevelResolver, changePublisher); } default boolean isEnabled(String loggerName, java.lang.System.Logger.Level level) { @@ -621,10 +632,22 @@ default void onChange(Consumer router) { public RouteChangePublisher changePublisher(); + @Override + default boolean isChangeable(String loggerName) { + return changePublisher().loggerChangeable.apply(loggerName); + } + final class RouteChangePublisher { private final Collection> consumers = new CopyOnWriteArrayList>(); + private final Function loggerChangeable; + + RouteChangePublisher(Function loggerChangeable) { + super(); + this.loggerChangeable = loggerChangeable; + } + void add(Consumer consumer) { consumers.add(consumer); } @@ -646,10 +669,6 @@ void transferTo(RouteChangePublisher changePublisher) { record SingleSyncRootRouter(Router router, RouteChangePublisher changePublisher) implements InternalRootRouter { - public SingleSyncRootRouter(Router router) { - this(router, new RouteChangePublisher()); - } - @Override public void start(LogConfig config) { router.start(config); @@ -674,10 +693,6 @@ public Route route(String loggerName, Level level) { record SingleAsyncRootRouter(Router router, RouteChangePublisher changePublisher) implements InternalRootRouter { - public SingleAsyncRootRouter(Router router) { - this(router, new RouteChangePublisher()); - } - @Override public void start(LogConfig config) { router.start(config); @@ -703,10 +718,6 @@ public Route route(String loggerName, Level level) { record CompositeLogRouter(Router[] routers, LevelResolver levelResolver, RouteChangePublisher changePublisher) implements InternalRootRouter, Route { - public CompositeLogRouter(Router[] routers, LevelResolver levelResolver) { - this(routers, levelResolver, new RouteChangePublisher()); - } - @Override public Route route(String loggerName, Level level) { /* @@ -767,7 +778,7 @@ final class QueueEventsRouter implements InternalRootRouter, Route { private static final LevelResolver INFO_RESOLVER = StaticLevelResolver.of(Level.INFO); - private final RouteChangePublisher changePublisher = new RouteChangePublisher(); + private final RouteChangePublisher changePublisher = new RouteChangePublisher(s -> true); @Override public LevelResolver levelResolver() { diff --git a/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java b/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java index fe838e63..202b1303 100644 --- a/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java +++ b/core/src/main/java/io/jstach/rainbowgum/RainbowGum.java @@ -305,7 +305,7 @@ private RainbowGum build(UUID instanceId) { routes = routeNames.stream().map(n -> Router.builder(n, config).build()).toList(); } } - var root = InternalRootRouter.of(routes); + var root = InternalRootRouter.of(routes, config); config.changePublisher().subscribe(c -> { root.changePublisher().publish(root); }); @@ -332,6 +332,17 @@ public RainbowGum set() { return gum; } + /** + * Removes the currently set Rainbow Gum which will close it if one exists and + * will be replaced by the default provision process if {@link RainbowGum#of()} is + * called before {@link #set()}. + * @apiNote This is largely an internal detail for unit testing the provision + * process. + */ + public void unset() { + RainbowGumHolder.remove(null); + } + /** * For returning an optional for the LogProvider contract. * @return optional that always has a rainbow gum. @@ -415,17 +426,21 @@ static RainbowGum get() { } - static boolean remove(RainbowGum gum) { + static boolean remove(@Nullable RainbowGum gum) { lock.writeLock().lock(); try { var original = rainbowGum; - if (original != gum) { - return false; + if (gum != null) { + if (original != gum) { + return false; + } } /* * Reset the global router */ - LogRouter.global().close(); + if (original != null) { + LogRouter.global().close(); + } rainbowGum = null; supplier = RainbowGumServiceProvider::provide; return true; diff --git a/core/src/main/java/io/jstach/rainbowgum/spi/RainbowGumServiceProvider.java b/core/src/main/java/io/jstach/rainbowgum/spi/RainbowGumServiceProvider.java index ba1158fd..b13a54a4 100644 --- a/core/src/main/java/io/jstach/rainbowgum/spi/RainbowGumServiceProvider.java +++ b/core/src/main/java/io/jstach/rainbowgum/spi/RainbowGumServiceProvider.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; @@ -223,7 +224,7 @@ public non-sealed interface RainbowGumEagerLoad extends RainbowGumServiceProvide * @return true if something is marked as eager. */ public static boolean exists() { - var sl = ServiceLoaderCache.SERVICE_LOADER_CACHE.findServiceLoader(defaultClassLoader()); + var sl = ServiceLoaderCache.SERVICE_LOADER_CACHE.findServiceLoader(); return sl.stream() .filter(p -> RainbowGumEagerLoad.class.isAssignableFrom(p.type())) .findFirst() @@ -292,25 +293,26 @@ public static RainbowGum provide() { private static ServiceLoader cachedServiceLoader(@Nullable ClassLoader classLoader) { if (classLoader == null) { - classLoader = defaultClassLoader(); + return ServiceLoaderCache.SERVICE_LOADER_CACHE.findServiceLoader(); } return ServiceLoaderCache.SERVICE_LOADER_CACHE.serviceLoader(classLoader); } - private static ClassLoader defaultClassLoader() { - // TODO perhaps we should use RainbowGum.class classloader - return ClassLoader.getSystemClassLoader(); - } - } +@SuppressWarnings("ImmutableEnumChecker") enum ServiceLoaderCache { SERVICE_LOADER_CACHE; - private WeakHashMap> serviceLoaderCache = new WeakHashMap<>(); + private final Map> serviceLoaderCache = new WeakHashMap<>(); + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + static ClassLoader defaultClassLoader() { + // TODO perhaps we should use RainbowGum.class classloader + return ClassLoader.getSystemClassLoader(); + } ServiceLoader serviceLoader(ClassLoader classLoader) { lock.writeLock().lock(); @@ -325,27 +327,36 @@ ServiceLoader serviceLoader(ClassLoader classLoader) } } - ServiceLoader findServiceLoader(ClassLoader fallback) { - lock.readLock().lock(); - try { - @Nullable - ServiceLoader loader = null; - @Nullable - ClassLoader[] classLoaders = new @Nullable ClassLoader[] { Thread.currentThread().getContextClassLoader() }; - for (var cl : classLoaders) { - if (cl != null) { - loader = serviceLoader(cl); + private @Nullable ServiceLoader _findServiceLoader( + @Nullable ClassLoader[] classLoaders) { + for (var cl : classLoaders) { + if (cl != null) { + var sl = serviceLoaderCache.get(cl); + if (sl != null) { + return sl; } } - if (loader == null) { - return serviceLoader(fallback); - } - return loader; + } + return null; + } + ServiceLoader findServiceLoader() { + @Nullable + ServiceLoader loader = null; + @Nullable + ClassLoader[] classLoaders = new @Nullable ClassLoader[] { Thread.currentThread().getContextClassLoader(), + ServiceLoaderCache.class.getClassLoader(), defaultClassLoader() }; + lock.readLock().lock(); + try { + loader = _findServiceLoader(classLoaders); } finally { lock.readLock().unlock(); } + if (loader != null) { + return loader; + } + return serviceLoader(defaultClassLoader()); } } diff --git a/core/src/test/java/io/jstach/rainbowgum/RouterTest.java b/core/src/test/java/io/jstach/rainbowgum/RouterTest.java index c85057a5..eba483af 100644 --- a/core/src/test/java/io/jstach/rainbowgum/RouterTest.java +++ b/core/src/test/java/io/jstach/rainbowgum/RouterTest.java @@ -43,7 +43,8 @@ void testCompositeRouter() throws Exception { var publisher2 = new TestSyncPublisher(); var router2 = new SimpleRouter("2", publisher2, resolver2); - var root = InternalRootRouter.of(List.of(router1, router2)); + var config = LogConfig.builder().build(); + var root = InternalRootRouter.of(List.of(router1, router2), config); var route = root.route("stuff", Level.DEBUG); diff --git a/doc/overview.html b/doc/overview.html index 2282abe0..10e1d6bd 100644 --- a/doc/overview.html +++ b/doc/overview.html @@ -950,13 +950,19 @@

java.lang.System.Logger and java.util.logging

The integration will make sure that neither the System.Logger or java.util.logging initialize -Rainbow Gum too early by queueing the events. When a Rainbow Gum initializes and +Rainbow Gum too early by queueing the events if some other facade is detected like SLF4J. When a Rainbow Gum initializes and {@linkplain io.jstach.rainbowgum.RainbowGum#set(Supplier) set as global} the events will be replayed. If the events level are equal to {@link java.lang.System.Logger.Level#ERROR} and a normal Rainbow Gum has not been bound the messages will be printed to System.err. The idea is something catastrophic has happened that will probably cause Rainbow Gum to never load and thus never replay the events and you will not be able to figure out what happened otherwise.

+

+The exception of the above is if {@linkplain io.jstach.rainbowgum.spi.RainbowGumServiceProvider.RainbowGumEagerLoad no other facade is detected} +the module will initialize Rainbow Gum. The idea is that your application only uses the JDK logging facilities +(the Rainbow Gum SLF4J implementation is not found in the module/classpath). Thus only having the dependencies +rainbowgum-core, and rainbowgum-jdk will work as normal. +

SLF4J does provide an adapter/bridge for the System.Logger (org.slf4j:slf4j-jdk-platform-logging) @@ -969,7 +975,6 @@

java.lang.System.Logger and java.util.logging

(the SLF4J adapter will initialize Rainbow Gum on System.Logger usage if using {@link io.jstach.rainbowgum.slf4j/ }) - In the future Rainbow Gum may include a System.Logger provider module that does eager initialization.

NOTE: diff --git a/rainbowgum-jdk/src/main/java/module-info.java b/rainbowgum-jdk/src/main/java/module-info.java index 5a6b58e5..a22fc160 100644 --- a/rainbowgum-jdk/src/main/java/module-info.java +++ b/rainbowgum-jdk/src/main/java/module-info.java @@ -9,14 +9,19 @@ *

* The integration will make sure that neither the System.Logger or * java.util.logging will initialize Rainbow Gum too early by queueing the - * events. When a Rainbow Gum initializes and + * events if a + * {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.RainbowGumEagerLoad} + * implementation is found (SLF4J facade implements this to indicate it will load Rainbow Gum). + * When a Rainbow Gum initializes and * {@linkplain io.jstach.rainbowgum.RainbowGum#set(Supplier) set as global} the * events will be replayed. If the events level are equal to * {@link java.lang.System.Logger.Level#ERROR} and a normal Rainbow Gum has not * been bound the messages will be printed to System.err. The idea * is something catastrophic has happened that will probably cause Rainbow Gum * to never load and thus never replay the events and you will not be able to - * figure out what happened. + * figure out what happened. If no + * {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.RainbowGumEagerLoad} + * is found the SystemLogger will initialize Rainbow Gum. *

*

* SLF4J does provide an diff --git a/rainbowgum-systemlogger/src/main/java/io/jstach/rainbowgum/systemlogger/LevelSystemLogger.java b/rainbowgum-systemlogger/src/main/java/io/jstach/rainbowgum/systemlogger/LevelSystemLogger.java new file mode 100644 index 00000000..d8d92ce7 --- /dev/null +++ b/rainbowgum-systemlogger/src/main/java/io/jstach/rainbowgum/systemlogger/LevelSystemLogger.java @@ -0,0 +1,152 @@ +package io.jstach.rainbowgum.systemlogger; + +import static java.util.Objects.requireNonNullElse; + +import java.time.Instant; +import java.util.ResourceBundle; +import java.util.function.Supplier; + +import org.eclipse.jdt.annotation.Nullable; + +import io.jstach.rainbowgum.KeyValues; +import io.jstach.rainbowgum.LogEvent; +import io.jstach.rainbowgum.LogEventLogger; +import io.jstach.rainbowgum.LogMessageFormatter.StandardMessageFormatter; + +record LevelSystemLogger(String loggerName, int level, LogEventLogger logger) implements System.Logger { + + static System.Logger of(String loggerName, Level level, LogEventLogger logger) { + if (level == Level.OFF) { + return new OffSystemLogger(loggerName); + } + return new LevelSystemLogger(loggerName, fixLevel(level).getSeverity(), logger); + } + + record OffSystemLogger(String loggerName) implements System.Logger { + + @Override + public String getName() { + return loggerName; + } + + @Override + public boolean isLoggable(Level level) { + return false; + } + + @Override + public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) { + + } + + @Override + public void log(Level level, ResourceBundle bundle, String format, Object... params) { + } + } + + static Level fixLevel(Level level) { + if (level == Level.ALL) { + return Level.TRACE; + } + return level; + } + + @Override + public String getName() { + return loggerName; + } + + @Override + public final boolean isLoggable(Level level) { + if (level == Level.OFF) { + return false; + } + return fixLevel(level).getSeverity() >= this.level; + } + + @Override + public void log(Level level, @Nullable String msg) { + if (isLoggable(level)) { + String formattedMessage = msg == null ? "" : msg; + LogEvent event = LogEvent.of(level, loggerName, formattedMessage, null); + logger.log(event); + } + } + + @Override + public void log(Level level, Supplier msgSupplier) { + if (isLoggable(level)) { + String formattedMessage = requireNonNullElse(msgSupplier.get(), ""); + LogEvent event = LogEvent.of(level, loggerName, formattedMessage, null); + logger.log(event); + } + } + + @Override + public void log(Level level, @Nullable Object obj) { + if (isLoggable(level)) { + String formattedMessage = obj == null ? "" : obj.toString(); + LogEvent event = LogEvent.of(level, loggerName, formattedMessage, null); + logger.log(event); + } + } + + @Override + public void log(Level level, @Nullable String msg, @Nullable Throwable thrown) { + if (isLoggable(level)) { + String formattedMessage = requireNonNullElse(msg, ""); + LogEvent event = LogEvent.of(level, loggerName, formattedMessage, thrown); + logger.log(event); + } + } + + @Override + public void log(Level level, Supplier msgSupplier, Throwable thrown) { + if (isLoggable(level)) { + String formattedMessage = requireNonNullElse(msgSupplier.get(), ""); + LogEvent event = LogEvent.of(level, loggerName, formattedMessage, thrown); + logger.log(event); + } + } + + @Override + public void log(Level level, @Nullable String format, @Nullable Object... params) { + if (isLoggable(level)) { + var currentThread = Thread.currentThread(); + Instant timestamp = Instant.now(); + String threadName = currentThread.getName(); + long threadId = currentThread.threadId(); + String message = requireNonNullElse(format, ""); + Throwable throwable = null; + LogEvent event = LogEvent.ofAll(timestamp, threadName, threadId, level, loggerName, message, KeyValues.of(), + throwable, StandardMessageFormatter.JUL, params); + logger.log(event); + } + } + + @Override + public void log(Level level, @Nullable ResourceBundle bundle, @Nullable String msg, @Nullable Throwable thrown) { + if (isLoggable(level)) { + String formattedMessage = requireNonNullElse(msg, ""); + LogEvent event = LogEvent.of(level, loggerName, formattedMessage, thrown); + logger.log(event); + } + } + + @Override + public void log(Level level, @Nullable ResourceBundle bundle, @Nullable String format, @Nullable Object... params) { + if (isLoggable(level)) { + var currentThread = Thread.currentThread(); + Instant timestamp = Instant.now(); + String threadName = currentThread.getName(); + long threadId = currentThread.threadId(); + String message = requireNonNullElse(format, ""); + Throwable throwable = null; + LogEvent event = LogEvent.ofAll(timestamp, threadName, threadId, level, loggerName, message, KeyValues.of(), + throwable, StandardMessageFormatter.JUL, params); + logger.log(event); + } + + } + +} diff --git a/rainbowgum-systemlogger/src/main/java/io/jstach/rainbowgum/systemlogger/RainbowGumSystemLoggerFinder.java b/rainbowgum-systemlogger/src/main/java/io/jstach/rainbowgum/systemlogger/RainbowGumSystemLoggerFinder.java index fb25e609..4bf3f928 100644 --- a/rainbowgum-systemlogger/src/main/java/io/jstach/rainbowgum/systemlogger/RainbowGumSystemLoggerFinder.java +++ b/rainbowgum-systemlogger/src/main/java/io/jstach/rainbowgum/systemlogger/RainbowGumSystemLoggerFinder.java @@ -95,6 +95,10 @@ protected RainbowGumSystemLoggerFinder(InitOption opt) { @Override public Logger getLogger(String name, Module module) { var router = routerProvider.router(name); + if (!router.isChangeable(name)) { + var level = router.levelResolver().resolveLevel(name); + return LevelSystemLogger.of(name, level, router.route(name, level)); + } return RainbowGumSystemLogger.of(name, router); } @@ -113,7 +117,7 @@ protected static InitOption initOption(LogProperties properties) { private interface RouterProvider { - LogRouter router(String loggerName); + LogRouter.RootRouter router(String loggerName); } @@ -129,8 +133,8 @@ public InitRouterProvider(Supplier supplier) { } @Override - public LogRouter router(String loggerName) { - LogRouter router; + public LogRouter.RootRouter router(String loggerName) { + LogRouter.RootRouter router; RainbowGum gum = this.gum; if (gum == null) { gum = this.gum = supplier.get(); @@ -139,11 +143,12 @@ public LogRouter router(String loggerName) { router = LogRouter.global(); } else { - var rootRouter = gum.router(); - var levelResolver = rootRouter.levelResolver(); - var level = levelResolver.resolveLevel(loggerName); - var logger = rootRouter.route(loggerName, level); - router = LogRouter.ofLevel(logger, level); + // var rootRouter = gum.router(); + // var levelResolver = rootRouter.levelResolver(); + // var level = levelResolver.resolveLevel(loggerName); + // var logger = rootRouter.route(loggerName, level); + // router = LogRouter.ofLevel(logger, level); + router = gum.router(); } return router; } diff --git a/test/rainbowgum-test-jdk/src/test/java/io/jstach/rainbowgum/test/jdk/JDKSetupTest.java b/test/rainbowgum-test-jdk/src/test/java/io/jstach/rainbowgum/test/jdk/JDKSetupTest.java index 292062af..42acf993 100644 --- a/test/rainbowgum-test-jdk/src/test/java/io/jstach/rainbowgum/test/jdk/JDKSetupTest.java +++ b/test/rainbowgum-test-jdk/src/test/java/io/jstach/rainbowgum/test/jdk/JDKSetupTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.params.provider.MethodSource; import io.jstach.rainbowgum.LevelResolver; +import io.jstach.rainbowgum.LogFormatter; import io.jstach.rainbowgum.LogFormatter.LevelFormatter; import io.jstach.rainbowgum.LogProperties.MutableLogProperties; import io.jstach.rainbowgum.LogProperties; @@ -39,6 +40,7 @@ import io.jstach.rainbowgum.jdk.jul.JULConfigurator; import io.jstach.rainbowgum.jdk.systemlogger.SystemLoggingFactory; import io.jstach.rainbowgum.output.ListLogOutput; +import io.jstach.rainbowgum.systemlogger.RainbowGumSystemLoggerFinder.InitOption; @TestMethodOrder(OrderAnnotation.class) class JDKSetupTest { @@ -185,6 +187,32 @@ void testNullRecord(System.Logger.Level loggerLevel) throws InterruptedException }); } + @Order(13) + @Test + void testCheckDefault() throws InterruptedException { + /* + * We do the default of checking if something like SLF4J facade exists. This needs + * to be last as it will load the true System.Logger + */ + System.clearProperty(SystemLoggingFactory.INTIALIZE_RAINBOW_GUM_PROPERTY); + ListLogOutput output = new ListLogOutput(); + try (var gum = RainbowGum.builder().route(r -> { + r.appender("list", a -> { + a.output(output); + a.formatter(LogFormatter.builder().level().space().loggerName().space().message().newline().build()); + }); + }).set()) { + // var logger = System.getLogger("test"); + var logger = new SystemLoggingFactory().getLogger("check.default", null); + logger.log(Level.INFO, "Hello {0} from JUL Logger!", "Gum"); + String expected = """ + INFO check.default Hello Gum from JUL Logger! + """; + String actual = output.toString(); + assertEquals(expected, actual); + } + } + interface LoggerProvider { T logger(String loggerName); @@ -305,7 +333,13 @@ enum SystemLoggerTester implements LoggerTester { STATIC_GUM_SYSTEM_LOGGER() { @Override public java.lang.System.Logger logger(String loggerName) { - return new SystemLoggingFactory().getLogger(loggerName, null); + var logger = new SystemLoggingFactory().getLogger(loggerName, null); + /* + * Check for static logger. + */ + assertTrue( + logger.getClass().getName().startsWith("io.jstach.rainbowgum.systemlogger.LevelSystemLogger")); + return logger; } @Override @@ -323,7 +357,13 @@ public LogProperties properties() { REUSE_GUM_SYSTEM_LOGGER() { @Override public java.lang.System.Logger logger(String loggerName) { - return new SystemLoggingFactory().getLogger(loggerName, null); + var logger = new SystemLoggingFactory().getLogger(loggerName, null); + /* + * Check for static logger. + */ + assertTrue( + logger.getClass().getName().startsWith("io.jstach.rainbowgum.systemlogger.LevelSystemLogger")); + return logger; } @Override @@ -341,7 +381,12 @@ public LogProperties properties() { CHANGEABLE_GUM_SYSTEM_LOGGER() { @Override public java.lang.System.Logger logger(String loggerName) { - return new SystemLoggingFactory().getLogger(loggerName, null); + var logger = new SystemLoggingFactory().getLogger(loggerName, null); + /* + * Check for changeable logger. + */ + assertEquals("io.jstach.rainbowgum.systemlogger.RainbowGumSystemLogger", logger.getClass().getName()); + return logger; } @Override @@ -716,6 +761,8 @@ private static void doInLock(Runnable r) throws InterruptedException { private void _systemLoggingFactory() { + System.setProperty(SystemLoggingFactory.INTIALIZE_RAINBOW_GUM_PROPERTY, InitOption.FALSE.name()); + assertNull(RainbowGum.getOrNull(), "Rainbow Gum should not be loaded yet."); System.setProperty(JULConfigurator.JUL_DISABLE_PROPERTY, "true"); assertFalse(JULConfigurator.install(LogProperties.findGlobalProperties()));