LoggingEvent.java
package com.github.valfirst.slf4jtest;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;
import java.io.PrintStream;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.SortedMap;
import java.util.TreeMap;
import org.slf4j.Marker;
import org.slf4j.event.KeyValuePair;
import org.slf4j.event.Level;
import org.slf4j.helpers.MessageFormatter;
/**
* Representation of a call to a logger for test assertion purposes. The contract of {@link
* #equals(Object)} and {@link #hashCode} is that they compare the results of:
*
* <ul>
* <li>{@link #getLevel()}
* <li>{@link #getMdc()}
* <li>{@link #getMarkers()}
* <li>{@link #getKeyValuePairs()}
* <li>{@link #getThrowable()}
* <li>{@link #getMessage()}
* <li>{@link #getArguments()}
* </ul>
*
* <p>They do NOT compare the results of {@link #getTimestamp()}, {@link #getCreatingLogger()} or
* {@link #getThreadContextClassLoader()} as this would render it impractical to create appropriate
* expected {@link LoggingEvent}s to compare against.
*
* <p>Constructors and convenient static factory methods exist to create {@link LoggingEvent}s with
* appropriate defaults. These are not documented further as they should be self-evident.
*/
@SuppressWarnings({"PMD.ExcessivePublicCount", "PMD.TooManyMethods"})
public class LoggingEvent {
private static final DateTimeFormatter ISO_FORMAT =
new DateTimeFormatterBuilder().appendInstant(3).toFormatter();
private static final Object[] emptyObjectArray = {};
private final Level level;
private final SortedMap<String, String> mdc;
private final List<Marker> markers;
private final List<KeyValuePair> keyValuePairs;
private final Optional<Throwable> throwable;
private final String message;
private final List<Object> arguments;
private final Optional<TestLogger> creatingLogger;
private final Instant timestamp = Instant.now();
private final String threadName = Thread.currentThread().getName();
private final ClassLoader threadContextClassLoader =
Thread.currentThread().getContextClassLoader();
public static LoggingEvent trace(final String message, final Object... arguments) {
return new LoggingEvent(Level.TRACE, message, arguments);
}
public static LoggingEvent trace(
final Throwable throwable, final String message, final Object... arguments) {
return new LoggingEvent(Level.TRACE, throwable, message, arguments);
}
public static LoggingEvent trace(
final Marker marker, final String message, final Object... arguments) {
return new LoggingEvent(Level.TRACE, marker, message, arguments);
}
public static LoggingEvent trace(
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.TRACE, marker, throwable, message, arguments);
}
public static LoggingEvent trace(
final Map<String, String> mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.TRACE, mdc, message, arguments);
}
public static LoggingEvent trace(
final Map<String, String> mdc,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.TRACE, mdc, throwable, message, arguments);
}
public static LoggingEvent trace(
final Map<String, String> mdc,
final Marker marker,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.TRACE, mdc, marker, message, arguments);
}
public static LoggingEvent trace(
final Map<String, String> mdc,
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.TRACE, mdc, marker, throwable, message, arguments);
}
public static LoggingEvent debug(final String message, final Object... arguments) {
return new LoggingEvent(Level.DEBUG, message, arguments);
}
public static LoggingEvent debug(
final Throwable throwable, final String message, final Object... arguments) {
return new LoggingEvent(Level.DEBUG, throwable, message, arguments);
}
public static LoggingEvent debug(
final Marker marker, final String message, final Object... arguments) {
return new LoggingEvent(Level.DEBUG, marker, message, arguments);
}
public static LoggingEvent debug(
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.DEBUG, marker, throwable, message, arguments);
}
public static LoggingEvent debug(
final Map<String, String> mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.DEBUG, mdc, message, arguments);
}
public static LoggingEvent debug(
final Map<String, String> mdc,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.DEBUG, mdc, throwable, message, arguments);
}
public static LoggingEvent debug(
final Map<String, String> mdc,
final Marker marker,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.DEBUG, mdc, marker, message, arguments);
}
public static LoggingEvent debug(
final Map<String, String> mdc,
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.DEBUG, mdc, marker, throwable, message, arguments);
}
public static LoggingEvent info(final String message, final Object... arguments) {
return new LoggingEvent(Level.INFO, message, arguments);
}
public static LoggingEvent info(
final Throwable throwable, final String message, final Object... arguments) {
return new LoggingEvent(Level.INFO, throwable, message, arguments);
}
public static LoggingEvent info(
final Marker marker, final String message, final Object... arguments) {
return new LoggingEvent(Level.INFO, marker, message, arguments);
}
public static LoggingEvent info(
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.INFO, marker, throwable, message, arguments);
}
public static LoggingEvent info(
final Map<String, String> mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.INFO, mdc, message, arguments);
}
public static LoggingEvent info(
final Map<String, String> mdc,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.INFO, mdc, throwable, message, arguments);
}
public static LoggingEvent info(
final Map<String, String> mdc,
final Marker marker,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.INFO, mdc, marker, message, arguments);
}
public static LoggingEvent info(
final Map<String, String> mdc,
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.INFO, mdc, marker, throwable, message, arguments);
}
public static LoggingEvent warn(final String message, final Object... arguments) {
return new LoggingEvent(Level.WARN, message, arguments);
}
public static LoggingEvent warn(
final Throwable throwable, final String message, final Object... arguments) {
return new LoggingEvent(Level.WARN, throwable, message, arguments);
}
public static LoggingEvent warn(
final Marker marker, final String message, final Object... arguments) {
return new LoggingEvent(Level.WARN, marker, message, arguments);
}
public static LoggingEvent warn(
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.WARN, marker, throwable, message, arguments);
}
public static LoggingEvent warn(
final Map<String, String> mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.WARN, mdc, message, arguments);
}
public static LoggingEvent warn(
final Map<String, String> mdc,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.WARN, mdc, throwable, message, arguments);
}
public static LoggingEvent warn(
final Map<String, String> mdc,
final Marker marker,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.WARN, mdc, marker, message, arguments);
}
public static LoggingEvent warn(
final Map<String, String> mdc,
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.WARN, mdc, marker, throwable, message, arguments);
}
public static LoggingEvent error(final String message, final Object... arguments) {
return new LoggingEvent(Level.ERROR, message, arguments);
}
public static LoggingEvent error(
final Throwable throwable, final String message, final Object... arguments) {
return new LoggingEvent(Level.ERROR, throwable, message, arguments);
}
public static LoggingEvent error(
final Marker marker, final String message, final Object... arguments) {
return new LoggingEvent(Level.ERROR, marker, message, arguments);
}
public static LoggingEvent error(
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.ERROR, marker, throwable, message, arguments);
}
public static LoggingEvent error(
final Map<String, String> mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.ERROR, mdc, message, arguments);
}
public static LoggingEvent error(
final Map<String, String> mdc,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.ERROR, mdc, throwable, message, arguments);
}
public static LoggingEvent error(
final Map<String, String> mdc,
final Marker marker,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.ERROR, mdc, marker, message, arguments);
}
public static LoggingEvent error(
final Map<String, String> mdc,
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
return new LoggingEvent(Level.ERROR, mdc, marker, throwable, message, arguments);
}
/**
* Create a {@link LoggingEvent} from an SLF4J {@link org.slf4j.event.LoggingEvent}.
*
* @since 3.0.0
*/
public static LoggingEvent fromSlf4jEvent(org.slf4j.event.LoggingEvent event) {
return fromSlf4jEvent(event, Collections.emptyMap());
}
/**
* Create a {@link LoggingEvent} with an MDC from an SLF4J {@link org.slf4j.event.LoggingEvent}.
*
* @since 3.0.0
*/
public static LoggingEvent fromSlf4jEvent(
org.slf4j.event.LoggingEvent event, Map<String, String> mdc) {
List<Marker> markers = event.getMarkers();
List<KeyValuePair> keyValuePairs = event.getKeyValuePairs();
Object[] arguments = event.getArgumentArray();
return new LoggingEvent(
empty(),
event.getLevel(),
mdc,
markers == null
? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList<>(markers)),
keyValuePairs == null
? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList<>(keyValuePairs)),
ofNullable(event.getThrowable()),
event.getMessage(),
arguments == null ? emptyObjectArray : arguments);
}
public LoggingEvent(final Level level, final String message, final Object... arguments) {
this(level, Collections.emptySortedMap(), empty(), empty(), message, arguments);
}
public LoggingEvent(
final Level level,
final Throwable throwable,
final String message,
final Object... arguments) {
this(level, Collections.emptySortedMap(), empty(), ofNullable(throwable), message, arguments);
}
public LoggingEvent(
final Level level, final Marker marker, final String message, final Object... arguments) {
this(level, Collections.emptySortedMap(), ofNullable(marker), empty(), message, arguments);
}
public LoggingEvent(
final Level level,
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
this(
level,
Collections.emptySortedMap(),
ofNullable(marker),
ofNullable(throwable),
message,
arguments);
}
public LoggingEvent(
final Level level,
final Map<String, String> mdc,
final String message,
final Object... arguments) {
this(level, mdc, empty(), empty(), message, arguments);
}
public LoggingEvent(
final Level level,
final Map<String, String> mdc,
final Throwable throwable,
final String message,
final Object... arguments) {
this(level, mdc, empty(), ofNullable(throwable), message, arguments);
}
public LoggingEvent(
final Level level,
final Map<String, String> mdc,
final Marker marker,
final String message,
final Object... arguments) {
this(level, mdc, ofNullable(marker), empty(), message, arguments);
}
public LoggingEvent(
final Level level,
final Map<String, String> mdc,
final Marker marker,
final Throwable throwable,
final String message,
final Object... arguments) {
this(level, mdc, ofNullable(marker), ofNullable(throwable), message, arguments);
}
private LoggingEvent(
final Level level,
final Map<String, String> mdc,
final Optional<Marker> marker,
final Optional<Throwable> throwable,
final String message,
final Object... arguments) {
this(
empty(),
level,
mdc,
marker.map(Collections::singletonList).orElseGet(Collections::emptyList),
Collections.emptyList(),
throwable,
message,
arguments);
}
LoggingEvent(
final Optional<TestLogger> creatingLogger,
final Level level,
final Map<String, String> mdc,
final List<Marker> markers,
final List<KeyValuePair> keyValuePairs,
final Optional<Throwable> throwable,
final String message,
final Object... arguments) {
super();
this.creatingLogger = creatingLogger;
this.level = requireNonNull(level);
this.mdc =
requireNonNull(mdc).isEmpty()
? Collections.emptySortedMap()
: Collections.unmodifiableSortedMap(new TreeMap<>(mdc));
this.markers = markers;
this.keyValuePairs = keyValuePairs;
this.throwable = requireNonNull(throwable);
this.message = message;
this.arguments =
arguments.length == 0
? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList<>(Arrays.asList(arguments)));
}
public Level getLevel() {
return level;
}
/**
* Get the MDC of the event. For events created by {@link TestLogger}, this is an unmodifiable
* copy of the MDC of the thread when the event was created. For events constructed directly, this
* is unmodifiable copy of the MDC passed to the constructor, if any. If no MDC was used for
* construction, the copy is an empty map. The copy is a {@link SortedMap}, in order to make it
* easier to spot discrepancies in case an assertion fails. Natural ordering of the keys is used.
*/
public SortedMap<String, String> getMdc() {
return mdc;
}
/**
* Get the marker of the event.
*
* @deprecated As events created using the SLF4J fluent API can contain multiple markers, this
* method is deprecated in favor of {@link #getMarkers}.
* @throws IllegalStateException if the event has more than one marker.
*/
@Deprecated
public Optional<Marker> getMarker() {
if (markers.isEmpty()) {
return empty();
}
if (markers.size() == 1) {
return Optional.of(markers.get(0));
}
throw new IllegalStateException("LoggingEvent has more than one marker");
}
/**
* Get the markers of the event. If the event has no markers, an empty list is returned.
*
* @return an unmodifiable copy of the markers when the event was created.
* @since 3.0.0
*/
public List<Marker> getMarkers() {
return markers;
}
/**
* Get the key/value pairs of the event. If the event has no key/value pairs, an empty list is
* returned.
*
* @return an unmodifiable copy of the key/value pairs when the event was created.
* @since 3.0.0
*/
public List<KeyValuePair> getKeyValuePairs() {
return keyValuePairs;
}
public String getMessage() {
return message;
}
/**
* Get the arguments to the event.
*
* @return an unmodifiable copy of the arguments when the event was created.
*/
public List<Object> getArguments() {
return arguments;
}
public Optional<Throwable> getThrowable() {
return throwable;
}
/**
* @return the logger that created this logging event.
* @throws IllegalStateException if this logging event was not created by a logger
*/
public TestLogger getCreatingLogger() {
return creatingLogger.get();
}
/**
* @return the time at which this logging event was created
*/
public Instant getTimestamp() {
return timestamp;
}
/**
* @return the name of the thread that created this logging event
*/
public String getThreadName() {
return threadName;
}
/**
* @return the Thread Context Classloader used when this logging event was created
*/
public ClassLoader getThreadContextClassLoader() {
return threadContextClassLoader;
}
void print() {
final PrintStream output = printStreamForLevel();
output.println(formatLogStatement());
throwable.ifPresent(throwableToPrint -> throwableToPrint.printStackTrace(output));
}
private String formatLogStatement() {
return ISO_FORMAT.format(getTimestamp())
+ " ["
+ getThreadName()
+ "] "
+ getLevel()
+ safeLoggerName()
+ " - "
+ getFormattedMessage();
}
private String safeLoggerName() {
return creatingLogger.map(logger -> " " + logger.getName()).orElse("");
}
public String getFormattedMessage() {
Object[] argumentsWithNulls = getArguments().toArray();
return MessageFormatter.arrayFormat(getMessage(), argumentsWithNulls).getMessage();
}
private PrintStream printStreamForLevel() {
switch (level) {
case ERROR:
case WARN:
return System.err;
default:
return System.out;
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LoggingEvent that = (LoggingEvent) o;
return level == that.level
&& Objects.equals(mdc, that.mdc)
&& Objects.equals(markers, that.markers)
&& Objects.equals(keyValuePairs, that.keyValuePairs)
&& Objects.equals(throwable, that.throwable)
&& Objects.equals(message, that.message)
&& Objects.equals(arguments, that.arguments);
}
@Override
public int hashCode() {
return Objects.hash(level, mdc, markers, keyValuePairs, throwable, message, arguments);
}
@Override
public String toString() {
return "LoggingEvent{"
+ "level="
+ level
+ ", mdc="
+ mdc
+ ", markers="
+ markers
+ ", keyValuePairs="
+ keyValuePairs
+ ", throwable="
+ throwable
+ ", message="
+ (message == null ? "null" : '\'' + message + '\'')
+ ", arguments="
+ arguments
+ '}';
}
}