001package com.identityworksllc.iiq.common.vo; 002 003import com.fasterxml.jackson.annotation.JsonClassDescription; 004import com.fasterxml.jackson.annotation.JsonPropertyDescription; 005import com.fasterxml.jackson.databind.annotation.JsonAppend; 006import com.fasterxml.jackson.databind.annotation.JsonSerialize; 007import org.apache.commons.logging.Log; 008import org.apache.commons.logging.LogFactory; 009import sailpoint.tools.Message; 010import sailpoint.tools.Util; 011 012import java.io.IOException; 013import java.io.PrintWriter; 014import java.io.Serializable; 015import java.io.StringWriter; 016import java.text.SimpleDateFormat; 017import java.time.format.DateTimeFormatter; 018import java.util.Date; 019import java.util.HashMap; 020import java.util.Map; 021import java.util.Objects; 022import java.util.function.Supplier; 023 024/** 025 * Implements a timestamped log message similar to what would be captured by a 026 * logging framework. However, this one can be passed around as a VO for UI and 027 * other purposes. 028 * 029 * It is serialized to JSON by {@link StampedMessageSerializer}. 030 */ 031@JsonSerialize(using = StampedMessageSerializer.class) 032@JsonClassDescription("A message associated with a timestamp, akin to a log message from any logger tool") 033public class StampedMessage implements Serializable { 034 035 /** 036 * The log logging logger 037 */ 038 private static final Log metaLog = LogFactory.getLog(StampedMessage.class); 039 040 /** 041 * The exception associated with this log message 042 */ 043 @JsonPropertyDescription("The exception associated with this message, if any") 044 private final Throwable exception; 045 046 /** 047 * The log level 048 */ 049 @JsonPropertyDescription("The log level of the message") 050 private final LogLevel level; 051 052 /** 053 * The string message 054 */ 055 @JsonPropertyDescription("The message string") 056 private final String message; 057 058 /** 059 * The name of the thread producing this message 060 */ 061 @JsonPropertyDescription("The name of the thread producing this message") 062 private final String thread; 063 064 /** 065 * The timestamp in milliseconds that this object was created 066 */ 067 @JsonPropertyDescription("The message timestamp, in Unix epoch milliseconds") 068 private final long timestamp; 069 070 /** 071 * Creates a basic log of the given level with the given string message 072 * @param level The log level 073 * @param message The message 074 */ 075 public StampedMessage(LogLevel level, String message) { 076 this(level, message, null); 077 } 078 079 /** 080 * Full constructor taking a level, a message, and an exception. The log timestamp will be 081 * the system timestamp when this constructor is invoked. 082 * 083 * @param level The log level 084 * @param message The string message 085 * @param exception The exception (or null) 086 */ 087 public StampedMessage(LogLevel level, String message, Throwable exception) { 088 this.timestamp = System.currentTimeMillis(); 089 this.thread = Thread.currentThread().getName(); 090 this.level = Objects.requireNonNull(level); 091 this.message = message; 092 this.exception = exception; 093 } 094 095 /** 096 * Creates a log from a SailPoint Message object 097 * @param message A Sailpoint message 098 */ 099 public StampedMessage(Message message) { 100 this(message, null); 101 } 102 103 /** 104 * Creates a log from a SailPoint Message object and a throwable 105 * @param message The message object 106 * @param throwable The error object 107 */ 108 public StampedMessage(Message message, Throwable throwable) { 109 this(message.isError() ? LogLevel.ERROR : (message.isWarning() ? LogLevel.WARN : LogLevel.INFO), message.getMessage(), throwable); 110 } 111 112 /** 113 * Creates a basic INFO level log with the given string message 114 * @param message The string message 115 */ 116 public StampedMessage(String message) { 117 this(LogLevel.INFO, message, null); 118 } 119 120 /** 121 * Creates a log of ERROR level with the given message and Throwable 122 * @param message The message to log 123 * @param throwable The throwable to log 124 */ 125 public StampedMessage(String message, Throwable throwable) { 126 this(LogLevel.ERROR, message, throwable); 127 } 128 129 /** 130 * Creates a log of ERROR level with the given Throwable, using its getMessage() as the message 131 * @param throwable The throwable to log 132 */ 133 public StampedMessage(Throwable throwable) { 134 this(LogLevel.ERROR, throwable.getMessage(), throwable); 135 } 136 137 /** 138 * Gets the exception associated with this LogMessage 139 * @return The exception, or null 140 */ 141 public Throwable getException() { 142 return exception; 143 } 144 145 /** 146 * Gets the log level associated with this LogMessage 147 * @return The log level, never null 148 */ 149 public LogLevel getLevel() { 150 return level; 151 } 152 153 /** 154 * Gets the log message 155 * @return The log message 156 */ 157 public String getMessage() { 158 return message; 159 } 160 161 /** 162 * Gets the millisecond timestamp when this object was created 163 * @return The millisecond timestamp of creation 164 */ 165 public long getTimestamp() { 166 return timestamp; 167 } 168 169 /** 170 * Returns true if the log level of this message is exactly DEBUG 171 */ 172 public boolean isDebug() { 173 return Util.nullSafeEq(this.getLevel(), LogLevel.DEBUG); 174 } 175 176 /** 177 * Returns true if the log level of this message is exactly ERROR 178 */ 179 public boolean isError() { 180 return Util.nullSafeEq(this.getLevel(), LogLevel.ERROR); 181 } 182 183 /** 184 * Returns true if the log level of this message is exactly INFO 185 */ 186 public boolean isInfo() { 187 return Util.nullSafeEq(this.getLevel(), LogLevel.INFO); 188 } 189 190 /** 191 * Returns true if the log level of this message is INFO, WARN, or ERROR 192 */ 193 public boolean isInfoOrHigher() { 194 return this.isInfo() || this.isWarningOrHigher(); 195 } 196 197 /** 198 * Returns true if the log level of this message is WARN 199 */ 200 public boolean isWarning() { 201 return Util.nullSafeEq(this.getLevel(), LogLevel.WARN); 202 } 203 204 /** 205 * Returns true if the log level of this message is WARN or ERROR 206 */ 207 public boolean isWarningOrHigher() { 208 return this.isWarning() || this.isError(); 209 } 210 211 /** 212 * Transforms this object to a Map of strings, suitable for passing to a client as 213 * JSON or XML. This map will contain the following keys: 214 * 215 * - 'timestamp': The value of {@link #getTimestamp()} converted to a string 216 * - 'formattedDate': The value of {@link #getTimestamp()}, formatted using {@link DateTimeFormatter#ISO_INSTANT} 217 * - 'message': The message, exactly as passed to this class 218 * - 'level': The string name of the {@link LogLevel}, e.g., 'DEBUG' 219 * - 'exception': If present, the output of {@link Throwable#printStackTrace(PrintWriter)} 220 * 221 * The 'exception' key will be missing if this object does not return a value for {@link #getException()}. 222 * 223 * @return This object as a Map 224 */ 225 public Map<String, String> toMap() { 226 DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT; 227 Map<String, String> map = new HashMap<>(); 228 map.put("timestamp", String.valueOf(this.timestamp)); 229 map.put("formattedDate", formatter.format(new Date(timestamp).toInstant())); 230 map.put("message", message); 231 map.put("level", level.name()); 232 map.put("thread", thread); 233 234 if (exception != null) { 235 try (StringWriter target = new StringWriter()) { 236 try (PrintWriter printWriter = new PrintWriter(target)) { 237 exception.printStackTrace(printWriter); 238 } 239 map.put("exception", target.toString()); 240 } catch(IOException e) { 241 metaLog.error("Unable to print object of type " + exception.getClass(), e); 242 } 243 } 244 245 return map; 246 } 247 248 /** 249 * Transforms this object into a log4j-type log string. 250 * 251 * Example: `2024-04-15 14:42:31.112 [Thread-10] [DEBUG] Your message` 252 */ 253 public String toString() { 254 StringBuilder value = new StringBuilder(); 255 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 256 value.append(sdf.format(new Date(timestamp))); 257 value.append(" ["); 258 value.append(this.thread); 259 value.append("] "); 260 value.append(String.format("%5s", level.toString())); 261 value.append(" "); 262 value.append(message); 263 264 if (exception != null) { 265 value.append("\n"); 266 try (StringWriter target = new StringWriter()) { 267 try (PrintWriter printWriter = new PrintWriter(target)) { 268 exception.printStackTrace(printWriter); 269 } 270 value.append(target); 271 } catch(IOException e) { 272 metaLog.error("Unable to print object of type " + exception.getClass(), e); 273 } 274 } 275 276 return value.toString(); 277 } 278}