From a8e0534634548b5563f44f636f2448f321608ac0 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 13 Jun 2026 17:01:08 +0200 Subject: [PATCH] style(logging): colorize adaptive console logs Route console log level and logger columns through custom Logback converters so terminals with ANSI support get colored severity badges and compact colored class names. Keep the same habbo.console.style auto/ansi/plain behavior as the startup splash, including plain fallback for non-interactive output, NO_COLOR, and legacy Windows console paths. The file appenders keep their existing verbose patterns unchanged, so debug/error log files remain plain and grep-friendly. Cover the level formatter, logger formatter, override behavior, and Logback pattern wiring with tests. --- .../src/main/java/com/eu/habbo/Emulator.java | 30 +---- .../util/logback/ConsoleLevelConverter.java | 11 ++ .../util/logback/ConsoleLoggerConverter.java | 11 ++ .../eu/habbo/util/logback/ConsoleStyle.java | 106 ++++++++++++++++++ Emulator/src/main/resources/logback.xml | 5 +- .../eu/habbo/ConsoleLogbackLayoutTest.java | 3 +- .../habbo/util/logback/ConsoleStyleTest.java | 49 ++++++++ 7 files changed, 185 insertions(+), 30 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLevelConverter.java create mode 100644 Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLoggerConverter.java create mode 100644 Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleStyle.java create mode 100644 Emulator/src/test/java/com/eu/habbo/util/logback/ConsoleStyleTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 765f676a..26f113ce 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -18,6 +18,7 @@ import com.eu.habbo.plugin.events.emulator.EmulatorStartShutdownEvent; import com.eu.habbo.plugin.events.emulator.EmulatorStoppedEvent; import com.eu.habbo.threading.ThreadPooling; import com.eu.habbo.util.imager.badges.BadgeImager; +import com.eu.habbo.util.logback.ConsoleStyle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -368,34 +369,7 @@ public final class Emulator { } static boolean shouldStyleConsole(Map environment, boolean interactiveConsole, String osName, String styleProperty) { - String style = styleProperty == null ? "auto" : styleProperty.trim().toLowerCase(Locale.ROOT); - if (style.equals("ansi") || style.equals("color") || style.equals("colours") || style.equals("colors")) { - return true; - } - if (style.equals("plain") || style.equals("none") || style.equals("false") || style.equals("off")) { - return false; - } - if (!interactiveConsole) { - return false; - } - - Map env = environment == null ? Collections.emptyMap() : environment; - if (env.containsKey("NO_COLOR")) { - return false; - } - if (env.containsKey("WT_SESSION") || env.containsKey("ANSICON") || "ON".equalsIgnoreCase(env.get("ConEmuANSI"))) { - return true; - } - - String term = env.getOrDefault("TERM", ""); - if (term.equalsIgnoreCase("dumb")) { - return false; - } - if (!term.isBlank() && (term.contains("xterm") || term.contains("ansi") || term.contains("screen") || term.contains("tmux"))) { - return true; - } - - return osName == null || !osName.toLowerCase(Locale.ROOT).startsWith("windows"); + return ConsoleStyle.isEnabled(environment, interactiveConsole, osName, styleProperty); } private static String fit(String value, int width) { diff --git a/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLevelConverter.java b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLevelConverter.java new file mode 100644 index 00000000..43fa820d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLevelConverter.java @@ -0,0 +1,11 @@ +package com.eu.habbo.util.logback; + +import ch.qos.logback.classic.pattern.ClassicConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; + +public class ConsoleLevelConverter extends ClassicConverter { + @Override + public String convert(ILoggingEvent event) { + return ConsoleStyle.level(event == null ? null : event.getLevel(), ConsoleStyle.isRuntimeEnabled()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLoggerConverter.java b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLoggerConverter.java new file mode 100644 index 00000000..61b997d8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLoggerConverter.java @@ -0,0 +1,11 @@ +package com.eu.habbo.util.logback; + +import ch.qos.logback.classic.pattern.ClassicConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; + +public class ConsoleLoggerConverter extends ClassicConverter { + @Override + public String convert(ILoggingEvent event) { + return ConsoleStyle.logger(event == null ? "" : event.getLoggerName(), ConsoleStyle.isRuntimeEnabled()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleStyle.java b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleStyle.java new file mode 100644 index 00000000..7c59c2aa --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleStyle.java @@ -0,0 +1,106 @@ +package com.eu.habbo.util.logback; + +import ch.qos.logback.classic.Level; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +public final class ConsoleStyle { + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_BOLD = "\u001B[1m"; + private static final String ANSI_DIM = "\u001B[2m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_YELLOW = "\u001B[33m"; + private static final String ANSI_RED = "\u001B[31m"; + + private static final int LOGGER_WIDTH = 22; + + private ConsoleStyle() { + } + + public static boolean isRuntimeEnabled() { + return isEnabled( + System.getenv(), + System.console() != null, + System.getProperty("os.name", "Unknown"), + System.getProperty("habbo.console.style", "auto")); + } + + public static boolean isEnabled(Map environment, boolean interactiveConsole, String osName, String styleProperty) { + String style = styleProperty == null ? "auto" : styleProperty.trim().toLowerCase(Locale.ROOT); + if (style.equals("ansi") || style.equals("color") || style.equals("colours") || style.equals("colors")) { + return true; + } + if (style.equals("plain") || style.equals("none") || style.equals("false") || style.equals("off")) { + return false; + } + if (!interactiveConsole) { + return false; + } + + Map env = environment == null ? Collections.emptyMap() : environment; + if (env.containsKey("NO_COLOR")) { + return false; + } + if (env.containsKey("WT_SESSION") || env.containsKey("ANSICON") || "ON".equalsIgnoreCase(env.get("ConEmuANSI"))) { + return true; + } + + String term = env.getOrDefault("TERM", ""); + if (term.equalsIgnoreCase("dumb")) { + return false; + } + if (!term.isBlank() && (term.contains("xterm") || term.contains("ansi") || term.contains("screen") || term.contains("tmux"))) { + return true; + } + + return osName == null || !osName.toLowerCase(Locale.ROOT).startsWith("windows"); + } + + public static String level(Level level, boolean styled) { + String name = level == null ? "INFO" : level.toString(); + String plain = String.format("%-5s", name); + + if (!styled) { + return plain; + } + + if (Level.ERROR.equals(level)) { + return ANSI_BOLD + ANSI_RED + "[x] " + plain + ANSI_RESET; + } + if (Level.WARN.equals(level)) { + return ANSI_YELLOW + "[!] " + plain + ANSI_RESET; + } + if (Level.DEBUG.equals(level) || Level.TRACE.equals(level)) { + return ANSI_DIM + "[.] " + plain + ANSI_RESET; + } + + return ANSI_GREEN + "[i] " + plain + ANSI_RESET; + } + + public static String logger(String loggerName, boolean styled) { + String compact = compactLoggerName(loggerName); + String plain = fit(compact, LOGGER_WIDTH); + return styled ? ANSI_CYAN + plain + ANSI_RESET : plain; + } + + private static String compactLoggerName(String loggerName) { + if (loggerName == null || loggerName.isBlank()) { + return ""; + } + + int lastDot = loggerName.lastIndexOf('.'); + return lastDot >= 0 ? loggerName.substring(lastDot + 1) : loggerName; + } + + private static String fit(String value, int width) { + String safe = value == null ? "" : value; + if (safe.length() > width) { + return safe.substring(0, Math.max(0, width - 3)) + "..."; + } + + return String.format("%-" + width + "s", safe); + } +} diff --git a/Emulator/src/main/resources/logback.xml b/Emulator/src/main/resources/logback.xml index 5a972548..5235b60a 100644 --- a/Emulator/src/main/resources/logback.xml +++ b/Emulator/src/main/resources/logback.xml @@ -1,8 +1,11 @@ + + + - %d{HH:mm:ss.SSS} %-5level [%-12thread] %-22logger{0} | %msg%n + %d{HH:mm:ss.SSS} %morningstarLevel [%-12thread] %morningstarLogger | %msg%n diff --git a/Emulator/src/test/java/com/eu/habbo/ConsoleLogbackLayoutTest.java b/Emulator/src/test/java/com/eu/habbo/ConsoleLogbackLayoutTest.java index 0f044868..4e0d3724 100644 --- a/Emulator/src/test/java/com/eu/habbo/ConsoleLogbackLayoutTest.java +++ b/Emulator/src/test/java/com/eu/habbo/ConsoleLogbackLayoutTest.java @@ -13,7 +13,8 @@ class ConsoleLogbackLayoutTest { void consolePatternKeepsStartupMessagesReadable() throws Exception { String logback = Files.readString(Path.of("src/main/resources/logback.xml")); - assertTrue(logback.contains("%-22logger{0}"), "console should show compact class names"); + assertTrue(logback.contains("morningstarLevel"), "console should use the adaptive level formatter"); + assertTrue(logback.contains("morningstarLogger"), "console should use the adaptive logger formatter"); assertTrue(logback.contains("| %msg%n"), "console should leave a clear message column"); assertFalse(logback.contains("%-36logger{36}"), "wide package loggers waste console space"); } diff --git a/Emulator/src/test/java/com/eu/habbo/util/logback/ConsoleStyleTest.java b/Emulator/src/test/java/com/eu/habbo/util/logback/ConsoleStyleTest.java new file mode 100644 index 00000000..c62a4e3b --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/util/logback/ConsoleStyleTest.java @@ -0,0 +1,49 @@ +package com.eu.habbo.util.logback; + +import ch.qos.logback.classic.Level; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConsoleStyleTest { + @Test + void formatsLevelWithIconAndColorWhenStyled() { + String formatted = ConsoleStyle.level(Level.WARN, true); + + assertTrue(formatted.contains("\u001B[")); + assertTrue(formatted.contains("[!] WARN ")); + assertTrue(formatted.endsWith("\u001B[0m")); + } + + @Test + void formatsLevelAsPlainTextWhenNotStyled() { + assertEquals("WARN ", ConsoleStyle.level(Level.WARN, false)); + } + + @Test + void formatsLoggerWithColorWhenStyled() { + String formatted = ConsoleStyle.logger("com.eu.habbo.networking.Server", true); + + assertTrue(formatted.contains("\u001B[")); + assertTrue(formatted.contains("Server")); + assertTrue(formatted.endsWith("\u001B[0m")); + } + + @Test + void keepsLoggerPlainAndCompactWhenNotStyled() { + assertEquals("Server ", ConsoleStyle.logger("com.eu.habbo.networking.Server", false)); + } + + @Test + void honorsPlainOverrideEvenInWindowsTerminal() { + assertFalse(ConsoleStyle.isEnabled( + Map.of("WT_SESSION", "abc123"), + true, + "Windows 11", + "plain")); + } +}