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}