InMemoryLogAppender.java

/**
 * Copyright (C) 2022 Christopher J. Stehno
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.github.cjstehno.testthings.slf4j;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import lombok.Getter;
import org.hamcrest.Matcher;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.stream.Stream;

import static org.hamcrest.CoreMatchers.any;

/**
 * An {@link ch.qos.logback.core.Appender} for the SLF4J logging library (with logback backend) that allows for verifying
 * results that may only appear in log messages.
 * <p>
 * To use this appender you register the classes to be monitored using the <code>loggedClass(Class)</code> configuration
 * method and then call the <code>attach()</code> method on the appender to hook it into the logging system.
 * <p>
 * You should ensure that the <code>detach()</code> method is also called when you are done testing so that the test code
 * is not left in the logger configuration.
 * <p>
 * Various methods are provided to read through and filter the captured log messages.
 * <p>
 * See also the {@link io.github.cjstehno.testthings.junit.LogAppenderExtension} for a simple means of using this in tests.
 */
public class InMemoryLogAppender extends AppenderBase<ILoggingEvent> {

    @Getter private final List<ILoggingEvent> events = new CopyOnWriteArrayList<>();
    private final AppenderConfigImpl config;

    /**
     * Creates an in-memory Logback {@link ch.qos.logback.core.Appender} for testing with the provided configuration.
     *
     * @param consumer the configuration
     */
    public InMemoryLogAppender(final Consumer<AppenderConfig> consumer) {
        this(new AppenderConfigImpl());

        if (consumer != null) {
            consumer.accept(config);
        }
    }

    /**
     * Creates a log appender with the provided configuration object.
     *
     * @param config the configuration object
     */
    public InMemoryLogAppender(final AppenderConfig config) {
        this.config = (AppenderConfigImpl) config;
    }

    /**
     * Must be called to initiate the log event recording - to register the log appender with the logger.
     */
    public void attach() {
        start();
        config.forEachLogger(lgr -> lgr.addAppender(this));
    }

    /**
     * Should be called when done using the appender to clean up resources.
     */
    public void detach() {
        config.forEachLogger(lgr -> lgr.detachAppender(this));
        stop();
    }

    /**
     * Called when a logging event is to be added to the appender.
     *
     * @param event the logging event
     */
    @Override protected void append(final ILoggingEvent event) {
        if (config.matches(event)) {
            events.add(event);
        }
    }

    /**
     * Counts the number of captured events matching the provided matcher.
     *
     * @param matcher the event matcher
     * @return the count of matching events
     */
    public int count(final Matcher<ILoggingEvent> matcher) {
        return (int) events.stream().filter(matcher::matches).count();
    }

    /**
     * Counts all captured logging events.
     *
     * @return the count
     */
    public int count() {
        return count(any(ILoggingEvent.class));
    }

    /**
     * Retrieves all captured logging events matching the provided matcher.
     *
     * @param matcher the matcher
     * @return the list of matching events
     */
    public List<ILoggingEvent> events(final Matcher<ILoggingEvent> matcher) {
        return events.stream().filter(matcher::matches).toList();
    }

    /**
     * Retrieves all captured logging events.
     *
     * @return all captured logging events
     */
    public List<ILoggingEvent> events() {
        return events(any(ILoggingEvent.class));
    }

    /**
     * Returns <code>true</code> if there is at least one captured logging event matching the provided matcher.
     *
     * @param matcher the matcher
     * @return true if there is a matching event
     */
    public boolean hasEvent(final Matcher<ILoggingEvent> matcher) {
        return count(matcher) > 0;
    }

    /**
     * Retrieves the captured logging events matching the provided matcher, as a Stream.
     *
     * @param matcher the matcher
     * @return the stream of matching events
     */
    public Stream<ILoggingEvent> stream(final Matcher<ILoggingEvent> matcher) {
        return events.stream().filter(matcher::matches);
    }

    /**
     * Retrieves the captured logging events as a Stream.
     *
     * @return the stream of events
     */
    public Stream<ILoggingEvent> stream() {
        return stream(any(ILoggingEvent.class));
    }
}