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 static java.util.logging.Level.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.lang.ref.*; 023import java.util.*; 024import java.util.concurrent.*; 025import java.util.function.*; 026import java.util.logging.*; 027 028/** 029 * Extended logger that provides convenience methods for logging formatted messages. 030 * 031 * <p> 032 * This class extends {@link java.util.logging.Logger} and uses delegation internally to wrap 033 * an underlying {@link java.util.logging.Logger} instance. This design provides: 034 * </p> 035 * <ul> 036 * <li>Type compatibility - extends RichLogger for use anywhere a RichLogger is expected 037 * <li>Flexibility - can wrap existing logger instances 038 * <li>Convenience methods for formatted logging using {@link org.apache.juneau.commons.utils.Shorts#fs(String, Object...)} 039 * </ul> 040 * 041 * <h5 class='section'>Features:</h5> 042 * <ul> 043 * <li>All standard {@link java.util.logging.Logger} functionality via delegation 044 * <li>Convenience methods for formatted logging at all log levels 045 * <li>Lazy evaluation of formatted messages using {@link org.apache.juneau.commons.utils.Shorts#fs(String, Object...)} 046 * <li>Printf-style formatting (rendered via {@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)}) 047 * </ul> 048 * 049 * <h5 class='section'>Format Support:</h5> 050 * <p> 051 * The formatting methods use printf-style ({@link java.util.Formatter}) placeholders. MessageFormat-style 052 * (<js>"{0}"</js>) patterns are <b>not</b> expanded here — use {@link org.apache.juneau.commons.utils.Shorts#mf(String, Object...)} / 053 * {@link org.apache.juneau.commons.utils.StringUtils#mformat(String, Object...)} for those: 054 * </p> 055 * <ul> 056 * <li><b>Printf-style:</b> <js>"%s"</js>, <js>"%d"</js>, <js>"%1$s"</js>, etc. 057 * </ul> 058 * 059 * <p> 060 * Juneau {@link RichLogger} patterns are printf-style. Because {@link LogRecord#getMessage()} returns the 061 * already-substituted message, the {@link java.text.MessageFormat} parameter contract of the underlying 062 * {@link java.util.logging.LogRecord} only surfaces if a record is serialized and consumed as a plain 063 * {@link java.util.logging.LogRecord} by a non-Juneau handler. 064 * </p> 065 * 066 * <h5 class='section'>Example:</h5> 067 * <p class='bjava'> 068 * RichLogger <jv>logger</jv> = RichLogger.getLogger(MyClass.<jk>class</jk>.getName()); 069 * 070 * <jc>// Formatted logging at different levels (printf-style placeholders)</jc> 071 * <jv>logger</jv>.severe(<js>"Error processing user %s: %s"</js>, userId, error); 072 * <jv>logger</jv>.warning(<js>"Failed to connect to %s after %d attempts"</js>, hostname, attempts); 073 * <jv>logger</jv>.info(<js>"Processing %s items"</js>, count); 074 * <jv>logger</jv>.fine(<js>"Debug value: %s"</js>, debugValue); 075 * </p> 076 * 077 * <h5 class='section'>See Also:</h5><ul> 078 * <li class='jc'>{@link org.apache.juneau.commons.utils.Shorts#fs(String, Object...)} 079 * <li class='jc'>{@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)} 080 * <li class='jc'>{@link java.util.logging.Logger} 081 * </ul> 082 */ 083@SuppressWarnings({ 084 "java:S100", // Method names match java.util.logging.Logger for API compatibility 085 "java:S1192", // String literals intentionally duplicated for clarity 086 "java:S2176", // Class name intentionally matches java.util.logging.Logger; extends it to provide Juneau-specific logging enhancements 087 "resource" // Caller takes ownership of the returned LogRecordCapture 088}) 089public class RichLogger extends java.util.logging.Logger { 090 091 /** 092 * Permanent registry of logger instances by name. 093 * 094 * <p> 095 * Uses an unbounded {@link ConcurrentHashMap} so every name maps to exactly one 096 * {@link RichLogger} instance for the JVM lifetime. An evictable cache (e.g. one with a 097 * {@code maxSize} that clears the whole map) would cause a class-loaded 098 * {@code static final RichLogger} field to refer to an instance that is no longer in the 099 * registry; a subsequent call to {@link #getLogger(String)} would then return a new, 100 * different instance, breaking {@link LogRecordCapture} and any other code that 101 * assumes one name == one identity. 102 */ 103 private static final ConcurrentHashMap<String,LoggerRef> loggers = new ConcurrentHashMap<>(); 104 private static final ReferenceQueue<RichLogger> loggerQueue = new ReferenceQueue<>(); 105 106 private static final class LoggerRef extends WeakReference<RichLogger> { 107 final String name; 108 109 LoggerRef(String name, RichLogger referent, ReferenceQueue<RichLogger> q) { 110 super(referent, q); 111 this.name = name; 112 } 113 } 114 115 /** 116 * The underlying logger instance that we delegate to. 117 */ 118 private final java.util.logging.Logger delegate; 119 120 private final MessageGenerator generator; 121 private final RichLogger canonical; 122 123 /** 124 * List of log record listeners. 125 */ 126 private final List<LogRecordListener> listeners; 127 private volatile boolean useParentListeners = true; 128 129 /** 130 * Protected constructor - wraps an existing logger instance. 131 * 132 * @param delegate The underlying logger to delegate to. Must not be <jk>null</jk>. 133 */ 134 protected RichLogger(java.util.logging.Logger delegate) { 135 this(delegate, MessageGenerator.PRINTF, null); 136 } 137 138 private RichLogger(java.util.logging.Logger delegate, MessageGenerator generator, RichLogger canonical) { 139 super(delegate.getName(), null); 140 this.delegate = delegate; 141 this.generator = generator == null ? MessageGenerator.PRINTF : generator; 142 this.canonical = canonical == null ? this : canonical; 143 this.listeners = canonical == null ? Collections.synchronizedList(new ArrayList<>()) : null; 144 } 145 146 /** 147 * Creates a logger for the specified name. 148 * 149 * <p> 150 * This method returns the same RichLogger instance for a given name, ensuring that 151 * listeners attached to a logger persist across multiple calls to {@link #getLogger(String)}. 152 * RichLogger instances are cached and automatically created using the underlying 153 * {@link java.util.logging.Logger#getLogger(String)}. 154 * 155 * @param name The logger name. Must not be <jk>null</jk>. 156 * @return A logger instance (cached and reused for the same name). 157 */ 158 @SuppressWarnings({ 159 "java:S9149" // Intentional factory override of the hidden static Logger.getLogger(String); public API callers depend on this name/signature. 160 }) 161 public static RichLogger getLogger(String name) { 162 drainCollectedLoggers(); 163 while (true) { 164 var currentRef = loggers.get(name); 165 var current = currentRef == null ? null : currentRef.get(); 166 if (current != null) 167 return current; 168 var created = new RichLogger(java.util.logging.Logger.getLogger(name)); 169 var createdRef = new LoggerRef(name, created, loggerQueue); 170 if (currentRef == null) { 171 if (loggers.putIfAbsent(name, createdRef) == null) 172 return created; 173 } else if (loggers.replace(name, currentRef, createdRef)) 174 return created; 175 drainCollectedLoggers(); 176 } 177 } 178 179 /** 180 * Creates a logger for the specified class. 181 * 182 * @param clazz The class. Must not be <jk>null</jk>. 183 * @return A logger instance. 184 */ 185 public static RichLogger getLogger(Class<?> clazz) { 186 return getLogger(cn(clazz)); 187 } 188 189 /** 190 * Returns the thread-confined {@link LogContext} whose entries are auto-attached to every {@link LogRecord} emitted 191 * on the current thread while a scope is open. 192 * 193 * <p> 194 * Repeated calls return the same singleton (there is exactly one thread-local context map; sharing is harmless). 195 * 196 * @return The shared {@link LogContext} singleton. Never <jk>null</jk>. 197 */ 198 public static LogContext context() { 199 return LogContext.INSTANCE; 200 } 201 202 static RichLogger findLogger(String name) { 203 drainCollectedLoggers(); 204 var ref = loggers.get(name); 205 if (ref == null) 206 return null; 207 var logger = ref.get(); 208 if (logger == null) 209 loggers.remove(name, ref); 210 return logger; 211 } 212 213 static void forEachLiveAncestor(String name, Consumer<RichLogger> action) { 214 var ancestorName = parentName(name); 215 while (ancestorName != null) { 216 var ancestor = findLogger(ancestorName); 217 if (ancestor != null) 218 action.accept(ancestor); 219 ancestorName = parentName(ancestorName); 220 } 221 } 222 223 private static String parentName(String name) { 224 if (ie(name)) 225 return null; 226 var i = name.lastIndexOf('.'); 227 return i == -1 ? "" : name.substring(0, i); 228 } 229 230 private static void drainCollectedLoggers() { 231 LoggerRef ref; 232 while ((ref = (LoggerRef)loggerQueue.poll()) != null) 233 loggers.remove(ref.name, ref); 234 } 235 236 /** 237 * Creates a builder for the specified logger name. 238 * 239 * @param name The logger name. 240 * @return A new builder. 241 */ 242 public static Builder builder(String name) { 243 return new Builder(name); 244 } 245 246 /** 247 * Creates a builder for the specified class logger name. 248 * 249 * @param clazz The class. 250 * @return A new builder. 251 */ 252 public static Builder builder(Class<?> clazz) { 253 return builder(cn(clazz)); 254 } 255 256 /** 257 * Builder for creating configured {@link RichLogger} views. 258 */ 259 public static class Builder { 260 private final String name; 261 private MessageGenerator generator = MessageGenerator.PRINTF; 262 263 Builder(String name) { 264 this.name = name; 265 } 266 267 public Builder printf() { 268 generator = MessageGenerator.PRINTF; 269 return this; 270 } 271 272 public Builder messageFormat() { 273 generator = MessageGenerator.MESSAGE_FORMAT; 274 return this; 275 } 276 277 public Builder generator(MessageGenerator value) { 278 generator = value == null ? MessageGenerator.PRINTF : value; 279 return this; 280 } 281 282 public RichLogger build() { 283 var canonical = getLogger(name); 284 return new RichLogger(canonical.delegate, generator, canonical); 285 } 286 } 287 288 /** 289 * Creates a log record capture for testing. 290 * 291 * <p> 292 * This method creates a {@link LogRecordCapture} that will capture all log records 293 * from this logger. The capture should be closed when done (typically using try-with-resources). 294 * 295 * @return A LogRecordCapture instance. 296 */ 297 public LogRecordCapture captureEvents() { 298 return new LogRecordCapture(canonical); 299 } 300 301 public LogRecordCapture captureEvents(Level min) { 302 return new LogRecordCapture(canonical, x -> x.getLevel().intValue() >= min.intValue()); 303 } 304 305 public LogRecordCapture captureEvents(Predicate<java.util.logging.LogRecord> filter) { 306 return new LogRecordCapture(canonical, filter); 307 } 308 309 /** 310 * Adds a log record listener. 311 * 312 * @param listener The listener to add. 313 */ 314 void addLogRecordListener(LogRecordListener listener) { 315 canonical.listeners.add(listener); 316 } 317 318 /** 319 * Removes a log record listener. 320 * 321 * @param listener The listener to remove. 322 */ 323 void removeLogRecordListener(LogRecordListener listener) { 324 canonical.listeners.remove(listener); 325 } 326 327 public void setUseParentListeners(boolean useParentListeners) { 328 canonical.useParentListeners = useParentListeners; 329 } 330 331 public boolean isUseParentListeners() { 332 return canonical.useParentListeners; 333 } 334 335 @Override 336 public void log(java.util.logging.LogRecord rec) { 337 LogRecordContext.attachIfAbsent(rec); 338 canonical.listeners.forEach(x -> x.onLogRecord(rec)); 339 if (canonical.useParentListeners) { 340 var ancestors = new ArrayList<RichLogger>(); 341 forEachLiveAncestor(canonical.getName(), ancestors::add); 342 for (var ancestor : ancestors) { 343 ancestor.listeners.forEach(x -> x.onLogRecord(rec)); 344 if (!ancestor.useParentListeners) 345 break; 346 } 347 } 348 delegate.log(rec); 349 } 350 351 private static boolean hasAnyListenerInChain(RichLogger logger) { 352 if (!logger.listeners.isEmpty()) 353 return true; 354 if (!logger.useParentListeners) 355 return false; 356 var ancestors = new ArrayList<RichLogger>(); 357 forEachLiveAncestor(logger.getName(), ancestors::add); 358 for (var ancestor : ancestors) { 359 if (!ancestor.listeners.isEmpty()) 360 return true; 361 if (!ancestor.useParentListeners) 362 break; 363 } 364 return false; 365 } 366 367 /** 368 * Central logging method that all logging calls feed through. 369 * 370 * <p> 371 * This method creates a {@link LogRecord}, notifies all listeners, and then delegates 372 * to the underlying logger. 373 * 374 * <p> 375 * The method will proceed even if the log level is not loggable if there are listeners 376 * attached, allowing listeners to capture all log records regardless of level. 377 * 378 * @param level The log level. 379 * @param msg The log message or format pattern. 380 * @param args The format arguments, or <jk>null</jk> if none. 381 * @param thrown The throwable, or <jk>null</jk> if none. 382 */ 383 private void doLog(Level level, String msg, Object[] args, Throwable thrown) { 384 if (!isLoggable(level) && !hasAnyListenerInChain(canonical)) 385 return; 386 387 // Create LogRecord with lazy formatting support 388 var rec = new LogRecord(getName(), level, msg, args, thrown, generator); 389 this.log(rec); 390 } 391 392 private void logSupplier(Level level, Supplier<String> msgSupplier, Throwable thrown) { 393 if (!isLoggable(level) && !hasAnyListenerInChain(canonical)) 394 return; 395 var msg = msgSupplier == null ? null : msgSupplier.get(); 396 var rec = new LogRecord(getName(), level, msg, null, thrown, generator); 397 this.log(rec); 398 } 399 400 // Convenience methods with formatted strings 401 402 // Standard RichLogger methods - feed through central doLog method 403 @Override 404 public void severe(String msg) { 405 doLog(SEVERE, msg, null, null); 406 } 407 408 @Override 409 public void severe(Supplier<String> msgSupplier) { 410 logSupplier(SEVERE, msgSupplier, null); 411 } 412 413 @Override 414 public void warning(String msg) { 415 doLog(WARNING, msg, null, null); 416 } 417 418 @Override 419 public void warning(Supplier<String> msgSupplier) { 420 logSupplier(WARNING, msgSupplier, null); 421 } 422 423 @Override 424 public void info(String msg) { 425 doLog(INFO, msg, null, null); 426 } 427 428 @Override 429 public void info(Supplier<String> msgSupplier) { 430 logSupplier(INFO, msgSupplier, null); 431 } 432 433 @Override 434 public void config(String msg) { 435 doLog(CONFIG, msg, null, null); 436 } 437 438 @Override 439 public void config(Supplier<String> msgSupplier) { 440 logSupplier(CONFIG, msgSupplier, null); 441 } 442 443 @Override 444 public void fine(String msg) { 445 doLog(FINE, msg, null, null); 446 } 447 448 @Override 449 public void fine(Supplier<String> msgSupplier) { 450 logSupplier(FINE, msgSupplier, null); 451 } 452 453 @Override 454 public void finer(String msg) { 455 doLog(FINER, msg, null, null); 456 } 457 458 @Override 459 public void finer(Supplier<String> msgSupplier) { 460 logSupplier(FINER, msgSupplier, null); 461 } 462 463 @Override 464 public void finest(String msg) { 465 doLog(FINEST, msg, null, null); 466 } 467 468 @Override 469 public void finest(Supplier<String> msgSupplier) { 470 logSupplier(FINEST, msgSupplier, null); 471 } 472 473 @Override 474 public void log(Level level, String msg) { 475 doLog(level, msg, null, null); 476 } 477 478 @Override 479 public void log(Level level, Supplier<String> msgSupplier) { 480 logSupplier(level, msgSupplier, null); 481 } 482 483 @Override 484 public void log(Level level, String msg, Object param1) { 485 doLog(level, msg, new Object[]{param1}, null); 486 } 487 488 @Override 489 public void log(Level level, String msg, Object[] params) { 490 doLog(level, msg, params, null); 491 } 492 493 @Override 494 public void log(Level level, String msg, Throwable thrown) { 495 doLog(level, msg, null, thrown); 496 } 497 498 @Override 499 public boolean isLoggable(Level level) { 500 return delegate.isLoggable(level); 501 } 502 503 @Override 504 public Level getLevel() { 505 return delegate.getLevel(); 506 } 507 508 @Override 509 public void setLevel(Level newLevel) { 510 delegate.setLevel(newLevel); 511 } 512 513 /** 514 * Returns the wrapped JUL logger, which (unlike this wrapper) is linked into the JUL logger hierarchy. 515 * 516 * <p> 517 * Use it to walk {@link java.util.logging.Logger#getParent() parent} loggers, e.g. to find where a level was 518 * configured. 519 * 520 * @return The wrapped logger. Never <jk>null</jk>. 521 */ 522 public java.util.logging.Logger getDelegate() { 523 return delegate; 524 } 525 526 @Override 527 public String getName() { 528 return delegate.getName(); 529 } 530 531 @Override 532 public void addHandler(Handler handler) { 533 delegate.addHandler(handler); 534 } 535 536 @Override 537 public void removeHandler(Handler handler) { 538 delegate.removeHandler(handler); 539 } 540 541 @Override 542 public Handler[] getHandlers() { 543 return delegate.getHandlers(); 544 } 545 546 // Convenience methods with formatted strings 547 548 /** 549 * Logs a SEVERE level message with formatted string. 550 * 551 * @param pattern The format pattern. 552 * @param args The format arguments. 553 */ 554 public void severe(String pattern, Object...args) { 555 doLog(SEVERE, pattern, args, null); 556 } 557 558 /** 559 * Logs a SEVERE level message with formatted string and throwable. 560 * 561 * @param thrown The throwable. 562 * @param pattern The format pattern. 563 * @param args The format arguments. 564 */ 565 public void severe(Throwable thrown, String pattern, Object...args) { 566 doLog(SEVERE, pattern, args, thrown); 567 } 568 569 /** 570 * Logs a WARNING level message with formatted string. 571 * 572 * @param pattern The format pattern. 573 * @param args The format arguments. 574 */ 575 public void warning(String pattern, Object...args) { 576 doLog(WARNING, pattern, args, null); 577 } 578 579 /** 580 * Logs a WARNING level message with formatted string and throwable. 581 * 582 * @param thrown The throwable. 583 * @param pattern The format pattern. 584 * @param args The format arguments. 585 */ 586 public void warning(Throwable thrown, String pattern, Object...args) { 587 doLog(WARNING, pattern, args, thrown); 588 } 589 590 /** 591 * Logs an INFO level message with formatted string. 592 * 593 * @param pattern The format pattern. 594 * @param args The format arguments. 595 */ 596 public void info(String pattern, Object...args) { 597 doLog(INFO, pattern, args, null); 598 } 599 600 /** 601 * Logs an INFO level message with formatted string and throwable. 602 * 603 * @param thrown The throwable. 604 * @param pattern The format pattern. 605 * @param args The format arguments. 606 */ 607 public void info(Throwable thrown, String pattern, Object...args) { 608 doLog(INFO, pattern, args, thrown); 609 } 610 611 /** 612 * Logs a CONFIG level message with formatted string. 613 * 614 * @param pattern The format pattern. 615 * @param args The format arguments. 616 */ 617 public void config(String pattern, Object...args) { 618 doLog(CONFIG, pattern, args, null); 619 } 620 621 /** 622 * Logs a CONFIG level message with formatted string and throwable. 623 * 624 * @param thrown The throwable. 625 * @param pattern The format pattern. 626 * @param args The format arguments. 627 */ 628 public void config(Throwable thrown, String pattern, Object...args) { 629 doLog(CONFIG, pattern, args, thrown); 630 } 631 632 /** 633 * Logs a FINE level message with formatted string. 634 * 635 * @param pattern The format pattern. 636 * @param args The format arguments. 637 */ 638 public void fine(String pattern, Object...args) { 639 doLog(FINE, pattern, args, null); 640 } 641 642 /** 643 * Logs a FINE level message with formatted string and throwable. 644 * 645 * @param thrown The throwable. 646 * @param pattern The format pattern. 647 * @param args The format arguments. 648 */ 649 public void fine(Throwable thrown, String pattern, Object...args) { 650 doLog(FINE, pattern, args, thrown); 651 } 652 653 /** 654 * Logs a FINER level message with formatted string. 655 * 656 * @param pattern The format pattern. 657 * @param args The format arguments. 658 */ 659 public void finer(String pattern, Object...args) { 660 doLog(FINER, pattern, args, null); 661 } 662 663 /** 664 * Logs a FINER level message with formatted string and throwable. 665 * 666 * @param thrown The throwable. 667 * @param pattern The format pattern. 668 * @param args The format arguments. 669 */ 670 public void finer(Throwable thrown, String pattern, Object...args) { 671 doLog(FINER, pattern, args, thrown); 672 } 673 674 /** 675 * Logs a FINEST level message with formatted string. 676 * 677 * @param pattern The format pattern. 678 * @param args The format arguments. 679 */ 680 public void finest(String pattern, Object...args) { 681 doLog(FINEST, pattern, args, null); 682 } 683 684 /** 685 * Logs a FINEST level message with formatted string and throwable. 686 * 687 * @param thrown The throwable. 688 * @param pattern The format pattern. 689 * @param args The format arguments. 690 */ 691 public void finest(Throwable thrown, String pattern, Object...args) { 692 doLog(FINEST, pattern, args, thrown); 693 } 694 695 /** 696 * Logs a message at the specified level with formatted string. 697 * 698 * @param level The log level. Must not be <jk>null</jk>. 699 * @param pattern The format pattern. 700 * @param args The format arguments. 701 */ 702 public void logf(Level level, String pattern, Object...args) { 703 doLog(level, pattern, args, null); 704 } 705 706 /** 707 * Logs a message at the specified level with formatted string and throwable. 708 * 709 * @param level The log level. Must not be <jk>null</jk>. 710 * @param pattern The format pattern. 711 * @param thrown The throwable. 712 * @param args The format arguments. 713 */ 714 public void logf(Level level, String pattern, Throwable thrown, Object...args) { 715 doLog(level, pattern, args, thrown); 716 } 717}