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 lombok.NonNull; 008import org.apache.commons.logging.Log; 009import org.apache.commons.logging.LogFactory; 010import sailpoint.tools.Message; 011import sailpoint.tools.Util; 012 013import java.io.IOException; 014import java.io.PrintWriter; 015import java.io.Serializable; 016import java.io.StringWriter; 017import java.text.SimpleDateFormat; 018import java.time.format.DateTimeFormatter; 019import java.util.Date; 020import java.util.HashMap; 021import java.util.Map; 022import java.util.Objects; 023import java.util.function.Supplier; 024 025/** 026 * Implements a timestamped log message similar to what would be captured by a 027 * logging framework. However, this one can be passed around as a VO for UI and 028 * other purposes. 029 * 030 * It is serialized to JSON by {@link StampedMessageSerializer}. 031 */ 032@JsonSerialize(using = StampedMessageSerializer.class) 033@JsonClassDescription("A message associated with a timestamp, akin to a log message from any logger tool") 034public class StampedMessage implements Serializable { 035 036 /** 037 * A Builder for a StampedMessage, allowing a fluent API if needed 038 */ 039 public static final class Builder { 040 private Throwable exception; 041 private LogLevel level; 042 private String message; 043 private String thread; 044 private long timestamp; 045 046 private Builder() { 047 } 048 049 /** 050 * Builds a new {@link StampedMessage} 051 * @return A newly constructed {@link StampedMessage} 052 */ 053 public StampedMessage build() { 054 return new StampedMessage(this); 055 } 056 057 public Builder withException(Throwable val) { 058 exception = val; 059 return this; 060 } 061 062 public Builder withLevel(LogLevel val) { 063 level = val; 064 return this; 065 } 066 067 public Builder withMessage(String val) { 068 message = val; 069 return this; 070 } 071 072 public Builder withThread(String val) { 073 thread = val; 074 return this; 075 } 076 077 public Builder withTimestamp(long val) { 078 timestamp = val; 079 return this; 080 } 081 } 082 083 /** 084 * The log logging logger 085 */ 086 private static final Log metaLog = LogFactory.getLog(StampedMessage.class); 087 088 /** 089 * The exception associated with this log message 090 */ 091 @JsonPropertyDescription("The exception associated with this message, if any") 092 private final Throwable exception; 093 094 /** 095 * The log level 096 */ 097 @JsonPropertyDescription("The log level of the message") 098 private final LogLevel level; 099 100 /** 101 * The string message 102 */ 103 @JsonPropertyDescription("The message string") 104 private final String message; 105 106 /** 107 * The name of the thread producing this message 108 */ 109 @JsonPropertyDescription("The name of the thread producing this message") 110 private final String thread; 111 112 /** 113 * The timestamp in milliseconds that this object was created 114 */ 115 @JsonPropertyDescription("The message timestamp, in Unix epoch milliseconds") 116 private final long timestamp; 117 118 /** 119 * Creates a basic log of the given level with the given string message 120 * @param level The log level 121 * @param message The message 122 */ 123 public StampedMessage(@NonNull LogLevel level, String message) { 124 this(level, message, null); 125 } 126 127 /** 128 * Full constructor taking a level, a message, and an exception. The log timestamp will be 129 * the system timestamp when this constructor is invoked. 130 * 131 * @param level The log level 132 * @param message The string message 133 * @param exception The exception (or null) 134 */ 135 public StampedMessage(@NonNull LogLevel level, String message, Throwable exception) { 136 this.timestamp = System.currentTimeMillis(); 137 this.thread = Thread.currentThread().getName(); 138 this.level = Objects.requireNonNull(level); 139 this.message = message; 140 this.exception = exception; 141 } 142 143 /** 144 * Creates a log from a SailPoint Message object 145 * @param message A Sailpoint message 146 */ 147 public StampedMessage(@NonNull Message message) { 148 this(message, null); 149 } 150 151 /** 152 * Creates a log from a SailPoint Message object and a throwable 153 * @param message The message object 154 * @param throwable The error object 155 */ 156 public StampedMessage(@NonNull Message message, Throwable throwable) { 157 this(message.isError() ? LogLevel.ERROR : (message.isWarning() ? LogLevel.WARN : LogLevel.INFO), message.getMessage(), throwable); 158 } 159 160 /** 161 * Creates a basic INFO level log with the given string message 162 * @param message The string message 163 */ 164 public StampedMessage(@NonNull String message) { 165 this(LogLevel.INFO, message, null); 166 } 167 168 /** 169 * Creates a log of ERROR level with the given message and Throwable 170 * @param message The message to log 171 * @param throwable The throwable to log 172 */ 173 public StampedMessage(String message, Throwable throwable) { 174 this(LogLevel.ERROR, message, throwable); 175 } 176 177 /** 178 * Creates a log of ERROR level with the given Throwable, using its getMessage() as the message 179 * @param throwable The throwable to log 180 */ 181 public StampedMessage(@NonNull Throwable throwable) { 182 this(LogLevel.ERROR, throwable.getMessage(), throwable); 183 } 184 185 /** 186 * Used by the builder to construct a new {@link StampedMessage} 187 * @param builder The Builder object populated by this 188 */ 189 private StampedMessage(Builder builder) { 190 exception = builder.exception; 191 level = builder.level; 192 message = builder.message; 193 thread = builder.thread; 194 timestamp = builder.timestamp; 195 } 196 197 /** 198 * Constructs a new, empty {@link Builder} 199 * @return The builder 200 */ 201 public static Builder builder() { 202 return new Builder(); 203 } 204 205 /** 206 * Constructs a builder from an existing {@link StampedMessage} 207 * @param copy The item to copy 208 * @return The builder object 209 */ 210 public static Builder builder(StampedMessage copy) { 211 Builder builder = new Builder(); 212 builder.exception = copy.getException(); 213 builder.level = copy.getLevel(); 214 builder.message = copy.getMessage(); 215 builder.thread = copy.getThread(); 216 builder.timestamp = copy.getTimestamp(); 217 return builder; 218 } 219 220 /** 221 * Gets the exception associated with this LogMessage 222 * @return The exception, or null 223 */ 224 public Throwable getException() { 225 return exception; 226 } 227 228 /** 229 * Gets the log level associated with this LogMessage 230 * @return The log level, never null 231 */ 232 public LogLevel getLevel() { 233 return level; 234 } 235 236 /** 237 * Gets the log message 238 * @return The log message 239 */ 240 public String getMessage() { 241 return message; 242 } 243 244 /** 245 * Returns the thread associated with this 246 * @return The thread associated with this message 247 */ 248 public String getThread() { 249 return thread; 250 } 251 252 /** 253 * Gets the millisecond timestamp when this object was created 254 * @return The millisecond timestamp of creation 255 */ 256 public long getTimestamp() { 257 return timestamp; 258 } 259 260 /** 261 * Returns true if the log level of this message is exactly DEBUG 262 */ 263 public boolean isDebug() { 264 return Util.nullSafeEq(this.getLevel(), LogLevel.DEBUG); 265 } 266 267 /** 268 * Returns true if the log level of this message is exactly ERROR 269 */ 270 public boolean isError() { 271 return Util.nullSafeEq(this.getLevel(), LogLevel.ERROR); 272 } 273 274 /** 275 * Returns true if the log level of this message is exactly INFO 276 */ 277 public boolean isInfo() { 278 return Util.nullSafeEq(this.getLevel(), LogLevel.INFO); 279 } 280 281 /** 282 * Returns true if the log level of this message is INFO, WARN, or ERROR 283 */ 284 public boolean isInfoOrHigher() { 285 return this.isInfo() || this.isWarningOrHigher(); 286 } 287 288 /** 289 * Returns true if the log level of this message is WARN 290 */ 291 public boolean isWarning() { 292 return Util.nullSafeEq(this.getLevel(), LogLevel.WARN); 293 } 294 295 /** 296 * Returns true if the log level of this message is WARN or ERROR 297 */ 298 public boolean isWarningOrHigher() { 299 return this.isWarning() || this.isError(); 300 } 301 302 /** 303 * Transforms this object to a Map of strings, suitable for passing to a client as 304 * JSON or XML. This map will contain the following keys: 305 * 306 * - 'timestamp': The value of {@link #getTimestamp()} converted to a string 307 * - 'formattedDate': The value of {@link #getTimestamp()}, formatted using {@link DateTimeFormatter#ISO_INSTANT} 308 * - 'message': The message, exactly as passed to this class 309 * - 'level': The string name of the {@link LogLevel}, e.g., 'DEBUG' 310 * - 'exception': If present, the output of {@link Throwable#printStackTrace(PrintWriter)} 311 * 312 * The 'exception' key will be missing if this object does not return a value for {@link #getException()}. 313 * 314 * @return This object as a Map 315 */ 316 public Map<String, String> toMap() { 317 DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT; 318 Map<String, String> map = new HashMap<>(); 319 map.put("timestamp", String.valueOf(this.timestamp)); 320 map.put("formattedDate", formatter.format(new Date(timestamp).toInstant())); 321 map.put("message", message); 322 map.put("level", level.name()); 323 map.put("thread", thread); 324 325 if (exception != null) { 326 try (StringWriter target = new StringWriter()) { 327 try (PrintWriter printWriter = new PrintWriter(target)) { 328 exception.printStackTrace(printWriter); 329 } 330 map.put("exception", target.toString()); 331 } catch(IOException e) { 332 metaLog.error("Unable to print object of type " + exception.getClass(), e); 333 } 334 } 335 336 return map; 337 } 338 339 /** 340 * Transforms this object into a log4j-type log string. 341 * 342 * Example: `2024-04-15 14:42:31.112 [Thread-10] [DEBUG] Your message` 343 */ 344 public String toString() { 345 StringBuilder value = new StringBuilder(); 346 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 347 value.append(sdf.format(new Date(timestamp))); 348 value.append(" ["); 349 value.append(this.thread); 350 value.append("] "); 351 value.append(String.format("%5s", level.toString())); 352 value.append(" "); 353 value.append(message); 354 355 if (exception != null) { 356 value.append("\n"); 357 try (StringWriter target = new StringWriter()) { 358 try (PrintWriter printWriter = new PrintWriter(target)) { 359 exception.printStackTrace(printWriter); 360 } 361 value.append(target); 362 } catch(IOException e) { 363 metaLog.error("Unable to print object of type " + exception.getClass(), e); 364 } 365 } 366 367 return value.toString(); 368 } 369}