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}