001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.logging; 018 019import java.io.*; 020import java.util.*; 021import java.util.function.*; 022import java.util.logging.Handler; 023import java.util.regex.*; 024 025import org.apache.juneau.commons.function.*; 026 027/** 028 * Captures log records for testing purposes. 029 * 030 * <p> 031 * This class implements {@link LogRecordListener} to receive log records and stores them 032 * in memory for inspection during tests. It implements {@link Closeable} to automatically 033 * remove itself from the logger's listeners when closed. 034 * 035 * <h5 class='section'>Usage:</h5> 036 * <p class='bjava'> 037 * <jc>// Capture records using try-with-resources</jc> 038 * <jk>try</jk> (LogRecordCapture <jv>capture</jv> = RichLogger.getLogger(MyClass.<jk>class</jk>).captureEvents()) { 039 * <jv>logger</jv>.info(<js>"Test message"</js>); 040 * <jv>logger</jv>.warning(<js>"Warning message"</js>); 041 * 042 * <jc>// Inspect captured records</jc> 043 * List<LogRecord> <jv>records</jv> = <jv>capture</jv>.getRecords(); 044 * assertEquals(2, <jv>records</jv>.size()); 045 * 046 * <jc>// Get formatted messages</jc> 047 * List<String> <jv>messages</jv> = <jv>capture</jv>.getRecords(<js>"{level}: {msg}"</js>); 048 * assertEquals(<js>"INFO: Test message"</js>, <jv>messages</jv>.get(0)); 049 * } 050 * </p> 051 * 052 * <h5 class='section'>Format String:</h5> 053 * <p> 054 * The format string supports placeholders as documented in {@link LogRecord#formatted(String)}. 055 * </p> 056 * 057 * <h5 class='section'>See Also:</h5><ul> 058 * <li class='jc'>{@link RichLogger#captureEvents()} 059 * <li class='jc'>{@link LogRecord#formatted(String)} 060 * </ul> 061 */ 062public class LogRecordCapture implements LogRecordListener, Closeable { 063 064 /** 065 * Runs a block while capturing, and keeping off the console, everything logged to the named logger or any of its descendants. 066 * <p> 067 * Unlike {@link RichLogger#captureEvents()}, which only observes records, this also stops the logger's records from 068 * reaching the parent (console) handlers for the duration of the block. The logger's previous parent-handler setting 069 * is restored in a <c>finally</c> block. 070 * <p class='bjava'> 071 * List<LogRecord> <jv>records</jv> = LogRecordCapture.<jsm>quietly</jsm>(MyClass.<jk>class</jk>, () -> <jv>obj</jv>.doSomethingThatWarns()); 072 * assertEquals(1, <jv>records</jv>.size()); 073 * </p> 074 * 075 * @param loggerName The logger name. Records from descendant loggers are captured too. 076 * @param body The code to run. Checked exceptions are rethrown wrapped in a {@link RuntimeException}. 077 * @return The captured records, in the order logged. Never <jk>null</jk>. 078 */ 079 @SuppressWarnings({ 080 "java:S1181" // Snippet.run() throws Throwable; Errors are rethrown and checked exceptions wrapped 081 }) 082 public static List<java.util.logging.LogRecord> quietly(String loggerName, Snippet body) { 083 var records = Collections.synchronizedList(new ArrayList<java.util.logging.LogRecord>()); 084 var handler = new Handler() { 085 @Override public void publish(java.util.logging.LogRecord r) { records.add(r); } 086 @Override public void flush() { /* No-op. */ } 087 @Override public void close() { /* No-op. */ } 088 }; 089 var log = java.util.logging.Logger.getLogger(loggerName); 090 try (var q = quiet(loggerName)) { 091 log.addHandler(handler); 092 try { 093 body.run(); 094 } catch (RuntimeException | Error e) { 095 throw e; 096 } catch (Throwable e) { 097 throw new RuntimeException(e); 098 } finally { 099 log.removeHandler(handler); 100 } 101 } 102 synchronized (records) { 103 return List.copyOf(records); 104 } 105 } 106 107 /** 108 * Keeps everything logged to the named logger, and its descendants, off the console until the returned handle is closed. 109 * <p> 110 * Use this when a whole test class (for example from <ja>@BeforeAll</ja> / <ja>@AfterAll</ja>) expects a warning it is not 111 * asserting on. Calls nest and may overlap across threads: the logger's original parent-handler setting is restored when 112 * the last outstanding handle for that logger name is closed. 113 * 114 * @param loggerName The logger name. 115 * @return A handle that restores the logger when closed. Never <jk>null</jk>. 116 */ 117 public static Quiet quiet(String loggerName) { 118 synchronized (QUIET) { 119 var state = QUIET.computeIfAbsent(loggerName, k -> new QuietState(java.util.logging.Logger.getLogger(k))); 120 if (state.count++ == 0) { 121 state.useParent = state.log.getUseParentHandlers(); 122 state.log.setUseParentHandlers(false); 123 } 124 return () -> release(loggerName); 125 } 126 } 127 128 /** 129 * Same as {@link #quiet(String)}, keyed by the logger's class name. 130 * 131 * @param loggerClass The class whose logger is silenced. 132 * @return A handle that restores the logger when closed. Never <jk>null</jk>. 133 */ 134 @SuppressWarnings({ 135 "resource" // The returned handle is owned by the caller, who closes it. 136 }) 137 public static Quiet quiet(Class<?> loggerClass) { 138 return quiet(loggerClass.getName()); 139 } 140 141 /** 142 * Handle returned by {@link #quiet(String)}. 143 */ 144 @FunctionalInterface 145 public interface Quiet extends AutoCloseable { 146 @Override void close(); 147 } 148 149 private static final Map<String,QuietState> QUIET = new HashMap<>(); 150 151 private static final class QuietState { 152 final java.util.logging.Logger log; // Strong reference: the LogManager only holds loggers weakly. 153 int count; 154 boolean useParent; 155 QuietState(java.util.logging.Logger log) { this.log = log; } 156 } 157 158 private static void release(String loggerName) { 159 synchronized (QUIET) { 160 var state = QUIET.get(loggerName); 161 if (--state.count == 0) { 162 state.log.setUseParentHandlers(state.useParent); 163 QUIET.remove(loggerName); 164 } 165 } 166 } 167 168 /** 169 * Same as {@link #quietly(String, Snippet)}, keyed by the logger's class name. 170 * 171 * @param loggerClass The class whose logger is captured. 172 * @param body The code to run. 173 * @return The captured records, in the order logged. Never <jk>null</jk>. 174 */ 175 public static List<java.util.logging.LogRecord> quietly(Class<?> loggerClass, Snippet body) { 176 return quietly(loggerClass.getName(), body); 177 } 178 179 private final RichLogger logger; 180 private final List<RichLogger> pinnedLoggers = new ArrayList<>(); 181 private final List<java.util.logging.LogRecord> records = Collections.synchronizedList(new ArrayList<>()); 182 private final Predicate<java.util.logging.LogRecord> filter; 183 184 /** 185 * Constructor. 186 * 187 * @param logger The logger to capture records from. 188 */ 189 LogRecordCapture(RichLogger logger) { 190 this(logger, x -> true); 191 } 192 193 LogRecordCapture(RichLogger logger, Predicate<java.util.logging.LogRecord> filter) { 194 this.logger = logger; 195 this.filter = filter == null ? x -> true : filter; 196 pinnedLoggers.add(logger); 197 RichLogger.forEachLiveAncestor(logger.getName(), pinnedLoggers::add); 198 logger.addLogRecordListener(this); 199 } 200 201 /** 202 * Called when a log record is logged. 203 * 204 * @param rec The log record that was logged. 205 */ 206 @Override 207 public void onLogRecord(java.util.logging.LogRecord rec) { 208 if (filter.test(rec)) 209 records.add(rec); 210 } 211 212 /** 213 * Returns an unmodifiable list of all captured log records. 214 * 215 * @return An unmodifiable list of captured LogRecords. 216 */ 217 public List<java.util.logging.LogRecord> getRecords() { 218 synchronized (records) { 219 return List.copyOf(records); 220 } 221 } 222 223 /** 224 * Returns captured log records formatted as strings. 225 * 226 * <p> 227 * The format string supports placeholders as documented in {@link LogRecord#formatted(String)}. 228 * 229 * @param format The format string with placeholders. 230 * @return A list of formatted record strings. 231 */ 232 public List<String> getRecords(String format) { 233 synchronized (records) { 234 return records.stream() 235 .map(x -> LogRecord.formatted(x, format)) 236 .toList(); 237 } 238 } 239 240 /** 241 * Clears all captured records. 242 */ 243 public void clear() { 244 records.clear(); 245 } 246 247 /** 248 * Returns the number of captured records. 249 * 250 * @return The number of captured records. 251 */ 252 public int size() { 253 return records.size(); 254 } 255 256 /** 257 * Returns <jk>true</jk> if any records have been captured. 258 * 259 * @return <jk>true</jk> if records have been captured. 260 */ 261 public boolean isEmpty() { 262 return records.isEmpty(); 263 } 264 265 public List<String> messages() { 266 synchronized (records) { 267 return records.stream().map(java.util.logging.LogRecord::getMessage).toList(); 268 } 269 } 270 271 public java.util.logging.LogRecord last() { 272 synchronized (records) { 273 return records.isEmpty() ? null : records.get(records.size() - 1); 274 } 275 } 276 277 public List<java.util.logging.LogRecord> byLevel(java.util.logging.Level level) { 278 synchronized (records) { 279 return records.stream().filter(x -> x.getLevel().equals(level)).toList(); 280 } 281 } 282 283 public List<java.util.logging.LogRecord> matching(String regex) { 284 var p = Pattern.compile(regex); 285 synchronized (records) { 286 return records.stream().filter(x -> p.matcher(x.getMessage()).find()).toList(); 287 } 288 } 289 290 /** 291 * Returns the value of a {@link LogContext} entry attached to the specified captured record. 292 * 293 * <p> 294 * Convenience over {@link LogRecordContext#of(java.util.logging.LogRecord)} so capture-based tests do not need to 295 * know about the side table directly. 296 * 297 * @param rec The captured record. 298 * @param key The context key. 299 * @return The attached value for the key, or <jk>null</jk> if not present. 300 */ 301 public Object contextValue(java.util.logging.LogRecord rec, String key) { 302 return LogRecordContext.of(rec).get(key); 303 } 304 305 public String assertMessage() { 306 var r = last(); 307 return r == null ? null : r.getMessage(); 308 } 309 310 public java.util.logging.Level assertLevel() { 311 var r = last(); 312 return r == null ? null : r.getLevel(); 313 } 314 315 public Throwable assertThrown() { 316 var r = last(); 317 return r == null ? null : r.getThrown(); 318 } 319 320 /** 321 * Closes this capture and removes it from the logger's listeners. 322 */ 323 @Override 324 public void close() { 325 logger.removeLogRecordListener(this); 326 pinnedLoggers.clear(); 327 } 328}