001/* 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved. 004 * 005 * This program and the accompanying materials are dual-licensed under 006 * either the terms of the Eclipse Public License v2.0 as published by 007 * the Eclipse Foundation 008 * 009 * or (per the licensee's choosing) 010 * 011 * under the terms of the GNU Lesser General Public License version 2.1 012 * as published by the Free Software Foundation. 013 */ 014package ch.qos.logback.core; 015 016import java.io.OutputStream; 017 018import org.jline.jansi.AnsiConsole; 019 020import ch.qos.logback.core.joran.spi.ConsoleTarget; 021 022import static ch.qos.logback.core.util.Loader.isClassLoadable; 023 024/** 025 * A {@link ConsoleAppender} that always writes through JLine's 026 * {@link AnsiConsole}, enabling ANSI sequences on platforms that need Jansi 027 * (notably Windows). 028 * <p> 029 * Unlike {@link ConsoleAppender}'s deprecated {@code withJansi} path, this 030 * class overrides {@link #wrapTarget(OutputStream)} and calls 031 * {@link AnsiConsole} directly (no reflection). It requires 032 * {@code org.jline:jansi-core} on the classpath. 033 * </p> 034 * <p> 035 * {@link AnsiConsole#systemInstall()} is paired with 036 * {@link AnsiConsole#systemUninstall()} on {@link #stop()} when this appender 037 * performed the install. Console streams are still only flushed on stop (not 038 * closed); see {@link ConsoleAppender#closeOutputStream()}. 039 * </p> 040 * 041 * @param <E> the type of logging events 042 * @author Ceki Gülcü 043 * @since 1.6.3 044 * @see AnsiConsole 045 * @see ConsoleAppender#wrapTarget(OutputStream) 046 */ 047public class JansiConsoleAppender<E> extends ConsoleAppender<E> { 048 049 050 static final String JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME = "org.jline.jansi.AnsiConsole"; 051 /** 052 * Status message emitted when {@link #JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME} 053 * cannot be loaded. The appender then falls back on the raw console stream. 054 */ 055 static final String JANSI_NOT_LOADABLE_MSG0 = "Could not find " + JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME 056 + " on the class path. Falling back on the default stream."; 057 058 static final String JANSI_NOT_LOADABLE_MSG1= "To enable JANSI, add org.jline:jansi-core to the class path."; 059 static final String JANSI_NOT_LOADABLE_MSG2= "See also "+CoreConstants.CODES_URL+"#missingJlineJansi"; 060 061 /** 062 * True after this instance has successfully called 063 * {@link AnsiConsole#systemInstall()} and until the matching 064 * {@link AnsiConsole#systemUninstall()} on {@link #stop()}. 065 */ 066 private boolean installedByThisAppender; 067 068 /** 069 * Flushes the console stream (via {@link ConsoleAppender#stop()}), then 070 * undoes {@link AnsiConsole#systemInstall()} if this appender performed it. 071 */ 072 @Override 073 public void stop() { 074 try { 075 super.stop(); 076 } finally { 077 uninstallAnsiConsoleIfInstalledByThisAppender(); 078 } 079 } 080 081 /** 082 * Installs Jansi and returns {@link AnsiConsole#out()} or 083 * {@link AnsiConsole#err()} according to the configured target. 084 * <p> 085 * Does not use the deprecated {@code withJansi} / {@code wrapWithJansi} 086 * path. {@link AnsiConsole#systemInstall()} is invoked at most once per 087 * install ownership of this instance. 088 * </p> 089 * <p> 090 * If {@link #JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME} is not loadable, a warning 091 * is emitted and {@code targetStream} is returned unchanged. 092 * </p> 093 */ 094 @Override 095 protected OutputStream wrapTarget(OutputStream targetStream) { 096 boolean jansiLoadable = isClassLoadable(JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME, getContext()); 097 if (!jansiLoadable) { 098 addWarn(JANSI_NOT_LOADABLE_MSG0); 099 addWarn(JANSI_NOT_LOADABLE_MSG1); 100 addWarn(JANSI_NOT_LOADABLE_MSG2); 101 return targetStream; 102 } 103 try { 104 addInfo("Enabling JANSI AnsiPrintStream via " + JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME + "."); 105 if (!installedByThisAppender) { 106 AnsiConsole.systemInstall(); 107 installedByThisAppender = true; 108 } 109 if (target == ConsoleTarget.SystemErr) { 110 return AnsiConsole.err(); 111 } else { 112 return AnsiConsole.out(); 113 } 114 } catch (Exception e) { 115 addWarn("Failed to create AnsiPrintStream. Falling back on the default stream.", e); 116 return targetStream; 117 } 118 } 119 120 private void uninstallAnsiConsoleIfInstalledByThisAppender() { 121 if (!installedByThisAppender) { 122 return; 123 } 124 installedByThisAppender = false; 125 try { 126 AnsiConsole.systemUninstall(); 127 addInfo("Uninstalled JANSI AnsiConsole previously installed by this appender."); 128 } catch (RuntimeException e) { 129 addWarn("Failed to uninstall AnsiConsole.", e); 130 } 131 } 132 133}