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; 017import java.io.PrintStream; 018import java.lang.reflect.Method; 019import java.lang.reflect.Modifier; 020import java.util.Arrays; 021import java.util.NoSuchElementException; 022import java.util.Optional; 023 024import ch.qos.logback.core.joran.spi.ConsoleTarget; 025import ch.qos.logback.core.status.Status; 026import ch.qos.logback.core.status.WarnStatus; 027import ch.qos.logback.core.util.Loader; 028import ch.qos.logback.core.util.ReentryGuard; 029import ch.qos.logback.core.util.ReentryGuardFactory; 030 031/** 032 * ConsoleAppender appends log events to <code>System.out</code> or 033 * <code>System.err</code> using a layout specified by the user. The default 034 * target is <code>System.out</code>. 035 * <p> 036 * 037 * </p> 038 * For more information about this appender, please refer to the online manual 039 * at http://logback.qos.ch/manual/appenders.html#ConsoleAppender 040 * 041 * @author Ceki Gülcü 042 * @author Tom SH Liu 043 * @author Ruediger Dohna 044 */ 045 046public class ConsoleAppender<E> extends OutputStreamAppender<E> { 047 048 protected ConsoleTarget target = ConsoleTarget.SystemOut; 049 protected boolean withJansi = false; 050 051 052 public final static String JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME = "org.jline.jansi.AnsiConsole"; 053 public final static String FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME = "org.fusesource.jansi.AnsiConsole"; 054 055 // Jansi was migrated from FuseSource (org.fusesource.jansi) to JLine (org.jline.jansi), which 056 // changed the package of AnsiConsole. Probe the JLine coordinates first, then fall back to the 057 // legacy FuseSource ones so that <withJansi> keeps working with both artifacts. See LOGBACK issue 1043. 058 private final static String[] ANSI_CONSOLE_CLASS_NAMES = { JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME, 059 FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME }; 060 061 protected String preferredJansiClassName = null; 062 063 private final static String JANSI2_OUT_METHOD_NAME = "out"; 064 private final static String JANSI2_ERR_METHOD_NAME = "err"; 065 private final static String WRAP_SYSTEM_OUT_METHOD_NAME = "wrapSystemOut"; 066 private final static String WRAP_SYSTEM_ERR_METHOD_NAME = "wrapSystemErr"; 067 private final static String SYSTEM_INSTALL_METHOD_NAME = "systemInstall"; 068 private final static Class<?>[] ARGUMENT_TYPES = { PrintStream.class }; 069 070 private final static String CONSOLE_APPENDER_WARNING_URL = CoreConstants.CODES_URL+"#slowConsole"; 071 072 /** 073 * Sets the value of the <b>Target</b> option. Recognized values are 074 * "System.out" and "System.err". Any other value will be ignored. 075 */ 076 public void setTarget(String value) { 077 ConsoleTarget t = ConsoleTarget.findByName(value.trim()); 078 if (t == null) { 079 targetWarn(value); 080 } else { 081 target = t; 082 } 083 } 084 085 /** 086 * Returns the current value of the <b>target</b> property. The default value of 087 * the option is "System.out". 088 * <p> 089 * See also {@link #setTarget}. 090 */ 091 public String getTarget() { 092 return target.getName(); 093 } 094 095 /** 096 * 097 * @return the preferred Jansi class name 098 */ 099 public String getPreferredJansiClassName() { 100 return preferredJansiClassName; 101 } 102 103 /** 104 * It allows to force Jansi class name used for probing. 105 * 106 * <p>Used for testing purposes.</p> 107 * <p>Valid values are {@link #JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME} and 108 * {@link #FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME}.</p> 109 * 110 * @param preferredJansiClassName the preferred Jansi class name 111 * @since 1.6.1 112 */ 113 public void setPreferredJansiClassName(String preferredJansiClassName) { 114 this.preferredJansiClassName = preferredJansiClassName; 115 } 116 117 private boolean isValidPreferredJansiClassName(String className) { 118 return JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME.equals(className) 119 || FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME.equals(className); 120 } 121 122 private void preferredJansiClassNameWarn(String val) { 123 Status status = new WarnStatus( 124 "[" + val + "] should be one of " + Arrays.toString(ANSI_CONSOLE_CLASS_NAMES), this); 125 status.add(new WarnStatus("Ignoring preferredJansiClassName, using default probing order.", this)); 126 addStatus(status); 127 } 128 129 private void targetWarn(String val) { 130 Status status = new WarnStatus("[" + val + "] should be one of " + Arrays.toString(ConsoleTarget.values()), 131 this); 132 status.add(new WarnStatus("Using previously set target, System.out by default.", this)); 133 addStatus(status); 134 } 135 136 @Override 137 public void start() { 138 addInfo("NOTE: Writing to the console can be slow. Try to avoid logging to the "); 139 addInfo("console in production environments, especially in high volume systems."); 140 addInfo("See also "+CONSOLE_APPENDER_WARNING_URL); 141 OutputStream targetStream = target.getStream(); 142 // enable jansi only if withJansi set to true 143 if (withJansi) { 144 targetStream = wrapWithJansi(targetStream); 145 } 146 setOutputStream(targetStream); 147 super.start(); 148 } 149 150 /** 151 * Create a ThreadLocal ReentryGuard to prevent recursive appender invocations. 152 * @return a ReentryGuard instance of type {@link ReentryGuardFactory.GuardType#THREAD_LOCAL THREAD_LOCAL}. 153 */ 154 protected ReentryGuard buildReentryGuard() { 155 return ReentryGuardFactory.makeGuard(ReentryGuardFactory.GuardType.THREAD_LOCAL); 156 } 157 158 private OutputStream wrapWithJansi(OutputStream targetStream) { 159 try { 160 addInfo("Enabling JANSI AnsiPrintStream for the console."); 161 ClassLoader classLoader = Loader.getClassLoaderOfObject(context); 162 Class<?> classObj = loadAnsiConsoleClass(classLoader); 163 164 Method systemInstallMethod = classObj.getMethod(SYSTEM_INSTALL_METHOD_NAME); 165 if(systemInstallMethod != null) { 166 systemInstallMethod.invoke(null); 167 } 168 169 // check for JAnsi 2 170 String methodNameJansi2 = target == ConsoleTarget.SystemOut ? JANSI2_OUT_METHOD_NAME 171 : JANSI2_ERR_METHOD_NAME; 172 final Optional<Method> optOutMethod = Arrays.stream(classObj.getMethods()) 173 .filter(m -> m.getName().equals(methodNameJansi2)) 174 .filter(m -> m.getParameters().length == 0) 175 .filter(m -> Modifier.isStatic(m.getModifiers())) 176 .filter(m -> PrintStream.class.isAssignableFrom(m.getReturnType())) 177 .findAny(); 178 if (optOutMethod.isPresent()) { 179 final Method outMethod = optOutMethod.orElseThrow(() -> new NoSuchElementException("No out/err method present")); 180 return (PrintStream) outMethod.invoke(null); 181 } 182 183 // JAnsi 1 184 String methodName = target == ConsoleTarget.SystemOut ? WRAP_SYSTEM_OUT_METHOD_NAME 185 : WRAP_SYSTEM_ERR_METHOD_NAME; 186 Method method = classObj.getMethod(methodName, ARGUMENT_TYPES); 187 return (OutputStream) method.invoke(null, new PrintStream(targetStream)); 188 } catch (Exception e) { 189 addWarn("Failed to create AnsiPrintStream. Falling back on the default stream.", e); 190 } 191 return targetStream; 192 } 193 194 /** 195 * Loads the Jansi {@code AnsiConsole} class. 196 * <p> 197 * If {@link #preferredJansiClassName} is set to a valid value 198 * ({@link #JLINE_JANSI_ANSI_CONSOLE_CLASS_NAME} or {@link #FUSESOURCE_JANSI_ANSI_CONSOLE_CLASS_NAME}), 199 * that class is loaded. An invalid preferred value is reported and ignored. 200 * <p> 201 * If {@code preferredJansiClassName} is not set (or was invalid), candidates are probed in 202 * {@link #ANSI_CONSOLE_CLASS_NAMES} order (JLine's {@code org.jline.jansi} first, then the legacy 203 * FuseSource {@code org.fusesource.jansi}). This keeps {@code <withJansi>} working across the Jansi 204 * migration from FuseSource to JLine. 205 * 206 * @throws ClassNotFoundException if none of the candidate classes is available. 207 */ 208 Class<?> loadAnsiConsoleClass(ClassLoader classLoader) throws ClassNotFoundException { 209 if (preferredJansiClassName != null) { 210 if (isValidPreferredJansiClassName(preferredJansiClassName)) { 211 return classLoader.loadClass(preferredJansiClassName); 212 } else { 213 preferredJansiClassNameWarn(preferredJansiClassName); 214 } 215 } 216 ClassNotFoundException lastException = null; 217 for (String className : ANSI_CONSOLE_CLASS_NAMES) { 218 try { 219 return classLoader.loadClass(className); 220 } catch (ClassNotFoundException e) { 221 lastException = e; 222 } 223 } 224 throw lastException; 225 } 226 227 /** 228 * @return whether to use JANSI or not. 229 */ 230 public boolean isWithJansi() { 231 return withJansi; 232 } 233 234 /** 235 * If true, this appender will output to a stream provided by the JANSI library. 236 * 237 * @param withJansi whether to use JANSI or not. 238 * @since 1.0.5 239 */ 240 public void setWithJansi(boolean withJansi) { 241 this.withJansi = withJansi; 242 } 243 244}