diff --git a/clients/src/test/java/org/apache/kafka/common/utils/LogCaptureAppender.java b/clients/src/test/java/org/apache/kafka/common/utils/LogCaptureAppender.java index 2df74c8681c59..b76d2ad4e09eb 100644 --- a/clients/src/test/java/org/apache/kafka/common/utils/LogCaptureAppender.java +++ b/clients/src/test/java/org/apache/kafka/common/utils/LogCaptureAppender.java @@ -109,16 +109,16 @@ public void setClassLogger(final Class clazz, Level level) { LoggerContext ctx = (LoggerContext) LogManager.getContext(false); Configuration config = ctx.getConfiguration(); String loggerName = clazz.getName(); - LoggerConfig loggerConfig = config.getLoggerConfig(loggerName); + org.apache.logging.log4j.core.Logger logger = ctx.getLoggerRegistry().getLogger(loggerName); - Level originalLevel = loggerConfig.getLevel(); + Level originalLevel = logger.getLevel(); logLevelChanges.add(new LogLevelChange(originalLevel, clazz)); - if (!loggerConfig.getName().equals(loggerName)) { + if (!logger.getName().equals(loggerName)) { LoggerConfig newLoggerConfig = new LoggerConfig(loggerName, level, true); config.addLogger(loggerName, newLoggerConfig); } else { - loggerConfig.setLevel(level); + logger.setLevel(level); } ctx.updateLoggers(); } @@ -182,7 +182,7 @@ public void close() { for (final LogLevelChange logLevelChange : logLevelChanges) { String loggerName = logLevelChange.clazz.getName(); - LoggerConfig loggerConfig = config.getLoggerConfig(loggerName); + org.apache.logging.log4j.core.Logger loggerConfig = ctx.getLogger(loggerName); if (!loggerConfig.getName().equals(loggerName)) { LoggerConfig newLoggerConfig = new LoggerConfig(loggerName, logLevelChange.originalLevel, true); config.addLogger(loggerName, newLoggerConfig); diff --git a/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/Loggers.java b/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/Loggers.java index 0c16d0d6f0160..f3c69e3aab1ca 100644 --- a/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/Loggers.java +++ b/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/Loggers.java @@ -23,12 +23,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configurator; -import org.apache.logging.log4j.core.config.LoggerConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -48,14 +46,17 @@ public class Loggers { private static final Logger log = LoggerFactory.getLogger(Loggers.class); + private static final String ROOT_LOGGER_NAME = "root"; + /** * Log4j uses "root" (case-insensitive) as name of the root logger. * Note: In log4j, the root logger's name was "root" and Kafka also followed that name for dynamic logging control feature. * - * The root logger's name is changed in log4j2 to empty string (see: [[LogManager.ROOT_LOGGER_NAME]]) but for backward- - * compatibility. Kafka keeps its original root logger name. It is why here is a dedicated definition for the root logger name. + * While log4j2 changed the root logger's name to empty string (see: [[LogManager.ROOT_LOGGER_NAME]]), + * for backward-compatibility purposes, we accept both empty string and "root" as valid root logger names. + * This is why we have a dedicated definition that includes both values. */ - private static final String ROOT_LOGGER_NAME = "root"; + private static final List VALID_ROOT_LOGGER_NAMES = List.of(LogManager.ROOT_LOGGER_NAME, ROOT_LOGGER_NAME); private final Time time; private final Map lastModifiedTimes; @@ -75,10 +76,10 @@ public synchronized LoggerLevel level(String logger) { Objects.requireNonNull(logger, "Logger may not be null"); org.apache.logging.log4j.Logger foundLogger = null; - if (ROOT_LOGGER_NAME.equalsIgnoreCase(logger)) { + if (isValidRootLoggerName(logger)) { foundLogger = rootLogger(); } else { - List currentLoggers = currentLoggers(); + var currentLoggers = currentLoggers().values(); // search within existing loggers for the given name. // using LogManger.getLogger() will create a logger if it doesn't exist // (potential leak since these don't get cleaned up). @@ -103,18 +104,16 @@ public synchronized LoggerLevel level(String logger) { * @return the levels of all known loggers; may be empty, but never null */ public synchronized Map allLevels() { - Map result = new TreeMap<>(); - - currentLoggers().stream() - .filter(logger -> !logger.getLevel().equals(Level.OFF)) - .forEach(logger -> result.put(logger.getName(), loggerLevel(logger))); - - org.apache.logging.log4j.Logger root = rootLogger(); - if (!root.getLevel().equals(Level.OFF)) { - result.put(ROOT_LOGGER_NAME, loggerLevel(root)); - } - - return result; + return currentLoggers() + .values() + .stream() + .filter(logger -> !logger.getLevel().equals(Level.OFF)) + .collect(Collectors.toMap( + this::getLoggerName, + this::loggerLevel, + (existing, replacing) -> replacing, + TreeMap::new) + ); } /** @@ -129,12 +128,12 @@ public synchronized List setLevel(String namespace, Level level) { Objects.requireNonNull(level, "Level may not be null"); log.info("Setting level of namespace {} and children to {}", namespace, level); - List childLoggers = loggers(namespace); + var loggers = loggers(namespace); List result = new ArrayList<>(); - for (org.apache.logging.log4j.Logger logger: childLoggers) { + for (org.apache.logging.log4j.Logger logger: loggers) { setLevel(logger, level); - result.add(logger.getName()); + result.add(getLoggerName(logger)); } Collections.sort(result); @@ -151,15 +150,13 @@ public synchronized List setLevel(String namespace, Level level) { private synchronized List loggers(String namespace) { Objects.requireNonNull(namespace, "Logging namespace may not be null"); - if (ROOT_LOGGER_NAME.equalsIgnoreCase(namespace)) { - List result = currentLoggers(); - result.add(rootLogger()); - return result; + if (isValidRootLoggerName(namespace)) { + return new ArrayList<>(currentLoggers().values()); } - List result = new ArrayList<>(); - org.apache.logging.log4j.Logger ancestorLogger = lookupLogger(namespace); - List currentLoggers = currentLoggers(); + var result = new ArrayList(); + var ancestorLogger = lookupLogger(namespace); + var currentLoggers = currentLoggers().values(); boolean present = false; for (org.apache.logging.log4j.Logger currentLogger : currentLoggers) { if (currentLogger.getName().startsWith(namespace)) { @@ -179,17 +176,16 @@ private synchronized List loggers(String namesp // visible for testing org.apache.logging.log4j.Logger lookupLogger(String logger) { - return LogManager.getLogger(logger); + return LogManager.getLogger(isValidRootLoggerName(logger) ? LogManager.ROOT_LOGGER_NAME : logger); } - List currentLoggers() { + Map currentLoggers() { LoggerContext context = (LoggerContext) LogManager.getContext(false); - Collection loggerConfigs = context.getConfiguration().getLoggers().values(); - return loggerConfigs.stream() - .map(LoggerConfig::getName) - .distinct() - .map(LogManager::getLogger) - .collect(Collectors.toCollection(ArrayList::new)); + // Make sure root logger has been initialized + var results = new HashMap(); + context.getConfiguration().getLoggers().forEach((name, logger) -> results.put(name, LogManager.getLogger(name))); + context.getLoggerRegistry().getLoggers().forEach(logger -> results.put(logger.getName(), logger)); + return results; } // visible for testing @@ -198,26 +194,37 @@ org.apache.logging.log4j.Logger rootLogger() { } private void setLevel(org.apache.logging.log4j.Logger logger, Level level) { - String loggerName = logger.getName(); - LoggerContext context = (LoggerContext) LogManager.getContext(false); - LoggerConfig loggerConfig = context.getConfiguration().getLoggerConfig(loggerName); - Level currentLevel = loggerConfig.getLevel(); + Level currentLevel = logger.getLevel(); if (level.equals(currentLevel)) { - log.debug("Skipping update for logger {} since its level is already {}", loggerName, level); + log.debug("Skipping update for logger {} since its level is already {}", logger.getName(), level); return; } - log.debug("Setting level of logger {} (excluding children) to {}", loggerName, level); - Configurator.setLevel(loggerName, level); - lastModifiedTimes.put(loggerName, time.milliseconds()); + log.debug("Setting level of logger {} (excluding children) to {}", logger.getName(), level); + Configurator.setLevel(logger.getName(), level); + lastModifiedTimes.put(logger.getName(), time.milliseconds()); } private LoggerLevel loggerLevel(org.apache.logging.log4j.Logger logger) { - LoggerContext context = (LoggerContext) LogManager.getContext(false); - LoggerConfig loggerConfig = context.getConfiguration().getLoggerConfig(logger.getName()); - Level level = loggerConfig.getLevel(); Long lastModified = lastModifiedTimes.get(logger.getName()); - return new LoggerLevel(Objects.toString(level), lastModified); + return new LoggerLevel(Objects.toString(logger.getLevel()), lastModified); + } + + private boolean isValidRootLoggerName(String namespace) { + return VALID_ROOT_LOGGER_NAMES.stream() + .anyMatch(rootLoggerNames -> rootLoggerNames.equalsIgnoreCase(namespace)); + } + + /** + * Converts logger name to ensure backward compatibility between log4j and log4j2. + * If the logger name is empty (log4j2's root logger representation), converts it to "root" (log4j's style). + * Otherwise, returns the original logger name. + * + * @param logger The logger instance to get the name from + * @return The logger name - returns "root" for empty string, otherwise returns the original logger name + */ + private String getLoggerName(org.apache.logging.log4j.Logger logger) { + return logger.getName().equals(LogManager.ROOT_LOGGER_NAME) ? ROOT_LOGGER_NAME : logger.getName(); } } diff --git a/connect/runtime/src/test/java/org/apache/kafka/connect/integration/StandaloneWorkerIntegrationTest.java b/connect/runtime/src/test/java/org/apache/kafka/connect/integration/StandaloneWorkerIntegrationTest.java index f13781c8ceabb..7a48660629518 100644 --- a/connect/runtime/src/test/java/org/apache/kafka/connect/integration/StandaloneWorkerIntegrationTest.java +++ b/connect/runtime/src/test/java/org/apache/kafka/connect/integration/StandaloneWorkerIntegrationTest.java @@ -184,8 +184,7 @@ private Map testSetLoggingLevel( newLevels, e -> hasNamespace(e, namespace) && (!level(e).equals(level) - || !isModified(e) - || lastModified(e) < requestTime + || (isModified(e) && lastModified(e) < requestTime) ) ); assertEquals( diff --git a/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/LoggersTest.java b/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/LoggersTest.java index 23e5f753cdeb2..8bae78bd90f28 100644 --- a/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/LoggersTest.java +++ b/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/LoggersTest.java @@ -20,248 +20,164 @@ import org.apache.kafka.common.utils.Time; import org.apache.kafka.connect.runtime.rest.entities.LoggerLevel; -import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.Configurator; -import org.apache.logging.log4j.core.config.LoggerConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import static org.apache.logging.log4j.Level.DEBUG; +import static org.apache.logging.log4j.Level.ERROR; +import static org.apache.logging.log4j.Level.INFO; +import static org.apache.logging.log4j.Level.WARN; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.STRICT_STUBS) public class LoggersTest { - private static final long INITIAL_TIME = 1696951712135L; + private final LoggerContext context = (LoggerContext) LogManager.getContext(false); + private Loggers loggers; private Time time; @BeforeEach public void setup() { + context.stop(); + context.start(); + context.reconfigure(); time = new MockTime(0, INITIAL_TIME, 0); + loggers = new Loggers(time); } @Test - public void testGetLoggersIgnoresNullLevels() { - LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); - Logger root = loggerContext.getRootLogger(); - Configurator.setLevel(root, Level.OFF); - - Logger a = loggerContext.getLogger("a"); - Configurator.setLevel(a, null); - - Logger b = loggerContext.getLogger("b"); - Configurator.setLevel(b, Level.INFO); - - Loggers loggers = new TestLoggers(root, a, b); + public void testLevelWithNullLoggerName() { + NullPointerException exception = assertThrows(NullPointerException.class, () -> loggers.level(null)); + assertEquals("Logger may not be null", exception.getMessage()); + } - Map expectedLevels = Collections.singletonMap( - "b", - new LoggerLevel(Level.INFO.toString(), null) + @Test + public void testLevelWithValidRootLoggerNames() { + assertEquals( + loggers.level(""), + loggers.level("root"), + "Root logger level should be the same whether accessed via empty string or 'root' name" ); - Map actualLevels = loggers.allLevels(); - assertEquals(expectedLevels, actualLevels); } @Test - public void testGetLoggerFallsBackToEffectiveLogLevel() { - LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); - Logger root = loggerContext.getRootLogger(); - Configurator.setLevel(root, Level.ERROR); - - Logger a = loggerContext.getLogger("a"); - Configurator.setLevel(a, null); - - Logger b = loggerContext.getLogger("b"); - Configurator.setLevel(b, Level.INFO); + public void testLevelWithExistLoggerName() { + loggers.setLevel("foo", DEBUG); + assertEquals(new LoggerLevel(DEBUG.name(), INITIAL_TIME), + loggers.level("foo") + ); + } - Loggers loggers = new TestLoggers(root, a, b); + @Test + public void testLevelWithNonExistLoggerName() { + assertNull(loggers.level("dummy"), "Unconfigured logger should return null"); + } - LoggerLevel expectedLevel = new LoggerLevel(Level.ERROR.toString(), null); - LoggerLevel actualLevel = loggers.level("a"); - assertEquals(expectedLevel, actualLevel); + @Test + public void testLevelWithNewlyCreatedLogger() { + loggers.setLevel("dummy", WARN); + assertEquals( + new LoggerLevel(WARN.name(), time.milliseconds()), + loggers.level("dummy"), + "Newly created logger should have the level we just set" + ); } @Test - public void testGetUnknownLogger() { - LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); - Logger root = loggerContext.getRootLogger(); - Configurator.setLevel(root, Level.ERROR); + public void testAllLevelsAfterCreatingNewLogger() { + loggers.setLevel("foo", WARN); + loggers.setLevel("bar", ERROR); + Map loggerToLevel = loggers.allLevels(); + Map expectedLevels = Map.of( + "foo", new LoggerLevel(WARN.name(), INITIAL_TIME), + "bar", new LoggerLevel(ERROR.name(), INITIAL_TIME) + ); - Logger a = loggerContext.getLogger("a"); - Configurator.setLevel(a, null); + assertTrue(loggerToLevel.entrySet().containsAll(expectedLevels.entrySet())); + } - Logger b = loggerContext.getLogger("b"); - Configurator.setLevel(b, Level.INFO); + @Test + public void testSetLevelWithNullNameSpaceOrNullLevel() { + NullPointerException exception = assertThrows(NullPointerException.class, () -> loggers.setLevel(null, null)); + assertEquals("Logging namespace may not be null", exception.getMessage()); - Loggers loggers = new TestLoggers(root, a, b); + exception = assertThrows(NullPointerException.class, () -> loggers.setLevel("dummy", null)); + assertEquals("Level may not be null", exception.getMessage()); + } - LoggerLevel level = loggers.level("c"); - assertNull(level); + @Test + public void testSetLevelWithValidRootLoggerNames() { + assertEquals(loggers.setLevel("", ERROR), loggers.setLevel("root", ERROR)); + assertTrue(loggers.setLevel("", ERROR).contains("root")); } @Test public void testSetLevel() { - LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); - Logger root = loggerContext.getRootLogger(); - Configurator.setLevel(root, Level.ERROR); - - Logger x = loggerContext.getLogger("a.b.c.p.X"); - Logger y = loggerContext.getLogger("a.b.c.p.Y"); - Logger z = loggerContext.getLogger("a.b.c.p.Z"); - Logger w = loggerContext.getLogger("a.b.c.s.W"); - Configurator.setLevel(x, Level.INFO); - Configurator.setLevel(y, Level.INFO); - Configurator.setLevel(z, Level.INFO); - Configurator.setLevel(w, Level.INFO); - - // We don't explicitly register a logger for a.b.c.p, so it won't appear in the list of current loggers; - // one should be created by the Loggers instance when we set the level - TestLoggers loggers = new TestLoggers(root, x, y, z, w); - - List modified = loggers.setLevel("a.b.c.p", Level.DEBUG); - assertEquals(Arrays.asList("a.b.c.p", "a.b.c.p.X", "a.b.c.p.Y", "a.b.c.p.Z"), modified); - assertEquals(Level.DEBUG.toString(), loggers.level("a.b.c.p").level()); - assertEquals(Level.DEBUG, x.getLevel()); - assertEquals(Level.DEBUG, y.getLevel()); - assertEquals(Level.DEBUG, z.getLevel()); - - LoggerLevel expectedLevel = new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME); - LoggerLevel actualLevel = loggers.level("a.b.c.p"); - assertEquals(expectedLevel, actualLevel); - - // Sleep a little and adjust the level of a leaf logger - time.sleep(10); - loggers.setLevel("a.b.c.p.X", Level.ERROR); - expectedLevel = new LoggerLevel(Level.ERROR.toString(), INITIAL_TIME + 10); - actualLevel = loggers.level("a.b.c.p.X"); - assertEquals(expectedLevel, actualLevel); - - // Make sure that the direct parent logger and a sibling logger remain unaffected - expectedLevel = new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME); - actualLevel = loggers.level("a.b.c.p"); - assertEquals(expectedLevel, actualLevel); - - expectedLevel = new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME); - actualLevel = loggers.level("a.b.c.p.Y"); - assertEquals(expectedLevel, actualLevel); - - // Set the same level again, and verify that the last modified time hasn't been altered - time.sleep(10); - loggers.setLevel("a.b.c.p.X", Level.ERROR); - expectedLevel = new LoggerLevel(Level.ERROR.toString(), INITIAL_TIME + 10); - actualLevel = loggers.level("a.b.c.p.X"); - assertEquals(expectedLevel, actualLevel); + loggers.setLevel("a.b.c", DEBUG); + loggers.setLevel("a.b", ERROR); + loggers.setLevel("a", WARN); + Map expected = Map.of( + "a", new LoggerLevel(WARN.name(), INITIAL_TIME), + "a.b", new LoggerLevel(WARN.name(), INITIAL_TIME), + "a.b.c", new LoggerLevel(WARN.name(), INITIAL_TIME) + ); + assertTrue(loggers.allLevels().entrySet().containsAll(expected.entrySet())); } @Test - public void testSetRootLevel() { - // In this test case, we focus on setting the level for the root logger. - // Ideally, we want to start with a "clean" configuration to conduct this test case. - // By programmatically creating a new configuration at the beginning, we can ensure - // that this test case is not affected by existing Log4j configurations. - LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); - Configuration config = loggerContext.getConfiguration(); - String rootLoggerName = "root"; - LoggerConfig rootConfig = new LoggerConfig(rootLoggerName, Level.ERROR, false); - config.addLogger(rootLoggerName, rootConfig); - loggerContext.updateLoggers(); - - Logger root = LogManager.getLogger(rootLoggerName); - Configurator.setLevel(root, Level.ERROR); - - Logger p = loggerContext.getLogger("a.b.c.p"); - Logger x = loggerContext.getLogger("a.b.c.p.X"); - Logger y = loggerContext.getLogger("a.b.c.p.Y"); - Logger z = loggerContext.getLogger("a.b.c.p.Z"); - Logger w = loggerContext.getLogger("a.b.c.s.W"); - Configurator.setLevel(p, Level.INFO); - Configurator.setLevel(x, Level.INFO); - Configurator.setLevel(y, Level.INFO); - Configurator.setLevel(z, Level.INFO); - Configurator.setLevel(w, Level.INFO); - - Loggers loggers = new TestLoggers(root, x, y, z, w); - - List modified = loggers.setLevel(rootLoggerName, Level.DEBUG); - assertEquals(Arrays.asList("a.b.c.p.X", "a.b.c.p.Y", "a.b.c.p.Z", "a.b.c.s.W", rootLoggerName), modified); - - assertEquals(p.getLevel(), Level.INFO); - - assertEquals(root.getLevel(), Level.DEBUG); - - assertEquals(w.getLevel(), Level.DEBUG); - assertEquals(x.getLevel(), Level.DEBUG); - assertEquals(y.getLevel(), Level.DEBUG); - assertEquals(z.getLevel(), Level.DEBUG); - - Map expectedLevels = new HashMap<>(); - expectedLevels.put(rootLoggerName, new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME)); - expectedLevels.put("a.b.c.p.X", new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME)); - expectedLevels.put("a.b.c.p.Y", new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME)); - expectedLevels.put("a.b.c.p.Z", new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME)); - expectedLevels.put("a.b.c.s.W", new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME)); - - Map actualLevels = loggers.allLevels(); - assertEquals(expectedLevels, actualLevels); + public void testLookupLoggerAfterCreatingNewLogger() { + loggers.setLevel("dummy", INFO); + Logger logger = loggers.lookupLogger("dummy"); + assertNotNull(logger); + assertEquals(INFO, logger.getLevel()); } @Test - public void testSetLevelNullArguments() { - LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); - Logger root = loggerContext.getRootLogger(); - Loggers loggers = new TestLoggers(root); - assertThrows(NullPointerException.class, () -> loggers.setLevel(null, Level.INFO)); - assertThrows(NullPointerException.class, () -> loggers.setLevel("root", null)); + public void testSetLevelWithSameLevel() { + String loggerName = "dummy"; + loggers.setLevel(loggerName, DEBUG); + time.sleep(100); + loggers.setLevel(loggerName, DEBUG); + assertEquals( + new LoggerLevel(DEBUG.name(), INITIAL_TIME), + loggers.allLevels().get(loggerName), + "Setting same log level should not update the lastModified timestamp" + ); } - private class TestLoggers extends Loggers { - - private final Logger rootLogger; - private final Map currentLoggers; - - public TestLoggers(Logger rootLogger, Logger... knownLoggers) { - super(time); - this.rootLogger = rootLogger; - this.currentLoggers = new HashMap<>(Stream.of(knownLoggers) - .collect(Collectors.toMap( - Logger::getName, - Function.identity() - ))); - } - - @Override - Logger lookupLogger(String logger) { - return currentLoggers.computeIfAbsent(logger, LogManager::getLogger); - } + @Test + public void testSetLevelWithDifferentLevels() { + String loggerName = "dummy"; + loggers.setLevel(loggerName, DEBUG); + time.sleep(100); + loggers.setLevel(loggerName, WARN); + assertEquals( + new LoggerLevel(WARN.name(), INITIAL_TIME + 100), + loggers.allLevels().get(loggerName), + "Setting different log level should update the lastModified timestamp" + ); + } - @Override - List currentLoggers() { - return new ArrayList<>(currentLoggers.values()); - } + @Test + public void testLookupLoggerWithValidRootLoggerNames() { + assertEquals( + loggers.lookupLogger("root"), + loggers.lookupLogger(""), + "Both 'root' and empty string should retrieve the root logger" + ); - @Override - Logger rootLogger() { - return rootLogger; - } + assertEquals( + loggers.lookupLogger(""), + loggers.rootLogger(), + "Empty string lookup should match direct root logger access" + ); } } diff --git a/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/MockLoggersTest.java b/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/MockLoggersTest.java new file mode 100644 index 0000000000000..a7d747ca4731e --- /dev/null +++ b/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/MockLoggersTest.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.kafka.connect.runtime; + +import org.apache.kafka.common.utils.MockTime; +import org.apache.kafka.common.utils.Time; +import org.apache.kafka.connect.runtime.rest.entities.LoggerLevel; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.STRICT_STUBS) +public class MockLoggersTest { + + private static final long INITIAL_TIME = 1696951712135L; + private Time time; + + @BeforeEach + public void setup() { + time = new MockTime(0, INITIAL_TIME, 0); + } + + @Test + public void testGetLoggersIgnoresNullLevels() { + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + Logger root = loggerContext.getRootLogger(); + Configurator.setLevel(root, Level.OFF); + + Logger a = loggerContext.getLogger("a"); + Configurator.setLevel(a, null); + + Logger b = loggerContext.getLogger("b"); + Configurator.setLevel(b, Level.INFO); + + Loggers loggers = new TestLoggers(root, a, b); + + Map expectedLevels = Collections.singletonMap( + "b", + new LoggerLevel(Level.INFO.toString(), null) + ); + Map actualLevels = loggers.allLevels(); + assertEquals(expectedLevels, actualLevels); + } + + @Test + public void testGetLoggerFallsBackToEffectiveLogLevel() { + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + Logger root = loggerContext.getRootLogger(); + Configurator.setLevel(root, Level.ERROR); + + Logger a = loggerContext.getLogger("a"); + Configurator.setLevel(a, null); + + Logger b = loggerContext.getLogger("b"); + Configurator.setLevel(b, Level.INFO); + + Loggers loggers = new TestLoggers(root, a, b); + + LoggerLevel expectedLevel = new LoggerLevel(Level.ERROR.toString(), null); + LoggerLevel actualLevel = loggers.level("a"); + assertEquals(expectedLevel, actualLevel); + } + + @Test + public void testGetUnknownLogger() { + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + Logger root = loggerContext.getRootLogger(); + Configurator.setLevel(root, Level.ERROR); + + Logger a = loggerContext.getLogger("a"); + Configurator.setLevel(a, null); + + Logger b = loggerContext.getLogger("b"); + Configurator.setLevel(b, Level.INFO); + + Loggers loggers = new TestLoggers(root, a, b); + + LoggerLevel level = loggers.level("c"); + assertNull(level); + } + + @Test + public void testSetLevel() { + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + Logger root = loggerContext.getRootLogger(); + Configurator.setLevel(root, Level.ERROR); + + Logger x = loggerContext.getLogger("a.b.c.p.X"); + Logger y = loggerContext.getLogger("a.b.c.p.Y"); + Logger z = loggerContext.getLogger("a.b.c.p.Z"); + Logger w = loggerContext.getLogger("a.b.c.s.W"); + Configurator.setLevel(x, Level.INFO); + Configurator.setLevel(y, Level.INFO); + Configurator.setLevel(z, Level.INFO); + Configurator.setLevel(w, Level.INFO); + + // We don't explicitly register a logger for a.b.c.p, so it won't appear in the list of current loggers; + // one should be created by the Loggers instance when we set the level + TestLoggers loggers = new TestLoggers(root, x, y, z, w); + + List modified = loggers.setLevel("a.b.c.p", Level.DEBUG); + assertEquals(Arrays.asList("a.b.c.p", "a.b.c.p.X", "a.b.c.p.Y", "a.b.c.p.Z"), modified); + assertEquals(Level.DEBUG.toString(), loggers.level("a.b.c.p").level()); + assertEquals(Level.DEBUG, x.getLevel()); + assertEquals(Level.DEBUG, y.getLevel()); + assertEquals(Level.DEBUG, z.getLevel()); + + LoggerLevel expectedLevel = new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME); + LoggerLevel actualLevel = loggers.level("a.b.c.p"); + assertEquals(expectedLevel, actualLevel); + + // Sleep a little and adjust the level of a leaf logger + time.sleep(10); + loggers.setLevel("a.b.c.p.X", Level.ERROR); + expectedLevel = new LoggerLevel(Level.ERROR.toString(), INITIAL_TIME + 10); + actualLevel = loggers.level("a.b.c.p.X"); + assertEquals(expectedLevel, actualLevel); + + // Make sure that the direct parent logger and a sibling logger remain unaffected + expectedLevel = new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME); + actualLevel = loggers.level("a.b.c.p"); + assertEquals(expectedLevel, actualLevel); + + expectedLevel = new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME); + actualLevel = loggers.level("a.b.c.p.Y"); + assertEquals(expectedLevel, actualLevel); + + // Set the same level again, and verify that the last modified time hasn't been altered + time.sleep(10); + loggers.setLevel("a.b.c.p.X", Level.ERROR); + expectedLevel = new LoggerLevel(Level.ERROR.toString(), INITIAL_TIME + 10); + actualLevel = loggers.level("a.b.c.p.X"); + assertEquals(expectedLevel, actualLevel); + } + + @Test + public void testSetRootLevel() { + // In this test case, we focus on setting the level for the root logger. + // Ideally, we want to start with a "clean" configuration to conduct this test case. + // By programmatically creating a new configuration at the beginning, we can ensure + // that this test case is not affected by existing Log4j configurations. + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + Configuration config = loggerContext.getConfiguration(); + String rootLoggerName = "root"; + LoggerConfig rootConfig = new LoggerConfig(rootLoggerName, Level.ERROR, false); + config.addLogger(rootLoggerName, rootConfig); + loggerContext.updateLoggers(); + + Logger root = LogManager.getLogger(rootLoggerName); + Configurator.setLevel(root, Level.ERROR); + + Logger p = loggerContext.getLogger("a.b.c.p"); + Logger x = loggerContext.getLogger("a.b.c.p.X"); + Logger y = loggerContext.getLogger("a.b.c.p.Y"); + Logger z = loggerContext.getLogger("a.b.c.p.Z"); + Logger w = loggerContext.getLogger("a.b.c.s.W"); + Configurator.setLevel(p, Level.INFO); + Configurator.setLevel(x, Level.INFO); + Configurator.setLevel(y, Level.INFO); + Configurator.setLevel(z, Level.INFO); + Configurator.setLevel(w, Level.INFO); + + Loggers loggers = new TestLoggers(root, x, y, z, w); + + List modified = loggers.setLevel(rootLoggerName, Level.DEBUG); + assertEquals(Arrays.asList("a.b.c.p.X", "a.b.c.p.Y", "a.b.c.p.Z", "a.b.c.s.W", rootLoggerName), modified); + + assertEquals(Level.INFO, p.getLevel()); + + assertEquals(Level.DEBUG, root.getLevel()); + + assertEquals(Level.DEBUG, w.getLevel()); + assertEquals(Level.DEBUG, x.getLevel()); + assertEquals(Level.DEBUG, y.getLevel()); + assertEquals(Level.DEBUG, z.getLevel()); + + Map expectedLevels = Map.of( + "a.b.c.p.X", new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME), + "a.b.c.p.Y", new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME), + "a.b.c.p.Z", new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME), + "a.b.c.s.W", new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME), + rootLoggerName, new LoggerLevel(Level.DEBUG.toString(), INITIAL_TIME) + ); + + Map actualLevels = loggers.allLevels(); + assertEquals(expectedLevels, actualLevels); + } + + @Test + public void testSetLevelNullArguments() { + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + Logger root = loggerContext.getRootLogger(); + Loggers loggers = new TestLoggers(root); + assertThrows(NullPointerException.class, () -> loggers.setLevel(null, Level.INFO)); + assertThrows(NullPointerException.class, () -> loggers.setLevel("root", null)); + } + + private class TestLoggers extends Loggers { + + private final Logger rootLogger; + private final Map currentLoggers; + + public TestLoggers(Logger rootLogger, Logger... knownLoggers) { + super(time); + this.rootLogger = rootLogger; + this.currentLoggers = new HashMap<>(Stream.of(knownLoggers) + .collect(Collectors.toMap( + Logger::getName, + Function.identity() + ))); + this.currentLoggers.put(rootLogger.getName(), rootLogger); + } + + @Override + Logger lookupLogger(String logger) { + return currentLoggers.computeIfAbsent(logger, LogManager::getLogger); + } + + @Override + Map currentLoggers() { + return currentLoggers; + } + + @Override + Logger rootLogger() { + return rootLogger; + } + } +} diff --git a/tests/kafkatest/tests/connect/connect_distributed_test.py b/tests/kafkatest/tests/connect/connect_distributed_test.py index e54118c3881be..d50026da87179 100644 --- a/tests/kafkatest/tests/connect/connect_distributed_test.py +++ b/tests/kafkatest/tests/connect/connect_distributed_test.py @@ -550,7 +550,8 @@ def test_dynamic_logging(self, metadata_quorum): for logger, level in all_loggers.items(): if not logger.startswith(namespace): assert level['level'] == new_root - assert root_request_time <= level['last_modified'] < request_time + if level['last_modified'] is not None: + assert root_request_time <= level['last_modified'] < request_time # Verify that the last worker-scoped request we issued had no effect on other # workers in the cluster @@ -563,7 +564,14 @@ def test_dynamic_logging(self, metadata_quorum): # have been discarded self._restart_worker(worker) restarted_loggers = self.cc.get_all_loggers(worker) - assert initial_loggers == restarted_loggers + + for loggerName in restarted_loggers: + logger = self.cc.get_logger(worker, loggerName) + level = logger['level'] + if loggerName == 'org.apache.kafka.clients.consumer.ConsumerConfig': + assert level == 'ERROR' + else: + assert level == 'DEBUG' def _different_level(self, current_level): return 'INFO' if current_level is None or current_level.upper() != 'INFO' else 'WARN'