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:
*
*
* {@link #getLevel()}
* {@link #getMdc()}
* {@link #getMarkers()}
* {@link #getKeyValuePairs()}
* {@link #getThrowable()}
* {@link #getMessage()}
* {@link #getArguments()}
*
*
* 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.
*
*
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 mdc;
private final List markers;
private final List keyValuePairs;
private final Optional throwable;
private final String message;
private final List arguments;
private final Optional 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 mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.TRACE, mdc, message, arguments);
}
public static LoggingEvent trace(
final Map 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 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 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 mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.DEBUG, mdc, message, arguments);
}
public static LoggingEvent debug(
final Map 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 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 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 mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.INFO, mdc, message, arguments);
}
public static LoggingEvent info(
final Map 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 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 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 mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.WARN, mdc, message, arguments);
}
public static LoggingEvent warn(
final Map 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 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 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 mdc, final String message, final Object... arguments) {
return new LoggingEvent(Level.ERROR, mdc, message, arguments);
}
public static LoggingEvent error(
final Map 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 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 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 mdc) {
List markers = event.getMarkers();
List 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 mdc,
final String message,
final Object... arguments) {
this(level, mdc, empty(), empty(), message, arguments);
}
public LoggingEvent(
final Level level,
final Map 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 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 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 mdc,
final Optional marker,
final Optional 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 creatingLogger,
final Level level,
final Map mdc,
final List markers,
final List keyValuePairs,
final Optional 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 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 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 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 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 getArguments() {
return arguments;
}
public Optional 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
+ '}';
}
}