在 slf4j 中在运行时设置消息的日志级别

使用 log4j 时,可以使用 Logger.log(Priority p, Object message)方法,并且可以用于在运行时确定的日志级别记录消息。我们使用这个事实和 这个小费将 stderr 重定向到特定日志级别的日志记录器。

Slf4j 没有我能找到的通用 log()方法。这是否意味着没有办法实现以上所述?

更新: 在 SLF4J 2.0版本中,可以使用 Logger.atLevel()方法,例如:

import org.slf4j.event.Level;
import org.slf4j.Logger;
...
Logger logger = ... // some slf4j logger of choice
logger.atLevel(Level.Info).log("hello world");
159573 次浏览

no, it has a number of methods, info(), debug(), warn(), etc (this replaces the priority field)

看看完整的 Logger API 的 http://www.slf4j.org/api/org/slf4j/Logger.html

在 slf4j 1.x 中没有办法做到这一点。

我猜想这个功能缺失的原因是,几乎不可能为 slf4j 构造一个 Level类型,这个类型可以有效地映射到 facade 后面所有可能的日志实现中使用的 Level(或等效)类型。或者,设计者决定用 你的用例太不寻常了来证明支持它的开销是合理的。

Concerning @ Ripper 234's use-case (unit testing), I think the pragmatic solution is modify the unit test(s) to hard-wire knowledge of what logging system is behind the slf4j facade ... when running the unit tests.


UPDATE

他们打算在 slf4j 2.0中实现日志事件(具有动态日志级别)的零碎构造; 请参阅 https://jira.qos.ch/browse/SLF4J-124。根据@Ceki (参见注释) ,它是在2.0.0-alpha2版本中实现的。

这可以通过一个 enum和一个 helper 方法来完成:

enum LogLevel {
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
}


public static void log(Logger logger, LogLevel level, String format, Object[] argArray) {
switch (level) {
case TRACE:
logger.trace(format, argArray);
break;
case DEBUG:
logger.debug(format, argArray);
break;
case INFO:
logger.info(format, argArray);
break;
case WARN:
logger.warn(format, argArray);
break;
case ERROR:
logger.error(format, argArray);
break;
}
}


// example usage:
private static final Logger logger = ...
final LogLevel level = ...
log(logger, level, "Something bad happened", ...);

您可以添加 log的其他变体,比如您想要 SLF4J 的1参数或2参数 warn/error/etc 方法的通用等价物。

理查德 · 费恩的想法是正确的,所以我根据他的基本代码编写了完整的类。希望短到可以贴在这里。复制和粘贴用于享受。我也许应该加上一些魔法咒语: “此代码公开发布”

import org.slf4j.Logger;


public class LogLevel {


/**
* Allowed levels, as an enum. Import using "import [package].LogLevel.Level"
* Every logging implementation has something like this except SLF4J.
*/


public static enum Level {
TRACE, DEBUG, INFO, WARN, ERROR
}


/**
* This class cannot be instantiated, why would you want to?
*/


private LogLevel() {
// Unreachable
}


/**
* Log at the specified level. If the "logger" is null, nothing is logged.
* If the "level" is null, nothing is logged. If the "txt" is null,
* behaviour depends on the SLF4J implementation.
*/


public static void log(Logger logger, Level level, String txt) {
if (logger != null && level != null) {
switch (level) {
case TRACE:
logger.trace(txt);
break;
case DEBUG:
logger.debug(txt);
break;
case INFO:
logger.info(txt);
break;
case WARN:
logger.warn(txt);
break;
case ERROR:
logger.error(txt);
break;
}
}
}


/**
* Log at the specified level. If the "logger" is null, nothing is logged.
* If the "level" is null, nothing is logged. If the "format" or the "argArray"
* are null, behaviour depends on the SLF4J-backing implementation.
*/


public static void log(Logger logger, Level level, String format, Object[] argArray) {
if (logger != null && level != null) {
switch (level) {
case TRACE:
logger.trace(format, argArray);
break;
case DEBUG:
logger.debug(format, argArray);
break;
case INFO:
logger.info(format, argArray);
break;
case WARN:
logger.warn(format, argArray);
break;
case ERROR:
logger.error(format, argArray);
break;
}
}
}


/**
* Log at the specified level, with a Throwable on top. If the "logger" is null,
* nothing is logged. If the "level" is null, nothing is logged. If the "format" or
* the "argArray" or the "throwable" are null, behaviour depends on the SLF4J-backing
* implementation.
*/


public static void log(Logger logger, Level level, String txt, Throwable throwable) {
if (logger != null && level != null) {
switch (level) {
case TRACE:
logger.trace(txt, throwable);
break;
case DEBUG:
logger.debug(txt, throwable);
break;
case INFO:
logger.info(txt, throwable);
break;
case WARN:
logger.warn(txt, throwable);
break;
case ERROR:
logger.error(txt, throwable);
break;
}
}
}


/**
* Check whether a SLF4J logger is enabled for a certain loglevel.
* If the "logger" or the "level" is null, false is returned.
*/


public static boolean isEnabledFor(Logger logger, Level level) {
boolean res = false;
if (logger != null && level != null) {
switch (level) {
case TRACE:
res = logger.isTraceEnabled();
break;
case DEBUG:
res = logger.isDebugEnabled();
break;
case INFO:
res = logger.isInfoEnabled();
break;
case WARN:
res = logger.isWarnEnabled();
break;
case ERROR:
res = logger.isErrorEnabled();
break;
}
}
return res;
}
}

using java introspection you can do it, for example:

private void changeRootLoggerLevel(int level) {


if (logger instanceof org.slf4j.impl.Log4jLoggerAdapter) {
try {
Class loggerIntrospected = logger.getClass();
Field fields[] = loggerIntrospected.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
String fieldName = fields[i].getName();
if (fieldName.equals("logger")) {
fields[i].setAccessible(true);
org.apache.log4j.Logger loggerImpl = (org.apache.log4j.Logger) fields[i]
.get(logger);


if (level == DIAGNOSTIC_LEVEL) {
loggerImpl.setLevel(Level.DEBUG);
} else {
loggerImpl.setLevel(org.apache.log4j.Logger.getRootLogger().getLevel());
}


// fields[i].setAccessible(false);
}
}
} catch (Exception e) {
org.apache.log4j.Logger.getLogger(LoggerSLF4JImpl.class).error("An error was thrown while changing the Logger level", e);
}
}


}

尝试转到 Logback 使用

ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(Level.toLevel("info"));

我相信这将是对 Logback 的唯一调用,其余代码将保持不变。Logback 使用 SLF4J,并且迁移过程将是无痛的,只需要更改 xml 配置文件即可。

记住在完成后将日志级别设置回来。

任何人想要一个完全兼容 SLF4J 的解决方案,这个问题可能想看看 Lidalia SLF4J 扩展-它的 Maven 中心。

我刚刚遇到了类似的需求。 在我的示例中,slf4j 配置了 Java 日志适配器(jdk14适配器)。 使用下面的代码片段,我设法在运行时更改了调试级别:

Logger logger = LoggerFactory.getLogger("testing");
java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger("testing");
julLogger.setLevel(java.util.logging.Level.FINE);
logger.debug("hello world");

基于 massimo virgilio 的答案,我还设法用 slf4j-log4j 使用内省来完成。

Logger LOG = LoggerFactory.getLogger(MyOwnClass.class);


org.apache.logging.slf4j.Log4jLogger LOGGER = (org.apache.logging.slf4j.Log4jLogger) LOG;


try {
Class loggerIntrospected = LOGGER.getClass();
Field fields[] = loggerIntrospected.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
String fieldName = fields[i].getName();
if (fieldName.equals("logger")) {
fields[i].setAccessible(true);
org.apache.logging.log4j.core.Logger loggerImpl = (org.apache.logging.log4j.core.Logger) fields[i].get(LOGGER);
loggerImpl.setLevel(Level.DEBUG);
}
}
} catch (Exception e) {
System.out.println("ERROR :" + e.getMessage());
}

您可以使用 Java8lambdas 来实现它。

import java.util.HashMap;
import java.util.Map;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;


public class LevelLogger {
private static final Logger LOGGER = LoggerFactory.getLogger(LevelLogger.class);
private static final Map<Level, LoggingFunction> map;


static {
map = new HashMap<>();
map.put(Level.TRACE, (o) -> LOGGER.trace(o));
map.put(Level.DEBUG, (o) -> LOGGER.debug(o));
map.put(Level.INFO, (o) -> LOGGER.info(o));
map.put(Level.WARN, (o) -> LOGGER.warn(o));
map.put(Level.ERROR, (o) -> LOGGER.error(o));
}


public static void log(Level level, String s) {
map.get(level).log(s);
}


@FunctionalInterface
private interface LoggingFunction {
public void log(String arg);
}
}

这里有一个 lambda 解决方案,在某种程度上不像@Paul Croarkin 那样友好(该级别实际上通过了两次)。但是我认为(a)用户应该通过 Logger; (b) AFAIU 最初的问题并不是要求在应用程序的任何地方都有一个方便的方法,只是在图书馆内使用很少的情况。

package test.lambda;
import java.util.function.*;
import org.slf4j.*;


public class LoggerLambda {
private static final Logger LOG = LoggerFactory.getLogger(LoggerLambda.class);


private LoggerLambda() {}


public static void log(BiConsumer<? super String, ? super Object[]> logFunc, Supplier<Boolean> logEnabledPredicate,
String format, Object... args) {
if (logEnabledPredicate.get()) {
logFunc.accept(format, args);
}
}


public static void main(String[] args) {
int a = 1, b = 2, c = 3;
Throwable e = new Exception("something went wrong", new IllegalArgumentException());
log(LOG::info, LOG::isInfoEnabled, "a = {}, b = {}, c = {}", a, b, c);


// warn(String, Object...) instead of warn(String, Throwable), but prints stacktrace nevertheless
log(LOG::warn, LOG::isWarnEnabled, "error doing something: {}", e, e);
}
}

从 slf4j允许在 varargs 参数中有一个 Throwable (应该记录其堆栈跟踪)开始,我认为没有必要为 (String, Object[])以外的其他消费者重载 log助手方法。

通过首先请求 SLF4J Logger 实例和设置绑定级别的 then,我可以为 JDK14绑定做到这一点——您可以在 Log4J 绑定中尝试这一点。

private void setLevel(Class loggerClass, java.util.logging.Level level) {
org.slf4j.LoggerFactory.getLogger(loggerClass);
java.util.logging.Logger.getLogger(loggerClass.getName()).setLevel(level);
}

我使用的方法是导入 ch.qos.logback 模块,然后将 slf4j Logger 实例类型强制转换为 ch.qos.logback.Classic。伐木工。此实例包含一个 setLevel ()方法。

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;


Logger levelSet = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);


// Now you can set the desired logging-level
levelSet.setLevel( Level.OFF );

To find out the possible Logging-levels, you can explode the ch.qos.logback class to see all the possible values for 水平:

prompt$ javap -cp logback-classic-1.2.3.jar ch.qos.logback.classic.Level

结果如下:

{
// ...skipping
public static final ch.qos.logback.classic.Level OFF;
public static final ch.qos.logback.classic.Level ERROR;
public static final ch.qos.logback.classic.Level WARN;
public static final ch.qos.logback.classic.Level INFO;
public static final ch.qos.logback.classic.Level DEBUG;
public static final ch.qos.logback.classic.Level TRACE;
public static final ch.qos.logback.classic.Level ALL;
}

我只是需要这样的东西,并想出:

@RequiredArgsConstructor //lombok annotation
public enum LogLevel{


TRACE(l -> l::trace),
INFO (l -> l::info),
WARN (l -> l::warn),
ERROR(l -> l::error);


private final Function<Logger, Consumer<String>> function;


public void log(Logger logger, String message) {
function.apply(logger).accept(message);
}
}

usage:

    LogLevel level = LogLevel.TRACE;
level.log(logger, "message");

Logger 是在调用期间传递的,所以类信息应该没问题,它与@Slf4j lombok 注释一起工作得很好。

not可以在 sjf4j1.x中立即指定日志级别。但是 slf4j 2.0修复 问题还是有希望的。在2.0版本中,它可能是这样的:

// POTENTIAL 2.0 SOLUTION
import org.slf4j.helpers.Util;
import static org.slf4j.spi.LocationAwareLogger.*;


// does not work with slf4j 1.x
Util.log(logger, DEBUG_INT, "hello world!");

同时,对于 slf4j 1.x,您可以使用以下变通方法:

将这个类复制到您的类路径中:

import org.slf4j.Logger;
import java.util.function.Function;


public enum LogLevel {


TRACE(l -> l::trace, Logger::isTraceEnabled),
DEBUG(l -> l::debug, Logger::isDebugEnabled),
INFO(l -> l::info, Logger::isInfoEnabled),
WARN(l -> l::warn, Logger::isWarnEnabled),
ERROR(l -> l::error, Logger::isErrorEnabled);


interface LogMethod {
void log(String format, Object... arguments);
}


private final Function<Logger, LogMethod> logMethod;
private final Function<Logger, Boolean> isEnabledMethod;


LogLevel(Function<Logger, LogMethod> logMethod, Function<Logger, Boolean> isEnabledMethod) {
this.logMethod = logMethod;
this.isEnabledMethod = isEnabledMethod;
}


public LogMethod prepare(Logger logger) {
return logMethod.apply(logger);
}


public boolean isEnabled(Logger logger) {
return isEnabledMethod.apply(logger);
}
}

Then you can use it like this:

Logger logger = LoggerFactory.getLogger(Application.class);


LogLevel level = LogLevel.ERROR;
level.prepare(logger).log("It works!"); // just message, without parameter
level.prepare(logger).log("Hello {}!", "world"); // with slf4j's parameter replacing


try {
throw new RuntimeException("Oops");
} catch (Throwable t) {
level.prepare(logger).log("Exception", t);
}


if (level.isEnabled(logger)) {
level.prepare(logger).log("logging is enabled");
}

这将输出如下日志:

[main] ERROR Application - It works!
[main] ERROR Application - Hello world!
[main] ERROR Application - Exception
java.lang.RuntimeException: Oops
at Application.main(Application.java:14)
[main] ERROR Application - logging is enabled

值得吗?

  • 它保持 源代码位置源代码位置(类名称,方法名称,行号将指向 你的代码)
  • 你可以很容易地定义 variables,参数和返回类型为 LogLevel
  • 您的业务代码保持简短易读,不需要额外的 依赖关系

作为最小示例的源代码托管在 在 GitHub 上上。

使用 slf4jAPI 不可能动态更改日志级别,但是您可以自己配置 logback (如果使用的话)。在这种情况下,为日志记录器创建工厂类,并使用所需的配置实现根日志记录器。

LoggerContext loggerContext = new LoggerContext();
ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);


// Configure appender
final TTLLLayout layout = new TTLLLayout();
layout.start(); // default layout of logging messages (the form that message displays
// e.g. 10:26:49.113 [main] INFO com.yourpackage.YourClazz - log message


final LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<>();
encoder.setCharset(StandardCharsets.UTF_8);
encoder.setLayout(layout);


final ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
appender.setContext(loggerContext);
appender.setEncoder(encoder);
appender.setName("console");
appender.start();


root.addAppender(appender);

After you configure root logger (only once is enough) you can delegate getting new logger by

final ch.qos.logback.classic.Logger logger = loggerContext.getLogger(clazz);

记住使用相同的 loggerContext

改变日志级别对于从 loggerContext给出的根日志记录器来说很容易。

root.setLevel(Level.DEBUG);

确认答案 Ondrej Skopek

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import org.slf4j.LoggerFactory;


var rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(Level.TRACE);

你会得到结果:

2020-05-1414:01:16,644 TRACE [][ o.a.k.c.m. 公制]测试人员 注册的度量名为 MetricName [ name = buffer-wait-time-total, group=producer-metrics, description=The total time an appender waits for space allocation., tags={client-id=producer-2}]

SLF4J v2.0中流畅的 API 引入了一种新的方法,即 Logger.atLevel(Level),用于实现所需的结果。

示例代码:

public void logAMessageAtGivenLevel(Level aLevel, String aMessage) {
Logger logger = .. // some slf4j logger of choice
logger.atLevel(aLevel).log(aMessage);
}

如果为给定的 Level禁用了日志记录器,那么默认实现将返回 NOPLoggingEventBuilder的单例实例。如名称 NOP 所示,LoggingEventBuilder接口的这个实现什么也不做,为禁用的日志消息保留了纳秒级的执行时间。