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.
This commit is contained in:
simoleo89
2026-06-13 17:01:08 +02:00
parent 98e366dd07
commit a8e0534634
7 changed files with 185 additions and 30 deletions
@@ -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<String, String> 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<String, String> 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) {
@@ -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());
}
}
@@ -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());
}
}
@@ -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<String, String> 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<String, String> 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);
}
}
+4 -1
View File
@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<conversionRule conversionWord="morningstarLevel" class="com.eu.habbo.util.logback.ConsoleLevelConverter" />
<conversionRule conversionWord="morningstarLogger" class="com.eu.habbo.util.logback.ConsoleLoggerConverter" />
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%-12thread] %-22logger{0} | %msg%n</pattern>
<pattern>%d{HH:mm:ss.SSS} %morningstarLevel [%-12thread] %morningstarLogger | %msg%n</pattern>
</encoder>
</appender>
@@ -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");
}
@@ -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"));
}
}