001/* 002 * (C) Copyright 2017-2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Thomas Roger 018 */ 019package org.nuxeo.runtime.test.runner; 020 021import java.lang.annotation.Annotation; 022import java.lang.annotation.ElementType; 023import java.util.ArrayList; 024import java.util.List; 025import java.util.Map; 026import java.util.concurrent.ConcurrentHashMap; 027 028import org.apache.commons.lang3.StringUtils; 029import org.apache.logging.log4j.Level; 030import org.apache.logging.log4j.core.Logger; 031import org.apache.logging.log4j.core.LoggerContext; 032import org.apache.logging.log4j.core.appender.ConsoleAppender; 033import org.apache.logging.log4j.core.appender.ConsoleAppender.Target; 034import org.apache.logging.log4j.core.config.Configurator; 035import org.apache.logging.log4j.core.filter.ThresholdFilter; 036import org.junit.runners.model.FrameworkMethod; 037 038/** 039 * @since 9.3 040 */ 041public class LogFeature implements RunnerFeature { 042 043 protected static final String CONSOLE_APPENDER = "CONSOLE"; 044 045 protected static final String CONSOLE_LOG_FEATURE_APPENDER = "CONSOLE_LOG_FEATURE"; 046 047 protected ConsoleAppender consoleAppender; 048 049 protected ConsoleAppender hiddenAppender; 050 051 /** 052 * Stores the original log level for a given logger name, which allows us to restore the level as defined before 053 * launching the tests. 054 * 055 * @since 11.1 056 */ 057 protected Map<LoggerLevelKey, Level> originalLevelByLogger = new ConcurrentHashMap<>(); 058 059 /** 060 * {@inheritDoc} 061 * <p> 062 * 063 * @since 11.1 064 */ 065 @Override 066 public void beforeRun(FeaturesRunner runner) { 067 originalLevelByLogger.clear(); 068 addOrUpdateLoggerLevel(runner, null); 069 addConsoleThresholdLogLevel(runner, null); 070 } 071 072 /** 073 * {@inheritDoc} 074 * <p> 075 * 076 * @since 11.1 077 */ 078 @Override 079 public void afterRun(FeaturesRunner runner) { 080 restoreLoggerLevel(runner, null); 081 restoreConsoleThresholdLogLevel(runner, null); 082 } 083 084 /** 085 * {@inheritDoc} 086 * <p> 087 * 088 * @since 11.1 089 */ 090 @Override 091 public void beforeMethodRun(FeaturesRunner runner, FrameworkMethod method, Object test) { 092 addOrUpdateLoggerLevel(runner, method); 093 addConsoleThresholdLogLevel(runner, method); 094 } 095 096 /** 097 * {@inheritDoc} 098 * <p> 099 * 100 * @since 11.1 101 */ 102 @Override 103 public void afterMethodRun(FeaturesRunner runner, FrameworkMethod method, Object test) { 104 restoreLoggerLevel(runner, method); 105 restoreConsoleThresholdLogLevel(runner, method); 106 } 107 108 /** 109 * @deprecated since 11.1. Use {@link ConsoleLogLevelThreshold} with {@link ConsoleLogLevelThreshold#value()} set to 110 * {@code ERROR}. 111 */ 112 @Deprecated(since = "11.1", forRemoval = true) 113 public void hideWarningFromConsoleLog() { 114 setConsoleLogThreshold(Level.ERROR.toString()); 115 } 116 117 /** 118 * @deprecated since 11.1. Use {@link ConsoleLogLevelThreshold} with {@link ConsoleLogLevelThreshold#value()} set to 119 * {@code FATAL}. 120 * @since 9.10 121 */ 122 @Deprecated(since = "11.1", forRemoval = true) 123 public void hideErrorFromConsoleLog() { 124 setConsoleLogThreshold(Level.FATAL.toString()); 125 } 126 127 /** 128 * @since 9.10 129 */ 130 public void setConsoleLogThreshold(String level) { 131 if (consoleAppender != null) { 132 return; 133 } 134 135 Logger rootLogger = LoggerContext.getContext(false).getRootLogger(); 136 consoleAppender = (ConsoleAppender) rootLogger.getAppenders().get(CONSOLE_APPENDER); 137 rootLogger.removeAppender(consoleAppender); 138 ConsoleAppender newAppender = ConsoleAppender.newBuilder() 139 .withName(CONSOLE_LOG_FEATURE_APPENDER) 140 .setTarget(Target.SYSTEM_OUT) 141 .withFilter(ThresholdFilter.createFilter(Level.toLevel(level), 142 null, null)) 143 .build(); 144 newAppender.start(); 145 rootLogger.addAppender(newAppender); 146 hiddenAppender = newAppender; 147 } 148 149 public void restoreConsoleLog() { 150 if (consoleAppender == null) { 151 return; 152 } 153 154 Logger rootLogger = LoggerContext.getContext(false).getRootLogger(); 155 rootLogger.removeAppender(hiddenAppender); 156 rootLogger.addAppender(consoleAppender); 157 consoleAppender = null; 158 hiddenAppender = null; 159 } 160 161 /** 162 * Adds the console threshold log level. To be proceed a {@code Class} / {@code Method} should be annotated by 163 * @see ConsoleLogLevelThreshold 164 * <p> 165 * @see #setConsoleLogThreshold(String) 166 * 167 * @param runner the feature runner, cannot be {@code null} 168 * @param method the framework method, can be {@code null} 169 * @since 11.1 170 */ 171 protected void addConsoleThresholdLogLevel(FeaturesRunner runner, FrameworkMethod method) { 172 // Remove the previous threshold if any. 173 restoreConsoleLog(); 174 // Set the new threshold 175 ConsoleLogLevelThreshold consoleLogThreshold = getAnnotation(runner, method, ConsoleLogLevelThreshold.class); 176 if (consoleLogThreshold.value() != null) { 177 setConsoleLogThreshold(consoleLogThreshold.value()); 178 } 179 } 180 181 /** 182 * Restores the console threshold log level. Based on if {@code Class} or {@code Method} is annotated by 183 * {@link ConsoleLogLevelThreshold}. 184 * <p> 185 * {@link #restoreConsoleLog()} 186 * 187 * @since 11.1 188 */ 189 protected void restoreConsoleThresholdLogLevel(FeaturesRunner runner, FrameworkMethod method) { 190 ConsoleLogLevelThreshold consoleLogThreshold = getAnnotation(runner, method, ConsoleLogLevelThreshold.class); 191 if (consoleLogThreshold.value() != null) { 192 restoreConsoleLog(); 193 } 194 } 195 196 /** 197 * Adds or updates the logger level. 198 * <p> 199 * The definition of {@link LoggerLevel} can be done on a given {@code Class} / {@code Method} test. At the end of 200 * the test each overriding logger must be restored to its original value for this the purpose we should save the 201 * original level. 202 * <p> 203 * {@link #restoreLoggerLevel(FeaturesRunner, FrameworkMethod)} to see how the restore part will be happened. 204 * 205 * @param runner the feature runner, cannot be {@code null} 206 * @param method the framework method, can be {@code null} 207 * @since 11.1 208 */ 209 protected void addOrUpdateLoggerLevel(FeaturesRunner runner, FrameworkMethod method) { 210 for (LoggerLevel logger : getLoggers(runner, method)) { 211 if (logger.level() != null) { 212 String loggerName = getLoggerName(logger); 213 LoggerContext context = LoggerContext.getContext(false); 214 // Initialize the undefined logger to simplify the restoring step. There is no way to remove / delete 215 // a logger this is why we turned it to OFF. 216 if (!context.hasLogger(loggerName)) { 217 Configurator.setLevel(loggerName, Level.OFF); 218 } 219 220 // Save the original value. 221 originalLevelByLogger.put(buildKey(logger, method), context.getLogger(loggerName).getLevel()); 222 // Set the new level that we want 223 Configurator.setLevel(loggerName, Level.toLevel(logger.level())); 224 } 225 } 226 } 227 228 /** 229 * Restores the original value of the logger level. 230 * <p> 231 * {@link #addOrUpdateLoggerLevel(FeaturesRunner, FrameworkMethod)}} to see how we store the original value and set 232 * the new one. 233 * 234 * @param runner the feature runner, cannot be {@code null} 235 * @param method the framework method, can be {@code null} 236 * @since 11.1 237 */ 238 protected void restoreLoggerLevel(FeaturesRunner runner, FrameworkMethod method) { 239 for (LoggerLevel logger : getLoggers(runner, method)) { 240 if (logger.level() != null) { 241 String loggerName = getLoggerName(logger); 242 Level level = originalLevelByLogger.remove(buildKey(logger, method)); 243 Configurator.setLevel(loggerName, level); 244 } 245 } 246 } 247 248 /** 249 * Retrieves the {@link LoggerLevel}, a {@code Class} or a {@code Method} can be annotated by one or more Logger. 250 * 251 * @since 11.1 252 */ 253 protected List<LoggerLevel> getLoggers(FeaturesRunner runner, FrameworkMethod method) { 254 // Unique annotation LoggerLevel. 255 List<LoggerLevel> loggers = new ArrayList<>(List.of(getAnnotation(runner, method, LoggerLevel.class))); 256 257 // Repeatable annotation LoggerLevel using LoggerLevels. 258 LoggerLevels configs = getAnnotation(runner, method, LoggerLevels.class); 259 if (configs.value() != null) { 260 loggers.addAll(List.of(configs.value())); 261 } 262 263 return loggers; 264 } 265 266 /** 267 * Gets the annotation for a given {@code FeaturesRunner}, {@code FrameworkMethod} and a {@code Class} type. 268 * 269 * @since 11.1 270 */ 271 protected <T extends Annotation> T getAnnotation(FeaturesRunner runner, FrameworkMethod method, Class<T> type) { 272 return method != null ? runner.getConfig(method, type) : runner.getConfig(type); 273 274 } 275 276 /** 277 * Gets the logger name from a given {@link LoggerLevel}. 278 * 279 * @since 11.1 280 */ 281 protected String getLoggerName(LoggerLevel logLevel) { 282 return StringUtils.defaultIfBlank(logLevel.name(), logLevel.klass().getName()); 283 } 284 285 /** 286 * Builds the logger key. 287 * 288 * @since 11.1 289 */ 290 protected LoggerLevelKey buildKey(LoggerLevel logger, FrameworkMethod method) { 291 ElementType type = method != null ? ElementType.METHOD : ElementType.TYPE; 292 String loggerName = getLoggerName(logger); 293 return new LoggerLevelKey(type, loggerName); 294 } 295}