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.IOException; 017import java.io.OutputStream; 018import java.io.PrintStream; 019import java.lang.reflect.Method; 020import java.lang.reflect.Modifier; 021import java.util.Arrays; 022import java.util.NoSuchElementException; 023import java.util.Optional; 024 025import ch.qos.logback.core.joran.spi.ConsoleTarget; 026import ch.qos.logback.core.status.ErrorStatus; 027import ch.qos.logback.core.status.Status; 028import ch.qos.logback.core.status.WarnStatus; 029import ch.qos.logback.core.util.Loader; 030import ch.qos.logback.core.util.ReentryGuard; 031import ch.qos.logback.core.util.ReentryGuardFactory; 032 033/** 034 * ConsoleAppender appends log events to <code>System.out</code> or 035 * <code>System.err</code> using a layout specified by the user. The default 036 * target is <code>System.out</code>. 037 * <p> 038 * 039 * </p> 040 * For more information about this appender, please refer to the online manual 041 * at http://logback.qos.ch/manual/appenders.html#ConsoleAppender 042 * <p> 043 * This appender does not own the console streams it writes to. On stop it 044 * flushes those streams but does not close them, so process-wide stdout/stderr 045 * (and Jansi streams wrapping them) remain usable after reconfiguration. 046 * </p> 047 * 048 * @author Ceki Gülcü 049 * @author Tom SH Liu 050 * @author Ruediger Dohna 051 * @see <a href="https://github.com/qos-ch/logback/issues/1063">logback issue #1063</a> 052 */ 053 054public class ConsoleAppender<E> extends OutputStreamAppender<E> { 055 056 protected ConsoleTarget target = ConsoleTarget.SystemOut; 057 protected boolean withJansi = false; 058 059 060 public final static String JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME = "org.jline.jansi.AnsiConsole"; 061 public final static String FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME = "org.fusesource.jansi.AnsiConsole"; 062 063 // Jansi was migrated from FuseSource (org.fusesource.jansi) to JLine (org.jline.jansi), which 064 // changed the package of AnsiConsole. Probe the JLine coordinates first, then fall back to the 065 // legacy FuseSource ones so that <withJansi> keeps working with both artifacts. See LOGBACK issue 1043. 066 private final static String[] ANSI_CONSOLE_CLASS_NAMES = { JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME, 067 FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME }; 068 069 protected String preferredJansiClassName = null; 070 071 private final static String JANSI2_OUT_METHOD_NAME = "out"; 072 private final static String JANSI2_ERR_METHOD_NAME = "err"; 073 private final static String WRAP_SYSTEM_OUT_METHOD_NAME = "wrapSystemOut"; 074 private final static String WRAP_SYSTEM_ERR_METHOD_NAME = "wrapSystemErr"; 075 private final static String SYSTEM_INSTALL_METHOD_NAME = "systemInstall"; 076 private final static Class<?>[] ARGUMENT_TYPES = { PrintStream.class }; 077 078 private final static String CONSOLE_APPENDER_WARNING_URL = CoreConstants.CODES_URL+"#slowConsole"; 079 080 /** 081 * Deprecation notice for {@link #setWithJansi(boolean)} / {@code <withJansi>}. 082 * {@link JansiConsoleAppender} is the recommended replacement. 083 */ 084 static final String WITH_JANSI_DEPRECATED_MSG = 085 "ConsoleAppender.withJansi is deprecated and will be removed in a future release. " 086 + "Use ch.qos.logback.core.JansiConsoleAppender instead."; 087 088 /** 089 * Sets the value of the <b>Target</b> option. Recognized values are 090 * "System.out" and "System.err". Any other value will be ignored. 091 */ 092 public void setTarget(String value) { 093 ConsoleTarget t = ConsoleTarget.findByName(value.trim()); 094 if (t == null) { 095 targetWarn(value); 096 } else { 097 target = t; 098 } 099 } 100 101 /** 102 * Returns the current value of the <b>target</b> property. The default value of 103 * the option is "System.out". 104 * <p> 105 * See also {@link #setTarget}. 106 */ 107 public String getTarget() { 108 return target.getName(); 109 } 110 111 /** 112 * 113 * @return the preferred Jansi class name 114 */ 115 public String getPreferredJansiClassName() { 116 return preferredJansiClassName; 117 } 118 119 /** 120 * It allows to force Jansi class name used for probing. 121 * 122 * <p>Used for testing purposes.</p> 123 * <p>Valid values are {@link #JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME} and 124 * {@link #FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME}.</p> 125 * 126 * @param preferredJansiClassName the preferred Jansi class name 127 * @since 1.6.1 128 */ 129 public void setPreferredJansiClassName(String preferredJansiClassName) { 130 this.preferredJansiClassName = preferredJansiClassName; 131 } 132 133 private boolean isValidPreferredJansiClassName(String className) { 134 return JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME.equals(className) 135 || FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME.equals(className); 136 } 137 138 private void preferredJansiClassNameWarn(String val) { 139 Status status = new WarnStatus( 140 "[" + val + "] should be one of " + Arrays.toString(ANSI_CONSOLE_CLASS_NAMES), this); 141 status.add(new WarnStatus("Ignoring preferredJansiClassName, using default probing order.", this)); 142 addStatus(status); 143 } 144 145 private void targetWarn(String val) { 146 Status status = new WarnStatus("[" + val + "] should be one of " + Arrays.toString(ConsoleTarget.values()), 147 this); 148 status.add(new WarnStatus("Using previously set target, System.out by default.", this)); 149 addStatus(status); 150 } 151 152 @Override 153 public void start() { 154 addInfo("NOTE: Writing to the console can be slow. Try to avoid logging to the "); 155 addInfo("console in production environments, especially in high volume systems."); 156 addInfo("See also "+CONSOLE_APPENDER_WARNING_URL); 157 OutputStream targetStream = wrapTarget(target.getStream()); 158 setOutputStream(targetStream); 159 super.start(); 160 } 161 162 /** 163 * Flush the console target without closing it. 164 * <p> 165 * {@link System#out}, {@link System#err}, and Jansi streams built on 166 * {@code FileDescriptor.out}/{@code err} are process-wide resources. Closing 167 * them from {@link OutputStreamAppender#stop()} would poison stdout for the 168 * rest of the JVM (see 169 * <a href="https://github.com/qos-ch/logback/issues/1063">issue #1063</a>). 170 * The non-Jansi {@link ConsoleTarget} streams already no-op on 171 * {@code close()}; this override keeps the same contract when Jansi is used. 172 * </p> 173 */ 174 @Override 175 protected void closeOutputStream() { 176 if (getOutputStream() != null) { 177 try { 178 // write encoder footer while the stream is still attached 179 encoderClose(); 180 getOutputStream().flush(); 181 } catch (IOException e) { 182 addStatus(new ErrorStatus("Could not flush output stream for ConsoleAppender.", this, e)); 183 } 184 } 185 } 186 187 /** 188 * Create a ThreadLocal ReentryGuard to prevent recursive appender invocations. 189 * @return a ReentryGuard instance of type {@link ReentryGuardFactory.GuardType#THREAD_LOCAL THREAD_LOCAL}. 190 */ 191 protected ReentryGuard buildReentryGuard() { 192 return ReentryGuardFactory.makeGuard(ReentryGuardFactory.GuardType.THREAD_LOCAL); 193 } 194 195 /** 196 * Optionally wraps the raw console target stream before it is used for 197 * logging. 198 * <p> 199 * The default implementation applies the deprecated {@code withJansi} 200 * reflection path when {@link #withJansi} is true. Subclasses such as 201 * {@link JansiConsoleAppender} should override this method to supply an 202 * alternate stream (without relying on {@code withJansi}). 203 * </p> 204 * 205 * @param targetStream the raw stream for {@link #target} 206 * @return the stream to use as this appender's output stream 207 * @since 1.6.3 208 */ 209 protected OutputStream wrapTarget(OutputStream targetStream) { 210 if (withJansi) { 211 return wrapWithJansi(targetStream); 212 } else { 213 return targetStream; 214 } 215 } 216 217 /** 218 * Wraps the console target stream with a Jansi ANSI-aware stream using 219 * reflection. 220 * <p> 221 * This path is <strong>deprecated</strong>. Prefer {@link JansiConsoleAppender}, 222 * which overrides {@link #wrapTarget(OutputStream)} and calls 223 * {@code org.jline.jansi.AnsiConsole} directly. 224 * </p> 225 * 226 * @param targetStream the raw console target stream 227 * @return a Jansi-backed stream, or {@code targetStream} on failure 228 * @deprecated Use {@link JansiConsoleAppender} / override {@link #wrapTarget(OutputStream)}. 229 */ 230 @Deprecated 231 protected OutputStream wrapWithJansi(OutputStream targetStream) { 232 addWarn(WITH_JANSI_DEPRECATED_MSG); 233 try { 234 addInfo("Enabling JANSI AnsiPrintStream for the console."); 235 ClassLoader classLoader = Loader.getClassLoaderOfObject(context); 236 Class<?> classObj = loadAnsiConsoleClass(classLoader); 237 238 Method systemInstallMethod = classObj.getMethod(SYSTEM_INSTALL_METHOD_NAME); 239 if(systemInstallMethod != null) { 240 systemInstallMethod.invoke(null); 241 } 242 243 // check for JAnsi 2 244 String methodNameJansi2 = target == ConsoleTarget.SystemOut ? JANSI2_OUT_METHOD_NAME 245 : JANSI2_ERR_METHOD_NAME; 246 final Optional<Method> optOutMethod = Arrays.stream(classObj.getMethods()) 247 .filter(m -> m.getName().equals(methodNameJansi2)) 248 .filter(m -> m.getParameters().length == 0) 249 .filter(m -> Modifier.isStatic(m.getModifiers())) 250 .filter(m -> PrintStream.class.isAssignableFrom(m.getReturnType())) 251 .findAny(); 252 if (optOutMethod.isPresent()) { 253 final Method outMethod = optOutMethod.orElseThrow(() -> new NoSuchElementException("No out/err method present")); 254 return (PrintStream) outMethod.invoke(null); 255 } 256 257 // JAnsi 1 258 String methodName = target == ConsoleTarget.SystemOut ? WRAP_SYSTEM_OUT_METHOD_NAME 259 : WRAP_SYSTEM_ERR_METHOD_NAME; 260 Method method = classObj.getMethod(methodName, ARGUMENT_TYPES); 261 return (OutputStream) method.invoke(null, new PrintStream(targetStream)); 262 } catch (Exception e) { 263 addWarn("Failed to create AnsiPrintStream. Falling back on the default stream.", e); 264 } 265 return targetStream; 266 } 267 268 /** 269 * Loads the Jansi {@code AnsiConsole} class. 270 * <p> 271 * If {@link #preferredJansiClassName} is set to a valid value 272 * ({@link #JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME} or {@link #FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME}), 273 * that class is loaded. An invalid preferred value is reported and ignored. 274 * <p> 275 * If {@code preferredJansiClassName} is not set (or was invalid), candidates are probed in 276 * {@link #ANSI_CONSOLE_CLASS_NAMES} order (JLine's {@code org.jline.jansi} first, then the legacy 277 * FuseSource {@code org.fusesource.jansi}). This keeps {@code <withJansi>} working across the Jansi 278 * migration from FuseSource to JLine. 279 * 280 * @throws ClassNotFoundException if none of the candidate classes is available. 281 */ 282 Class<?> loadAnsiConsoleClass(ClassLoader classLoader) throws ClassNotFoundException { 283 if (preferredJansiClassName != null) { 284 if (isValidPreferredJansiClassName(preferredJansiClassName)) { 285 return classLoader.loadClass(preferredJansiClassName); 286 } else { 287 preferredJansiClassNameWarn(preferredJansiClassName); 288 } 289 } 290 ClassNotFoundException lastException = null; 291 for (String className : ANSI_CONSOLE_CLASS_NAMES) { 292 try { 293 return classLoader.loadClass(className); 294 } catch (ClassNotFoundException e) { 295 lastException = e; 296 } 297 } 298 throw lastException; 299 } 300 301 /** 302 * @return whether to use JANSI or not. 303 * @deprecated Use {@link JansiConsoleAppender} instead of {@code withJansi} on 304 * {@link ConsoleAppender}. 305 */ 306 @Deprecated 307 public boolean isWithJansi() { 308 return withJansi; 309 } 310 311 /** 312 * If true, this appender will output to a stream provided by the JANSI library 313 * via reflection. 314 * <p> 315 * This option is <strong>deprecated</strong>. Use {@link JansiConsoleAppender}, 316 * which talks to {@code org.jline.jansi.AnsiConsole} directly, instead of 317 * {@code <withJansi>true</withJansi>} on a plain {@link ConsoleAppender}. 318 * </p> 319 * 320 * @param withJansi whether to use JANSI or not. 321 * @since 1.0.5 322 * @deprecated Use {@link JansiConsoleAppender} instead. 323 */ 324 @Deprecated 325 public void setWithJansi(boolean withJansi) { 326 this.withJansi = withJansi; 327 if (withJansi) { 328 addWarn(WITH_JANSI_DEPRECATED_MSG); 329 } 330 } 331 332}