All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
net.goui.flogger.testing.log4j2.Log4jInterceptor Maven / Gradle / Ivy
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Copyright (c) 2023, David Beaumont (https://github.com/hagbard).
This program and the accompanying materials are made available under the terms of the
Eclipse Public License v. 2.0 available at https://www.eclipse.org/legal/epl-2.0, or the
Apache License, Version 2.0 available at https://www.apache.org/licenses/LICENSE-2.0.
SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package net.goui.flogger.testing.log4j2;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.logging.log4j.core.config.Property.EMPTY_ARRAY;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.function.Consumer;
import net.goui.flogger.testing.LevelClass;
import net.goui.flogger.testing.LogEntry;
import net.goui.flogger.testing.api.*;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Filter.Result;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.filter.LevelRangeFilter;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
/** Log interceptor for Log4J2 logging. */
public final class Log4jInterceptor implements LogInterceptor {
@AutoService(LogInterceptor.Factory.class)
public static final class Factory extends AbstractLogInterceptorFactory {
@Override
public LogInterceptor get() {
return Log4jInterceptor.create();
}
@Override
protected void configureUnderlyingLoggerForInfoLogging(String loggerName) {
Logger underlyingLogger = (Logger) LogManager.getLogger(loggerName);
Configurator.setLevel(underlyingLogger.getName(), Level.INFO);
underlyingLogger.setAdditive(false);
}
}
private static final MetadataExtractor DEFAULT_LOG4J_EXTRACTOR =
e -> DefaultFormatMetadataParser.parse(e.getMessage().getFormattedMessage());
public static LogInterceptor create() {
return create(DEFAULT_LOG4J_EXTRACTOR);
}
public static LogInterceptor create(MetadataExtractor metadataExtractor) {
return new Log4jInterceptor(metadataExtractor);
}
private final MetadataExtractor metadataExtractor;
private Log4jInterceptor(MetadataExtractor metadataExtractor) {
this.metadataExtractor = checkNotNull(metadataExtractor);
}
@Override
public Recorder attachTo(
String loggerName, LevelClass level, Consumer collector, String testId) {
// WARNING: Log4J is unintuitive with log level ordering (compared to JDK). A "high" level means
// high verbosity (i.e. what most people call "low level logging").
Level log4JLevel = toLog4JLevel(level);
LevelRangeFilter specifiedLevelAndAbove =
LevelRangeFilter.createFilter(
/* minLevel (null ==> max) */ null,
/* maxLevel */ log4JLevel,
/* onMatch (null ==> accept) */ Result.NEUTRAL,
/* onMismatch (null ==> deny) */ Result.DENY);
Appender appender =
new AbstractAppender(
"CapturingAppender",
specifiedLevelAndAbove,
/* layout */ null,
/* ignoreExceptions */ true,
EMPTY_ARRAY) {
@Override
public void append(LogEvent event) {
MessageAndMetadata mm = metadataExtractor.extract(event);
if (LogInterceptor.shouldCollect(mm, testId)) {
collector.accept(toLogEntry(event, mm));
}
}
};
// If the call to configureUnderlyingLoggerForInfoLogging() succeeded, we should be
// able to cast the context instance.
LoggerContext context = (LoggerContext) LogManager.getContext(false);
// The existing config for the logger name might be a parent config.
LoggerConfig oldConfig = context.getConfiguration().getLoggerConfig(loggerName);
// Add a new logger config or use the existing one.
LoggerConfig config =
loggerName.equals(oldConfig.getName())
? oldConfig
: new LoggerConfig(loggerName, log4JLevel, true);
// Actually adds an AppenderRef
config.addAppender(appender, null, null);
// Sets the level of the LoggerConfig
config.setLevel(log4JLevel);
context.getConfiguration().addLogger(loggerName, config);
context.updateLoggers();
return () -> {
try {
context.getConfiguration().removeLogger(loggerName);
context.updateLoggers();
} catch (RuntimeException e) {
// Ignored on close().
}
};
}
private LogEntry toLogEntry(LogEvent event, MessageAndMetadata mm) {
StackTraceElement source = event.getSource(); // nullable
Level log4jLevel = event.getLevel();
var ts = event.getInstant();
Instant timestamp = Instant.ofEpochSecond(ts.getEpochSecond(), ts.getNanoOfSecond());
ImmutableMap> metadata = mergeMdcValues(event, mm.metadata());
return LogEntry.of(
source != null ? source.getClassName() : null,
source != null ? source.getMethodName() : null,
log4jLevel.name(),
toLevelClass(log4jLevel),
timestamp,
event.getThreadId(),
mm.message(),
metadata,
event.getThrown());
}
private static ImmutableMap> mergeMdcValues(
LogEvent event, ImmutableMap> metadata) {
ReadOnlyStringMap mdc = event.getContextData();
if (!mdc.isEmpty()) {
LinkedHashMap> withMdc = new LinkedHashMap<>(metadata);
mdc.forEach(
(k, v, b) -> {
Object safeValue = toSafeValue(v);
if (safeValue != null) {
ImmutableList values = withMdc.get(k);
if (values != null) {
// Since the MDC is NOT a multimap, each key only appears at most once, so we never
// see repeated resizing of the values list.
ArrayList appended = new ArrayList<>(values.size() + 1);
appended.add(safeValue);
withMdc.put(k, ImmutableList.copyOf(appended));
} else {
withMdc.put(k, ImmutableList.of(safeValue));
}
}
},
withMdc);
metadata = ImmutableMap.copyOf(withMdc);
}
return metadata;
}
// Convert arbitrary values into metadata compatible types (Double, Long, Boolean, String) to
// avoid carrying large/mutable instances into test logic.
private static Object toSafeValue(Object v) {
if (v instanceof Double || v instanceof Float || v instanceof BigDecimal) {
return ((Number) v).doubleValue();
}
if (v instanceof Number) {
return ((Number) v).longValue();
}
if (v instanceof Boolean) {
return v;
}
try {
return v.toString();
} catch (RuntimeException e) {
return null;
}
}
// WARNING: Log4J level values *decrease* with severity.
private static LevelClass toLevelClass(Level level) {
if (level.compareTo(Level.ERROR) <= 0) return LevelClass.SEVERE;
if (level.compareTo(Level.WARN) <= 0) return LevelClass.WARNING;
if (level.compareTo(Level.INFO) <= 0) return LevelClass.INFO;
if (level.compareTo(Level.DEBUG) <= 0) return LevelClass.FINE;
return LevelClass.FINEST;
}
private static Level toLog4JLevel(LevelClass level) {
switch (level) {
case FINEST:
return Level.TRACE;
case FINE:
return Level.DEBUG;
case INFO:
return Level.INFO;
case WARNING:
return Level.WARN;
case SEVERE:
return Level.ERROR;
default:
throw new AssertionError("unknown level: " + level);
}
}
}