View Javadoc
1   package com.github.valfirst.slf4jtest;
2   
3   import static java.util.Objects.requireNonNull;
4   import static java.util.Optional.empty;
5   import static java.util.Optional.ofNullable;
6   
7   import java.io.PrintStream;
8   import java.time.Instant;
9   import java.time.format.DateTimeFormatter;
10  import java.time.format.DateTimeFormatterBuilder;
11  import java.util.ArrayList;
12  import java.util.Arrays;
13  import java.util.Collections;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.Objects;
17  import java.util.Optional;
18  import java.util.SortedMap;
19  import java.util.TreeMap;
20  import org.slf4j.Marker;
21  import org.slf4j.event.KeyValuePair;
22  import org.slf4j.event.Level;
23  import org.slf4j.helpers.MessageFormatter;
24  
25  /**
26   * Representation of a call to a logger for test assertion purposes. The contract of {@link
27   * #equals(Object)} and {@link #hashCode} is that they compare the results of:
28   *
29   * <ul>
30   *   <li>{@link #getLevel()}
31   *   <li>{@link #getMdc()}
32   *   <li>{@link #getMarkers()}
33   *   <li>{@link #getKeyValuePairs()}
34   *   <li>{@link #getThrowable()}
35   *   <li>{@link #getMessage()}
36   *   <li>{@link #getArguments()}
37   * </ul>
38   *
39   * <p>They do NOT compare the results of {@link #getTimestamp()}, {@link #getCreatingLogger()} or
40   * {@link #getThreadContextClassLoader()} as this would render it impractical to create appropriate
41   * expected {@link LoggingEvent}s to compare against.
42   *
43   * <p>Constructors and convenient static factory methods exist to create {@link LoggingEvent}s with
44   * appropriate defaults. These are not documented further as they should be self-evident.
45   */
46  @SuppressWarnings({"PMD.ExcessivePublicCount", "PMD.TooManyMethods"})
47  public class LoggingEvent {
48      private static final DateTimeFormatter ISO_FORMAT =
49              new DateTimeFormatterBuilder().appendInstant(3).toFormatter();
50      private static final Object[] emptyObjectArray = {};
51  
52      private final Level level;
53      private final SortedMap<String, String> mdc;
54      private final List<Marker> markers;
55      private final List<KeyValuePair> keyValuePairs;
56      private final Optional<Throwable> throwable;
57      private final String message;
58      private final List<Object> arguments;
59  
60      private final Optional<TestLogger> creatingLogger;
61      private final Instant timestamp = Instant.now();
62      private final String threadName = Thread.currentThread().getName();
63      private final ClassLoader threadContextClassLoader =
64              Thread.currentThread().getContextClassLoader();
65  
66      public static LoggingEvent trace(final String message, final Object... arguments) {
67          return new LoggingEvent(Level.TRACE, message, arguments);
68      }
69  
70      public static LoggingEvent trace(
71              final Throwable throwable, final String message, final Object... arguments) {
72          return new LoggingEvent(Level.TRACE, throwable, message, arguments);
73      }
74  
75      public static LoggingEvent trace(
76              final Marker marker, final String message, final Object... arguments) {
77          return new LoggingEvent(Level.TRACE, marker, message, arguments);
78      }
79  
80      public static LoggingEvent trace(
81              final Marker marker,
82              final Throwable throwable,
83              final String message,
84              final Object... arguments) {
85          return new LoggingEvent(Level.TRACE, marker, throwable, message, arguments);
86      }
87  
88      public static LoggingEvent trace(
89              final Map<String, String> mdc, final String message, final Object... arguments) {
90          return new LoggingEvent(Level.TRACE, mdc, message, arguments);
91      }
92  
93      public static LoggingEvent trace(
94              final Map<String, String> mdc,
95              final Throwable throwable,
96              final String message,
97              final Object... arguments) {
98          return new LoggingEvent(Level.TRACE, mdc, throwable, message, arguments);
99      }
100 
101     public static LoggingEvent trace(
102             final Map<String, String> mdc,
103             final Marker marker,
104             final String message,
105             final Object... arguments) {
106         return new LoggingEvent(Level.TRACE, mdc, marker, message, arguments);
107     }
108 
109     public static LoggingEvent trace(
110             final Map<String, String> mdc,
111             final Marker marker,
112             final Throwable throwable,
113             final String message,
114             final Object... arguments) {
115         return new LoggingEvent(Level.TRACE, mdc, marker, throwable, message, arguments);
116     }
117 
118     public static LoggingEvent debug(final String message, final Object... arguments) {
119         return new LoggingEvent(Level.DEBUG, message, arguments);
120     }
121 
122     public static LoggingEvent debug(
123             final Throwable throwable, final String message, final Object... arguments) {
124         return new LoggingEvent(Level.DEBUG, throwable, message, arguments);
125     }
126 
127     public static LoggingEvent debug(
128             final Marker marker, final String message, final Object... arguments) {
129         return new LoggingEvent(Level.DEBUG, marker, message, arguments);
130     }
131 
132     public static LoggingEvent debug(
133             final Marker marker,
134             final Throwable throwable,
135             final String message,
136             final Object... arguments) {
137         return new LoggingEvent(Level.DEBUG, marker, throwable, message, arguments);
138     }
139 
140     public static LoggingEvent debug(
141             final Map<String, String> mdc, final String message, final Object... arguments) {
142         return new LoggingEvent(Level.DEBUG, mdc, message, arguments);
143     }
144 
145     public static LoggingEvent debug(
146             final Map<String, String> mdc,
147             final Throwable throwable,
148             final String message,
149             final Object... arguments) {
150         return new LoggingEvent(Level.DEBUG, mdc, throwable, message, arguments);
151     }
152 
153     public static LoggingEvent debug(
154             final Map<String, String> mdc,
155             final Marker marker,
156             final String message,
157             final Object... arguments) {
158         return new LoggingEvent(Level.DEBUG, mdc, marker, message, arguments);
159     }
160 
161     public static LoggingEvent debug(
162             final Map<String, String> mdc,
163             final Marker marker,
164             final Throwable throwable,
165             final String message,
166             final Object... arguments) {
167         return new LoggingEvent(Level.DEBUG, mdc, marker, throwable, message, arguments);
168     }
169 
170     public static LoggingEvent info(final String message, final Object... arguments) {
171         return new LoggingEvent(Level.INFO, message, arguments);
172     }
173 
174     public static LoggingEvent info(
175             final Throwable throwable, final String message, final Object... arguments) {
176         return new LoggingEvent(Level.INFO, throwable, message, arguments);
177     }
178 
179     public static LoggingEvent info(
180             final Marker marker, final String message, final Object... arguments) {
181         return new LoggingEvent(Level.INFO, marker, message, arguments);
182     }
183 
184     public static LoggingEvent info(
185             final Marker marker,
186             final Throwable throwable,
187             final String message,
188             final Object... arguments) {
189         return new LoggingEvent(Level.INFO, marker, throwable, message, arguments);
190     }
191 
192     public static LoggingEvent info(
193             final Map<String, String> mdc, final String message, final Object... arguments) {
194         return new LoggingEvent(Level.INFO, mdc, message, arguments);
195     }
196 
197     public static LoggingEvent info(
198             final Map<String, String> mdc,
199             final Throwable throwable,
200             final String message,
201             final Object... arguments) {
202         return new LoggingEvent(Level.INFO, mdc, throwable, message, arguments);
203     }
204 
205     public static LoggingEvent info(
206             final Map<String, String> mdc,
207             final Marker marker,
208             final String message,
209             final Object... arguments) {
210         return new LoggingEvent(Level.INFO, mdc, marker, message, arguments);
211     }
212 
213     public static LoggingEvent info(
214             final Map<String, String> mdc,
215             final Marker marker,
216             final Throwable throwable,
217             final String message,
218             final Object... arguments) {
219         return new LoggingEvent(Level.INFO, mdc, marker, throwable, message, arguments);
220     }
221 
222     public static LoggingEvent warn(final String message, final Object... arguments) {
223         return new LoggingEvent(Level.WARN, message, arguments);
224     }
225 
226     public static LoggingEvent warn(
227             final Throwable throwable, final String message, final Object... arguments) {
228         return new LoggingEvent(Level.WARN, throwable, message, arguments);
229     }
230 
231     public static LoggingEvent warn(
232             final Marker marker, final String message, final Object... arguments) {
233         return new LoggingEvent(Level.WARN, marker, message, arguments);
234     }
235 
236     public static LoggingEvent warn(
237             final Marker marker,
238             final Throwable throwable,
239             final String message,
240             final Object... arguments) {
241         return new LoggingEvent(Level.WARN, marker, throwable, message, arguments);
242     }
243 
244     public static LoggingEvent warn(
245             final Map<String, String> mdc, final String message, final Object... arguments) {
246         return new LoggingEvent(Level.WARN, mdc, message, arguments);
247     }
248 
249     public static LoggingEvent warn(
250             final Map<String, String> mdc,
251             final Throwable throwable,
252             final String message,
253             final Object... arguments) {
254         return new LoggingEvent(Level.WARN, mdc, throwable, message, arguments);
255     }
256 
257     public static LoggingEvent warn(
258             final Map<String, String> mdc,
259             final Marker marker,
260             final String message,
261             final Object... arguments) {
262         return new LoggingEvent(Level.WARN, mdc, marker, message, arguments);
263     }
264 
265     public static LoggingEvent warn(
266             final Map<String, String> mdc,
267             final Marker marker,
268             final Throwable throwable,
269             final String message,
270             final Object... arguments) {
271         return new LoggingEvent(Level.WARN, mdc, marker, throwable, message, arguments);
272     }
273 
274     public static LoggingEvent error(final String message, final Object... arguments) {
275         return new LoggingEvent(Level.ERROR, message, arguments);
276     }
277 
278     public static LoggingEvent error(
279             final Throwable throwable, final String message, final Object... arguments) {
280         return new LoggingEvent(Level.ERROR, throwable, message, arguments);
281     }
282 
283     public static LoggingEvent error(
284             final Marker marker, final String message, final Object... arguments) {
285         return new LoggingEvent(Level.ERROR, marker, message, arguments);
286     }
287 
288     public static LoggingEvent error(
289             final Marker marker,
290             final Throwable throwable,
291             final String message,
292             final Object... arguments) {
293         return new LoggingEvent(Level.ERROR, marker, throwable, message, arguments);
294     }
295 
296     public static LoggingEvent error(
297             final Map<String, String> mdc, final String message, final Object... arguments) {
298         return new LoggingEvent(Level.ERROR, mdc, message, arguments);
299     }
300 
301     public static LoggingEvent error(
302             final Map<String, String> mdc,
303             final Throwable throwable,
304             final String message,
305             final Object... arguments) {
306         return new LoggingEvent(Level.ERROR, mdc, throwable, message, arguments);
307     }
308 
309     public static LoggingEvent error(
310             final Map<String, String> mdc,
311             final Marker marker,
312             final String message,
313             final Object... arguments) {
314         return new LoggingEvent(Level.ERROR, mdc, marker, message, arguments);
315     }
316 
317     public static LoggingEvent error(
318             final Map<String, String> mdc,
319             final Marker marker,
320             final Throwable throwable,
321             final String message,
322             final Object... arguments) {
323         return new LoggingEvent(Level.ERROR, mdc, marker, throwable, message, arguments);
324     }
325 
326     /**
327      * Create a {@link LoggingEvent} from an SLF4J {@link org.slf4j.event.LoggingEvent}.
328      *
329      * @since 3.0.0
330      */
331     public static LoggingEvent fromSlf4jEvent(org.slf4j.event.LoggingEvent event) {
332         return fromSlf4jEvent(event, Collections.emptyMap());
333     }
334 
335     /**
336      * Create a {@link LoggingEvent} with an MDC from an SLF4J {@link org.slf4j.event.LoggingEvent}.
337      *
338      * @since 3.0.0
339      */
340     public static LoggingEvent fromSlf4jEvent(
341             org.slf4j.event.LoggingEvent event, Map<String, String> mdc) {
342         List<Marker> markers = event.getMarkers();
343         List<KeyValuePair> keyValuePairs = event.getKeyValuePairs();
344         Object[] arguments = event.getArgumentArray();
345         return new LoggingEvent(
346                 empty(),
347                 event.getLevel(),
348                 mdc,
349                 markers == null
350                         ? Collections.emptyList()
351                         : Collections.unmodifiableList(new ArrayList<>(markers)),
352                 keyValuePairs == null
353                         ? Collections.emptyList()
354                         : Collections.unmodifiableList(new ArrayList<>(keyValuePairs)),
355                 ofNullable(event.getThrowable()),
356                 event.getMessage(),
357                 arguments == null ? emptyObjectArray : arguments);
358     }
359 
360     public LoggingEvent(final Level level, final String message, final Object... arguments) {
361         this(level, Collections.emptySortedMap(), empty(), empty(), message, arguments);
362     }
363 
364     public LoggingEvent(
365             final Level level,
366             final Throwable throwable,
367             final String message,
368             final Object... arguments) {
369         this(level, Collections.emptySortedMap(), empty(), ofNullable(throwable), message, arguments);
370     }
371 
372     public LoggingEvent(
373             final Level level, final Marker marker, final String message, final Object... arguments) {
374         this(level, Collections.emptySortedMap(), ofNullable(marker), empty(), message, arguments);
375     }
376 
377     public LoggingEvent(
378             final Level level,
379             final Marker marker,
380             final Throwable throwable,
381             final String message,
382             final Object... arguments) {
383         this(
384                 level,
385                 Collections.emptySortedMap(),
386                 ofNullable(marker),
387                 ofNullable(throwable),
388                 message,
389                 arguments);
390     }
391 
392     public LoggingEvent(
393             final Level level,
394             final Map<String, String> mdc,
395             final String message,
396             final Object... arguments) {
397         this(level, mdc, empty(), empty(), message, arguments);
398     }
399 
400     public LoggingEvent(
401             final Level level,
402             final Map<String, String> mdc,
403             final Throwable throwable,
404             final String message,
405             final Object... arguments) {
406         this(level, mdc, empty(), ofNullable(throwable), message, arguments);
407     }
408 
409     public LoggingEvent(
410             final Level level,
411             final Map<String, String> mdc,
412             final Marker marker,
413             final String message,
414             final Object... arguments) {
415         this(level, mdc, ofNullable(marker), empty(), message, arguments);
416     }
417 
418     public LoggingEvent(
419             final Level level,
420             final Map<String, String> mdc,
421             final Marker marker,
422             final Throwable throwable,
423             final String message,
424             final Object... arguments) {
425         this(level, mdc, ofNullable(marker), ofNullable(throwable), message, arguments);
426     }
427 
428     private LoggingEvent(
429             final Level level,
430             final Map<String, String> mdc,
431             final Optional<Marker> marker,
432             final Optional<Throwable> throwable,
433             final String message,
434             final Object... arguments) {
435         this(
436                 empty(),
437                 level,
438                 mdc,
439                 marker.map(Collections::singletonList).orElseGet(Collections::emptyList),
440                 Collections.emptyList(),
441                 throwable,
442                 message,
443                 arguments);
444     }
445 
446     LoggingEvent(
447             final Optional<TestLogger> creatingLogger,
448             final Level level,
449             final Map<String, String> mdc,
450             final List<Marker> markers,
451             final List<KeyValuePair> keyValuePairs,
452             final Optional<Throwable> throwable,
453             final String message,
454             final Object... arguments) {
455         super();
456         this.creatingLogger = creatingLogger;
457         this.level = requireNonNull(level);
458         this.mdc =
459                 requireNonNull(mdc).isEmpty()
460                         ? Collections.emptySortedMap()
461                         : Collections.unmodifiableSortedMap(new TreeMap<>(mdc));
462         this.markers = markers;
463         this.keyValuePairs = keyValuePairs;
464         this.throwable = requireNonNull(throwable);
465         this.message = message;
466         this.arguments =
467                 arguments.length == 0
468                         ? Collections.emptyList()
469                         : Collections.unmodifiableList(new ArrayList<>(Arrays.asList(arguments)));
470     }
471 
472     public Level getLevel() {
473         return level;
474     }
475 
476     /**
477      * Get the MDC of the event. For events created by {@link TestLogger}, this is an unmodifiable
478      * copy of the MDC of the thread when the event was created. For events constructed directly, this
479      * is unmodifiable copy of the MDC passed to the constructor, if any. If no MDC was used for
480      * construction, the copy is an empty map. The copy is a {@link SortedMap}, in order to make it
481      * easier to spot discrepancies in case an assertion fails. Natural ordering of the keys is used.
482      */
483     public SortedMap<String, String> getMdc() {
484         return mdc;
485     }
486 
487     /**
488      * Get the marker of the event.
489      *
490      * @deprecated As events created using the SLF4J fluent API can contain multiple markers, this
491      *     method is deprecated in favor of {@link #getMarkers}.
492      * @throws IllegalStateException if the event has more than one marker.
493      */
494     @Deprecated
495     public Optional<Marker> getMarker() {
496         if (markers.isEmpty()) {
497             return empty();
498         }
499         if (markers.size() == 1) {
500             return Optional.of(markers.get(0));
501         }
502         throw new IllegalStateException("LoggingEvent has more than one marker");
503     }
504 
505     /**
506      * Get the markers of the event. If the event has no markers, an empty list is returned.
507      *
508      * @return an unmodifiable copy of the markers when the event was created.
509      * @since 3.0.0
510      */
511     public List<Marker> getMarkers() {
512         return markers;
513     }
514 
515     /**
516      * Get the key/value pairs of the event. If the event has no key/value pairs, an empty list is
517      * returned.
518      *
519      * @return an unmodifiable copy of the key/value pairs when the event was created.
520      * @since 3.0.0
521      */
522     public List<KeyValuePair> getKeyValuePairs() {
523         return keyValuePairs;
524     }
525 
526     public String getMessage() {
527         return message;
528     }
529 
530     /**
531      * Get the arguments to the event.
532      *
533      * @return an unmodifiable copy of the arguments when the event was created.
534      */
535     public List<Object> getArguments() {
536         return arguments;
537     }
538 
539     public Optional<Throwable> getThrowable() {
540         return throwable;
541     }
542 
543     /**
544      * @return the logger that created this logging event.
545      * @throws IllegalStateException if this logging event was not created by a logger
546      */
547     public TestLogger getCreatingLogger() {
548         return creatingLogger.get();
549     }
550 
551     /**
552      * @return the time at which this logging event was created
553      */
554     public Instant getTimestamp() {
555         return timestamp;
556     }
557 
558     /**
559      * @return the name of the thread that created this logging event
560      */
561     public String getThreadName() {
562         return threadName;
563     }
564 
565     /**
566      * @return the Thread Context Classloader used when this logging event was created
567      */
568     public ClassLoader getThreadContextClassLoader() {
569         return threadContextClassLoader;
570     }
571 
572     void print() {
573         final PrintStream output = printStreamForLevel();
574         output.println(formatLogStatement());
575         throwable.ifPresent(throwableToPrint -> throwableToPrint.printStackTrace(output));
576     }
577 
578     private String formatLogStatement() {
579         return ISO_FORMAT.format(getTimestamp())
580                 + " ["
581                 + getThreadName()
582                 + "] "
583                 + getLevel()
584                 + safeLoggerName()
585                 + " - "
586                 + getFormattedMessage();
587     }
588 
589     private String safeLoggerName() {
590         return creatingLogger.map(logger -> " " + logger.getName()).orElse("");
591     }
592 
593     public String getFormattedMessage() {
594         Object[] argumentsWithNulls = getArguments().toArray();
595         return MessageFormatter.arrayFormat(getMessage(), argumentsWithNulls).getMessage();
596     }
597 
598     private PrintStream printStreamForLevel() {
599         switch (level) {
600             case ERROR:
601             case WARN:
602                 return System.err;
603             default:
604                 return System.out;
605         }
606     }
607 
608     @Override
609     public boolean equals(Object o) {
610         if (this == o) {
611             return true;
612         }
613         if (o == null || getClass() != o.getClass()) {
614             return false;
615         }
616         LoggingEvent that = (LoggingEvent) o;
617         return level == that.level
618                 && Objects.equals(mdc, that.mdc)
619                 && Objects.equals(markers, that.markers)
620                 && Objects.equals(keyValuePairs, that.keyValuePairs)
621                 && Objects.equals(throwable, that.throwable)
622                 && Objects.equals(message, that.message)
623                 && Objects.equals(arguments, that.arguments);
624     }
625 
626     @Override
627     public int hashCode() {
628         return Objects.hash(level, mdc, markers, keyValuePairs, throwable, message, arguments);
629     }
630 
631     @Override
632     public String toString() {
633         return "LoggingEvent{"
634                 + "level="
635                 + level
636                 + ", mdc="
637                 + mdc
638                 + ", markers="
639                 + markers
640                 + ", keyValuePairs="
641                 + keyValuePairs
642                 + ", throwable="
643                 + throwable
644                 + ", message="
645                 + (message == null ? "null" : '\'' + message + '\'')
646                 + ", arguments="
647                 + arguments
648                 + '}';
649     }
650 }