001package com.identityworksllc.iiq.common.logging; 002 003import bsh.This; 004import org.apache.commons.collections.map.ListOrderedMap; 005import org.apache.commons.logging.Log; 006import org.apache.commons.logging.LogFactory; 007import org.w3c.dom.Document; 008import sailpoint.api.SailPointContext; 009import sailpoint.object.*; 010import sailpoint.tools.GeneralException; 011import sailpoint.tools.xml.AbstractXmlObject; 012 013import javax.xml.transform.OutputKeys; 014import javax.xml.transform.Transformer; 015import javax.xml.transform.TransformerFactory; 016import javax.xml.transform.dom.DOMSource; 017import javax.xml.transform.stream.StreamResult; 018import java.io.ByteArrayOutputStream; 019import java.io.IOException; 020import java.io.PrintStream; 021import java.io.PrintWriter; 022import java.io.StringWriter; 023import java.lang.reflect.Array; 024import java.text.DateFormat; 025import java.text.MessageFormat; 026import java.text.SimpleDateFormat; 027import java.time.ZonedDateTime; 028import java.util.*; 029import java.util.concurrent.Callable; 030import java.util.concurrent.atomic.AtomicReference; 031import java.util.function.Supplier; 032 033/** 034 * A wrapper around the Commons Logging {@link Log} class that supplements its 035 * features with some extras available in other logging libraries. Since this 036 * class itself implements {@link Log}, Beanshell should not care if you simply 037 * overwrite the 'log' variable in your code. 038 * 039 * Log strings are interpreted as Java {@link MessageFormat} objects and have 040 * the various features of that class in your JDK version. 041 * 042 * Values passed as arguments are only evaluated when the appropriate log 043 * level is active. If the log level is not active, the operation becomes a 044 * quick no-op. This prevents a lot of isDebugEnabled() type checks. 045 * 046 * You can also pass a {@link Supplier} to any argument, and the supplier's 047 * get() method will be called to derive the value, but only if the log level 048 * is active. This allows you to wrap complex operations in a lambda and 049 * only have them executed if the log level is active. 050 * 051 * If you invoke {@link #capture()}, all log messages will be captured in a 052 * thread-local buffer, which can be retrieved with {@link #getCapturedLogs()}. 053 * You should always invoke {@link #reset()} in a finally block to ensure that 054 * the thread-local buffer is cleared. Capture state is shared at the global 055 * level for a particular thread, so all instances of SLogger, regardless 056 * of classloader, will share the same capture buffer. 057 * 058 * In addition to the usual Log levels, this class supports the following levels: 059 * - HERE: A log message indicating that the code has reached a certain point. This is mostly useful for tracing execution. 060 * - ENTER: A log message indicating that the code is entering a certain segment, such as a method. This is logged at DEBUG level. 061 * - EXIT: A log message indicating that the code is exiting a certain segment, such as a method. This is logged at DEBUG level. 062 * 063 * The 'S' stands for Super. Super Logger. 064 */ 065public class SLogger implements org.apache.commons.logging.Log { 066 067 /** 068 * An enumeration of log levels to replace the one in log4j 069 */ 070 public enum Level { 071 /** 072 * Trace level logs 073 */ 074 TRACE, 075 /** 076 * Debug level logs 077 */ 078 DEBUG, 079 /** 080 * Info level logs 081 */ 082 INFO, 083 /** 084 * Warning level logs 085 */ 086 WARN, 087 /** 088 * Error level logs 089 */ 090 ERROR, 091 /** 092 * Fatal level logs 093 */ 094 FATAL, 095 /** 096 * Log messages indicating that the code has reached a certain point 097 */ 098 HERE, 099 /** 100 * Log messages indicating that the code is entering a certain segment 101 */ 102 ENTER, 103 /** 104 * Log messages indicating that the code is exiting a certain segment 105 */ 106 EXIT 107 } 108 109 /** 110 * Container class to format an object for logging. The format is only derived 111 * when {@link Formatter#toString()} is called, meaning that if you log one of these 112 * and the log level is not enabled, a slow string conversion will never occur. 113 * 114 * Null values are transformed into the special string '(null)'. 115 * 116 * Formatted values are cached after the first format operation, even if the 117 * underlying object is modified. 118 * 119 * The following types are handled by the Formatter: 120 * 121 * - null 122 * - Strings 123 * - Arrays of Objects 124 * - Arrays of StackTraceElements 125 * - Collections of Objects 126 * - Maps 127 * - Dates and Calendars 128 * - XML {@link Document}s 129 * - Various SailPointObjects 130 * 131 * Nested objects (e.g., the items in a list) are also passed through Formatter. 132 */ 133 public static class Formatter implements Supplier<String> { 134 /** 135 * Formats the given object for logging 136 * @param obj The object to format 137 * @return The formatted object 138 */ 139 public static String format(Object obj) { 140 return new Formatter(obj).toString(); 141 } 142 143 /** 144 * The formatted value cached after the first call to toString() 145 */ 146 private String formattedValue; 147 148 /** 149 * The object to format. 150 */ 151 private final Object item; 152 153 /** 154 * Creates a new formatter. 155 * 156 * @param Item The item to format. 157 */ 158 public Formatter(Object Item) { 159 this.item = Item; 160 this.formattedValue = null; 161 } 162 163 @SuppressWarnings("unchecked") 164 private Map<String, Object> createLogMap(SailPointObject value) { 165 Map<String, Object> map = new ListOrderedMap(); 166 map.put("class", value.getClass().getSimpleName()); 167 map.put("id", value.getId()); 168 if (!(value instanceof Link)) { 169 map.put("name", value.getName()); 170 } 171 return map; 172 } 173 174 /** 175 * Formats an object. Equivalent to format(valueToFormat, false). 176 * 177 * @param valueToFormat The object to format. 178 * @return The formatted version of that object. 179 */ 180 private String formatInternal(Object valueToFormat) { 181 return format(valueToFormat, false); 182 } 183 184 /** 185 * Formats an object according to multiple type-specific format rules. 186 * 187 * @param valueToFormat The object to format. 188 * @return The formatted version of that object. 189 */ 190 private String format(Object valueToFormat, boolean indent) { 191 StringBuilder value = new StringBuilder(); 192 193 if (valueToFormat == null) { 194 value.append("(null)"); 195 } else if (valueToFormat instanceof Supplier) { 196 // Note that the Supplier's code is only invoked at this point 197 value.append(format(((Supplier<?>) valueToFormat).get(), indent)); 198 } else if (valueToFormat instanceof String) { 199 value.append(valueToFormat); 200 } else if (valueToFormat instanceof bsh.This) { 201 String namespaceName = "???"; 202 if (((This) valueToFormat).getNameSpace() != null) { 203 namespaceName = ((This) valueToFormat).getNameSpace().getName(); 204 } 205 value.append("bsh.This[namespace=").append(namespaceName).append("]"); 206 } else if (valueToFormat instanceof StackTraceElement[]) { 207 for (StackTraceElement element : (StackTraceElement[]) valueToFormat) { 208 value.append("\n at "); 209 value.append(element); 210 } 211 } else if (valueToFormat instanceof Throwable) { 212 Throwable t = (Throwable)valueToFormat; 213 try (StringWriter target = new StringWriter()) { 214 try (PrintWriter printWriter = new PrintWriter(target)) { 215 t.printStackTrace(printWriter); 216 } 217 value.append(target); 218 } catch(IOException e) { 219 return "Exception printing object of type Throwable: " + e; 220 } 221 } else if (valueToFormat.getClass().isArray()) { 222 value.append("[\n"); 223 boolean first = true; 224 int length = Array.getLength(valueToFormat); 225 for (int i = 0; i < length; i++) { 226 if (!first) { 227 value.append(",\n"); 228 } 229 value.append(" "); 230 value.append(format(Array.get(valueToFormat, i), true)); 231 first = false; 232 } 233 value.append("\n]"); 234 } else if (valueToFormat instanceof Collection) { 235 value.append("[\n"); 236 boolean first = true; 237 for (Object arg : (Collection<?>) valueToFormat) { 238 if (!first) { 239 value.append(",\n"); 240 } 241 value.append(" ").append(format(arg, true)); 242 first = false; 243 } 244 value.append("\n]"); 245 } else if (valueToFormat instanceof Map) { 246 value.append("{\n"); 247 boolean first = true; 248 for (Map.Entry<?, ?> entry : new TreeMap<Object, Object>((Map<?, ?>) valueToFormat).entrySet()) { 249 if (!first) { 250 value.append(",\n"); 251 } 252 value.append(" "); 253 value.append(formatInternal(entry.getKey())); 254 value.append("="); 255 value.append(format(entry.getValue(), true)); 256 first = false; 257 } 258 value.append("\n}"); 259 } else if (valueToFormat instanceof Date) { 260 DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); 261 value.append(format.format((Date) valueToFormat)); 262 } else if (valueToFormat instanceof Calendar) { 263 value.append(formatInternal(((Calendar) valueToFormat).getTime())); 264 } else if (valueToFormat instanceof Document) { 265 try { 266 Transformer transformer = TransformerFactory.newInstance().newTransformer(); 267 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 268 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); 269 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { 270 transformer.transform(new DOMSource((Document) valueToFormat), new StreamResult(output)); 271 value.append(output); 272 } 273 } catch (Exception e) { 274 return "Exception transforming object of type Document " + e; 275 } 276 } else if (valueToFormat instanceof Identity) { 277 value.append("Identity").append(formatInternal(toLogMap((Identity) valueToFormat))); 278 } else if (valueToFormat instanceof Bundle) { 279 value.append("Bundle").append(formatInternal(toLogMap((Bundle)valueToFormat))); 280 } else if (valueToFormat instanceof ManagedAttribute) { 281 value.append("ManagedAttribute").append(formatInternal(toLogMap((ManagedAttribute) valueToFormat))); 282 } else if (valueToFormat instanceof Link) { 283 value.append("Link").append(formatInternal(toLogMap((Link) valueToFormat))); 284 } else if (valueToFormat instanceof Application) { 285 value.append("Application").append(formatInternal(toLogMap((Application) valueToFormat))); 286 } else if (valueToFormat instanceof SailPointContext) { 287 value.append("SailPointContext[").append(valueToFormat.hashCode()).append(", username = ").append(((SailPointContext) valueToFormat).getUserName()).append("]"); 288 } else if (valueToFormat instanceof ProvisioningPlan) { 289 try { 290 value.append(ProvisioningPlan.getLoggingPlan((ProvisioningPlan) valueToFormat).toXml()); 291 } catch (GeneralException e) { 292 return "Exception transforming object of type " + valueToFormat.getClass().getName() + " to XML: " + e; 293 } 294 } else if (valueToFormat instanceof Filter) { 295 value.append("Filter[").append(((Filter) valueToFormat).getExpression(true)).append("]"); 296 } else if (valueToFormat instanceof AbstractXmlObject) { 297 try { 298 value.append(((AbstractXmlObject)valueToFormat).toXml()); 299 } catch (GeneralException e) { 300 return "Exception transforming object of type " + valueToFormat.getClass().getName() + " to XML: " + e; 301 } 302 } else { 303 value.append(valueToFormat); 304 } 305 306 String result = value.toString(); 307 if (indent) { 308 result = result.replace("\n", "\n" + TAB_SPACES).trim(); 309 } 310 311 return result; 312 } 313 314 /** 315 * Returns the formatted string of the item when invoked via the {@link Supplier} interface. 316 * This output will NOT be cached, unlike a call to toString(). 317 * 318 * @return The formatted string, freshly calculated 319 * @see Supplier#get() 320 */ 321 @Override 322 public String get() { 323 return formatInternal(this.item); 324 } 325 326 /** 327 * Converts the Identity to a Map for logging purposes 328 * @param value The Identity convert 329 * @return A Map containing some basic identity details 330 */ 331 private Map<String, Object> toLogMap(Identity value) { 332 Map<String, Object> map = createLogMap(value); 333 map.put("type", value.getType()); 334 map.put("displayName", value.getDisplayName()); 335 map.put("disabled", value.isDisabled() || value.isInactive()); 336 map.put("attributes", formatInternal(value.getAttributes())); 337 return map; 338 } 339 340 /** 341 * Converts the Link to a Map for logging purposes 342 * @param value The Link to convert 343 * @return A Map containing some basic Link details 344 */ 345 private Map<String, Object> toLogMap(Link value) { 346 Map<String, Object> map = createLogMap(value); 347 map.put("application", value.getApplicationName()); 348 map.put("nativeIdentity", value.getNativeIdentity()); 349 map.put("displayName", value.getDisplayName()); 350 map.put("disabled", value.isDisabled()); 351 return map; 352 } 353 354 /** 355 * Converts the Application to a Map for logging purposes 356 * @param value The Application to convert 357 * @return A Map containing some basic Application details 358 */ 359 private Map<String, Object> toLogMap(Application value) { 360 Map<String, Object> map = createLogMap(value); 361 map.put("authoritative", value.isAuthoritative()); 362 map.put("connector", value.getConnector()); 363 if (value.isInMaintenance()) { 364 map.put("maintenance", true); 365 } 366 return map; 367 } 368 369 /** 370 * Converts the Bundle / Role to a Map for logging purposes 371 * @param value The Bundle to convert 372 * @return A Map containing some basic Bundle details 373 */ 374 private Map<String, Object> toLogMap(Bundle value) { 375 Map<String, Object> map = createLogMap(value); 376 map.put("type", value.getType()); 377 map.put("displayName", value.getDisplayName()); 378 return map; 379 } 380 381 /** 382 * Converts the ManagedAttribute / Entitlement object to a Map for logging purposes 383 * @param value The MA to convert 384 * @return A Map containing some basic MA details 385 */ 386 private Map<String, Object> toLogMap(ManagedAttribute value) { 387 Map<String, Object> map = createLogMap(value); 388 map.put("application", value.getApplication().getName()); 389 map.put("attribute", value.getAttribute()); 390 map.put("value", value.getValue()); 391 map.put("displayName", value.getDisplayName()); 392 return map; 393 } 394 395 /** 396 * If the formatted value exists, the cached version will be returned. 397 * Otherwise, the format string will be calculated at this time, cached, 398 * and then returned. 399 * 400 * @see java.lang.Object#toString() 401 */ 402 @Override 403 public String toString() { 404 if (this.formattedValue == null) { 405 this.formattedValue = formatInternal(item); 406 } 407 return this.formattedValue; 408 } 409 } 410 411 public static final String CUSTOM_GLOBAL_CAPTURED_LOGS_TOKEN = "IIQCommon.SLogger.CapturedLogs"; 412 413 /** 414 * Spaces to use for tabs in stack traces 415 */ 416 private static final String TAB_SPACES = " "; 417 /** 418 * The context name, typically the class name 419 */ 420 private String contextName; 421 /** 422 * The context stack, which can be used to track nested contexts, 423 * such as method calls. The context stack will be prepended to 424 * all log messages if it is not empty. 425 */ 426 protected final Stack<String> contextStack; 427 /** 428 * The underlying logger to use. 429 */ 430 protected final Log logger; 431 /** 432 * The underlying output stream to use. 433 */ 434 protected final PrintStream out; 435 436 /** 437 * Creates a new logger with the given logger and print stream. 438 * @param logger the underlying Commons Logging logger to use 439 * @param out the underlying PrintStream to use 440 */ 441 protected SLogger(Log logger, PrintStream out) { 442 contextStack = new Stack<>(); 443 this.logger = logger; 444 this.out = out; 445 } 446 /** 447 * Creates a new logger. 448 * @param name The name to log messages for. Typically, this is a class name, but may be a rule library, etc 449 */ 450 public SLogger(String name) { 451 this(LogFactory.getLog(name), null); 452 453 this.contextName = name; 454 } 455 456 /** 457 * Creates a new logger. 458 * 459 * @param Owner The class to log messages for. 460 */ 461 public SLogger(Class<?> Owner) { 462 this(LogFactory.getLog(Owner), null); 463 464 this.contextName = Owner.getName(); 465 } 466 467 /** 468 * Wraps the given log4j logger with this logger 469 * 470 * @param WrapLog The logger to wrap 471 */ 472 public SLogger(Log WrapLog) { 473 this(WrapLog, null); 474 } 475 476 /** 477 * Creates a new logger. 478 * 479 * @param Out The output stream to 480 */ 481 public SLogger(PrintStream Out) { 482 this(null, Out); 483 } 484 485 /** 486 * Wraps the arguments for future formatting. The format string is not resolved 487 * at this time, meaning that the toString() is lazily evaluated. 488 * 489 * @param args The arguments for any place-holders in the message template. 490 * @return The formatted arguments. 491 */ 492 public static SLogger.Formatter[] format(Object[] args) { 493 if (args == null) { 494 return null; 495 } 496 Formatter[] argsCopy = new Formatter[args.length]; 497 for (int i = 0; i < args.length; i++) { 498 argsCopy[i] = new Formatter(args[i]); 499 } 500 return argsCopy; 501 } 502 503 /** 504 * Gets the captured logs ref for the given thread from the CustomGlobal. 505 * If it does not exist, it will be created. Using CustomGlobal and only 506 * core JDK classes for this allows this trace to be shared across plugins 507 * that may contain an obfuscated instance of this class. 508 * 509 * If the ThreadLocal does not already exist, it will be created in a block 510 * synchronized on the CustomGlobal class itself, preventing double creation. 511 * 512 * The contents of the AtomicReference may be null if capture() has not been 513 * invoked yet, or if reset() has been invoked. 514 * 515 * @return The AtomicReference containing the StringBuilder for captured logs for this thread 516 */ 517 @SuppressWarnings("unchecked") 518 protected static AtomicReference<StringBuilder> getCapturedLogsRef() { 519 ThreadLocal<AtomicReference<StringBuilder>> threadLocal = (ThreadLocal<AtomicReference<StringBuilder>>) CustomGlobal.get(CUSTOM_GLOBAL_CAPTURED_LOGS_TOKEN); 520 if (threadLocal == null) { 521 synchronized(CustomGlobal.class) { 522 threadLocal = (ThreadLocal<AtomicReference<StringBuilder>>) CustomGlobal.get(CUSTOM_GLOBAL_CAPTURED_LOGS_TOKEN); 523 if (threadLocal == null) { 524 threadLocal = InheritableThreadLocal.withInitial(AtomicReference::new); 525 CustomGlobal.put(CUSTOM_GLOBAL_CAPTURED_LOGS_TOKEN, threadLocal); 526 } 527 } 528 } 529 return threadLocal.get(); 530 } 531 532 /** 533 * Renders the MessageTemplate using the given arguments 534 * @param messageTemplate The message template 535 * @param args The arguments 536 * @return The resolved message template 537 */ 538 public static String renderMessage(String messageTemplate, Object[] args) { 539 if (args != null && args.length > 0) { 540 MessageFormat template = new MessageFormat(messageTemplate); 541 return template.format(args); 542 } else { 543 return messageTemplate; 544 } 545 } 546 547 /** 548 * Appends the standard prefix to the captured logs, including timestamp and context stack 549 */ 550 protected void appendStandardPrefix() { 551 String prefix = getTimestamp(); 552 builder().append(prefix); 553 builder().append("[").append(contextName).append("] "); 554 if (!contextStack.isEmpty()) { 555 String ctx = "[" + contextStack.stream().reduce((a, b) -> a + " > " + b).orElse("") + "] "; 556 builder().append(ctx); 557 } 558 } 559 560 /** 561 * Returns the StringBuilder used to capture logs, initializing it if necessary 562 * @return The StringBuilder for captured logs 563 */ 564 protected StringBuilder builder() { 565 AtomicReference<StringBuilder> ref = getCapturedLogsRef(); 566 if (ref.get() == null) { 567 ref.set(new StringBuilder()); 568 } 569 return ref.get(); 570 } 571 572 /** 573 * Begins capturing logs. This will clear any previously captured logs by 574 * replacing the current StringBuilder with a new one. 575 */ 576 public void capture() { 577 builder().append("Starting capture at ").append(getTimestamp()).append("\n\n"); 578 } 579 580 /** 581 * @see Log#debug(Object) 582 * @param arg0 The message to log 583 */ 584 @Override 585 public void debug(Object arg0) { 586 debug("{0}", arg0); 587 } 588 589 /** 590 * @see Log#debug(Object, Throwable) 591 * @param arg0 The message to log 592 * @param arg1 The exception to log 593 */ 594 @Override 595 public void debug(Object arg0, Throwable arg1) { 596 debug("{0} {1}", arg0, arg1); 597 } 598 599 /** 600 * Logs an debugging message. 601 * 602 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 603 * @param Args The arguments for any place-holders in the message template. 604 */ 605 public void debug(String MessageTemplate, Object... Args) { 606 log(Level.DEBUG, MessageTemplate, (Object[]) format(Args)); 607 } 608 609 /** 610 * Logs an entry message for a segment of code. This will log at DEBUG level if 611 * the system configuration property IIQCommon.SLogger.EnterExitEnabled is set to true. 612 * 613 * @param value The value to log as the 'location', e.g., a method name, a chunk of code, etc 614 */ 615 public void enter(String value) { 616 if (isDebugEnabled()) { 617 log(Level.ENTER, "Entering: {0}", value); 618 } 619 } 620 621 /** 622 * Logs an entry message for a Beanshell function. This will log at DEBUG level if 623 * * the system configuration property IIQCommon.SLogger.EnterExitEnabled is set to true. 624 * 625 * @param bshThis The Beanshell 'this' object, which contains the namespace name. 626 */ 627 public void enter(bsh.This bshThis) { 628 if (bshThis != null && bshThis.getNameSpace() != null) { 629 enter(bshThis.getNameSpace().getName()); 630 } else { 631 enter("(unknown Beanshell function)"); 632 } 633 } 634 635 /** 636 * @see Log#error(Object) 637 */ 638 @Override 639 public void error(Object arg0) { 640 error("{0}", arg0); 641 } 642 643 /** 644 * @see Log#error(Object, Throwable) 645 */ 646 @Override 647 public void error(Object arg0, Throwable arg1) { 648 error("{0}", arg0); 649 handleException(arg1); 650 } 651 652 /** 653 * Logs an error message. 654 * 655 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 656 * @param Args The arguments for any place-holders in the message template. 657 */ 658 public void error(String MessageTemplate, Object... Args) { 659 log(Level.ERROR, MessageTemplate, (Object[]) format(Args)); 660 } 661 662 /** 663 * Logs an exit message for a segment of code. This will log at DEBUG level if 664 * the system configuration property IIQCommon.SLogger.EnterExitEnabled is set to true. 665 * 666 * @param value The value to log as the 'location', e.g., a method name, a chunk of code, etc 667 */ 668 public void exit(String value) { 669 if (isDebugEnabled()) { 670 log(Level.EXIT, "Exiting: {0}", value); 671 } 672 } 673 674 /** 675 * Logs an exit message for a Beanshell function. This will log at DEBUG level if 676 * the system configuration property IIQCommon.SLogger.EnterExitEnabled is set to true. 677 * 678 * @param bshThis The Beanshell 'this' object, which contains the namespace name. 679 */ 680 public void exit(bsh.This bshThis) { 681 if (bshThis != null && bshThis.getNameSpace() != null) { 682 exit(bshThis.getNameSpace().getName()); 683 } else { 684 exit("(unknown Beanshell function)"); 685 } 686 } 687 688 @Override 689 public void fatal(Object arg0) { 690 fatal("{0}", arg0); 691 } 692 693 @Override 694 public void fatal(Object arg0, Throwable arg1) { 695 fatal("{0}", arg0); 696 handleException(arg1); 697 } 698 699 /** 700 * Logs a fatal error message. 701 * 702 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 703 * @param Args The arguments for any place-holders in the message template. 704 */ 705 public void fatal(String MessageTemplate, Object... Args) { 706 log(Level.FATAL, MessageTemplate, format(Args)); 707 } 708 709 /** 710 * Returns the captured logs as a String 711 * @return The captured logs 712 */ 713 public String getCapturedLogs() { 714 return builder().toString(); 715 } 716 717 /** 718 * Gets the internal Log object wrapped by this class 719 * @return The internal log object 720 */ 721 /*package*/ Log getLogger() { 722 return logger; 723 } 724 725 /** 726 * Generates a timestamp string for captured log entries 727 * @return The timestamp string 728 */ 729 protected String getTimestamp() { 730 ZonedDateTime now = ZonedDateTime.now(); 731 String timestamp = now.toString(); 732 return "[" + timestamp + "] "; 733 } 734 735 /** 736 * Handles an exception. 737 * 738 * @param Error The exception to handle. 739 */ 740 public synchronized void handleException(Throwable Error) { 741 save(Error); 742 if (logger != null) { 743 logger.error(Error.toString(), Error); 744 } else if (out != null) { 745 Error.printStackTrace(out); 746 } 747 } 748 749 /** 750 * Logs a message "Here", along with a custom suffix, indicating that the code 751 * has reached a certain point. This will only be logged (at INFO level) if 752 * the system configuration property IIQCommon.SLogger.HereEnabled is set to true. 753 * 754 * @param value The value to log as the 'location', e.g., a method name, a chunk of code, etc. 755 */ 756 public void here(String value) { 757 log(Level.HERE, "Here: {0}", value); 758 } 759 760 @Override 761 public void info(Object arg0) { 762 info("{0}", arg0); 763 } 764 765 @Override 766 public void info(Object arg0, Throwable arg1) { 767 info("{0} {1}", arg0, arg1); 768 } 769 770 /** 771 * Logs an informational message. 772 * 773 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 774 * @param Args The arguments for any place-holders in the message template. 775 */ 776 public void info(String MessageTemplate, Object... Args) { 777 log(Level.INFO, MessageTemplate, format(Args)); 778 } 779 780 /** 781 * Returns true if log capturing is currently active 782 * @return true if capturing is active 783 */ 784 public boolean isCapturing() { 785 AtomicReference<StringBuilder> ref = getCapturedLogsRef(); 786 return ref.get() != null; 787 } 788 789 /** 790 * @see Log#isDebugEnabled() 791 */ 792 @Override 793 public boolean isDebugEnabled() { 794 if (logger != null) { 795 return logger.isDebugEnabled(); 796 } else { 797 return true; 798 } 799 } 800 801 /** 802 * Returns true if the logger is enabled for the given level. Unfortunately, Commons Logging doesn't have a friendly isEnabledFor(Level) type API, since some of its downstream loggers may not either. 803 * 804 * @param log The logger to check 805 * @param logLevel The level to check 806 * @return true if the logger is enabled 807 */ 808 private boolean isEnabledFor(Log log, Level logLevel) { 809 switch(logLevel) { 810 case TRACE: 811 return log.isTraceEnabled(); 812 case DEBUG: 813 return log.isDebugEnabled(); 814 case ENTER: 815 case EXIT: 816 Configuration sc1 = Configuration.getSystemConfig(); 817 boolean enterExitEnabled = sc1 != null && sc1.getBoolean("IIQCommon.SLogger.EnterExitEnabled", false); 818 return enterExitEnabled && log.isDebugEnabled(); 819 case INFO: 820 return log.isInfoEnabled(); 821 case WARN: 822 return log.isWarnEnabled(); 823 case ERROR: 824 return log.isErrorEnabled(); 825 case FATAL: 826 return log.isFatalEnabled(); 827 case HERE: 828 Configuration sc2 = Configuration.getSystemConfig(); 829 boolean hereEnabled = sc2 != null && sc2.getBoolean("IIQCommon.SLogger.HereEnabled", false); 830 return hereEnabled && log.isInfoEnabled(); 831 } 832 return false; 833 } 834 835 /** 836 * @see Log#isErrorEnabled() 837 */ 838 @Override 839 public boolean isErrorEnabled() { 840 if (logger != null) { 841 return logger.isErrorEnabled(); 842 } else { 843 return true; 844 } 845 } 846 847 /** 848 * @see Log#isFatalEnabled() 849 */ 850 @Override 851 public boolean isFatalEnabled() { 852 if (logger != null) { 853 return logger.isFatalEnabled(); 854 } else { 855 return true; 856 } 857 } 858 859 /** 860 * @see Log#isInfoEnabled() 861 */ 862 @Override 863 public boolean isInfoEnabled() { 864 if (logger != null) { 865 return logger.isInfoEnabled(); 866 } else { 867 return true; 868 } 869 } 870 871 /** 872 * @see Log#isTraceEnabled() 873 */ 874 @Override 875 public boolean isTraceEnabled() { 876 if (logger != null) { 877 return logger.isTraceEnabled(); 878 } else { 879 return true; 880 } 881 } 882 883 /** 884 * @see Log#isWarnEnabled() 885 */ 886 @Override 887 public boolean isWarnEnabled() { 888 if (logger != null) { 889 return logger.isWarnEnabled(); 890 } else { 891 return true; 892 } 893 } 894 895 /** 896 * Logs the message at the appropriate level according to the Commons Logging API 897 * @param logLevel The log level to log at 898 * @param message The message to log 899 */ 900 public void log(Level logLevel, String message) { 901 switch(logLevel) { 902 case TRACE: 903 logger.trace(message); 904 break; 905 case DEBUG: 906 case ENTER: 907 case EXIT: 908 logger.debug(message); 909 break; 910 case INFO: 911 case HERE: 912 logger.info(message); 913 break; 914 case WARN: 915 logger.warn(message); 916 break; 917 case ERROR: 918 logger.error(message); 919 break; 920 case FATAL: 921 logger.fatal(message); 922 break; 923 } 924 } 925 926 /** 927 * Logs a message. 928 * 929 * @param logLevel The level to log the message at. 930 * @param messageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 931 * @param args The arguments for any place-holders in the message template. 932 */ 933 protected void log(Level logLevel, String messageTemplate, Object... args) { 934 save(logLevel, messageTemplate, args); 935 if (logger != null) { 936 if (isEnabledFor(logger, logLevel)) { 937 String message = renderMessage(messageTemplate, args); 938 if (!contextStack.isEmpty()) { 939 String ctx = "[" + contextStack.stream().reduce((a, b) -> a + " > " + b).orElse("") + "]"; 940 message = ctx + " " + message; 941 } 942 log(logLevel, message); 943 } 944 } else if (out != null) { 945 String message = renderMessage(messageTemplate, args); 946 if (!contextStack.isEmpty()) { 947 String ctx = "[" + contextStack.stream().reduce((a, b) -> a + " > " + b).orElse("") + "]"; 948 message = ctx + " " + message; 949 } 950 out.println(message); 951 } 952 } 953 954 /** 955 * Pops the top value off the context stack 956 * @return The value popped off the stack, or null if the stack is empty. 957 */ 958 public String pop() { 959 if (contextStack.isEmpty()) { 960 return null; 961 } 962 return contextStack.pop(); 963 } 964 965 /** 966 * Pushes a value onto the context stack. This will be logged with the message 967 * @param value The context value to add 968 */ 969 public void push(String value) { 970 if (value != null) { 971 contextStack.push(value); 972 } 973 } 974 975 /** 976 * Resets/clears the captured logs 977 */ 978 public void reset() { 979 AtomicReference<StringBuilder> ref = getCapturedLogsRef(); 980 ref.set(new StringBuilder()); 981 } 982 983 /** 984 * Saves an error message and stack trace to the captured logs. If the 985 * error cannot be rendered for whatever reason, a message will be printed 986 * to standard error as a last resort. 987 * 988 * @param error The exception to handle. 989 */ 990 @SuppressWarnings("UseOfSystemOutOrSystemErr") 991 protected void save(Throwable error) { 992 if (!isCapturing()) { 993 return; 994 } 995 996 appendStandardPrefix(); 997 998 builder().append("[THROWABLE] "); 999 1000 try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { 1001 error.printStackTrace(pw); 1002 pw.flush(); 1003 String stackTrace = sw.toString().replace("\t", TAB_SPACES); 1004 builder().append(stackTrace).append("\n"); 1005 } catch (Exception e) { 1006 System.err.println("LAST RESORT: Error logging stack trace: " + e.getMessage()); 1007 error.printStackTrace(); 1008 } 1009 } 1010 1011 /** 1012 * Saves a log message to the captured logs 1013 * 1014 * @param LogLevel The level to log the message at. 1015 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 1016 * @param Args The arguments for any place-holders in the message template. 1017 */ 1018 protected void save(Level LogLevel, String MessageTemplate, Object[] Args) { 1019 if (!isCapturing()) { 1020 return; 1021 } 1022 1023 appendStandardPrefix(); 1024 1025 String formattedMessage = String.format(MessageTemplate, Args); 1026 builder().append("[").append(LogLevel.name()).append("] ").append(formattedMessage).append("\n"); 1027 } 1028 1029 @Override 1030 public void trace(Object arg0) { 1031 trace("{0}", arg0); 1032 } 1033 1034 @Override 1035 public void trace(Object arg0, Throwable arg1) { 1036 trace("{0} {1}", arg0, arg1); 1037 } 1038 1039 /** 1040 * Logs a trace message. 1041 * 1042 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 1043 * @param Args The arguments for any place-holders in the message template. 1044 */ 1045 public void trace(String MessageTemplate, Object... Args) { 1046 log(Level.TRACE, MessageTemplate, format(Args)); 1047 } 1048 1049 /** 1050 * @see Log#warn(Object) 1051 */ 1052 @Override 1053 public void warn(Object arg0) { 1054 warn("{0}", arg0); 1055 } 1056 1057 /** 1058 * @see Log#warn(Object, Throwable) 1059 */ 1060 @Override 1061 public void warn(Object arg0, Throwable arg1) { 1062 warn("{0} {1}", arg0, arg1); 1063 } 1064 1065 /** 1066 * Logs a warning message. 1067 * 1068 * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}. 1069 * @param Args The arguments for any place-holders in the message template. 1070 */ 1071 public void warn(String MessageTemplate, Object... Args) { 1072 log(Level.WARN, MessageTemplate, format(Args)); 1073 } 1074 1075}