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

Posted

技术标签:

【中文标题】在 slf4j 运行时设置消息的日志级别【英文标题】:Setting log level of message at runtime in slf4j 【发布时间】:2011-02-06 23:31:23 【问题描述】:

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

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

【问题讨论】:

在开发邮件列表中似乎有一些关于将其添加到 slf4j 2.0 的讨论:qos.ch/pipermail/slf4j-dev/2010-March/002865.html 看看 Marker,这是您可以传递给日志链的自定义数据。 @tuxSlayer 你能详细说明在这种情况下如何使用标记吗? 可能它不是“记录”的最佳主意,但您可以使用多个标记来记录日志条目“优先级”(高|低|正常,信息|警告|致命)并在 logback 或自定义中使用过滤appender 使用标记并将日志条目驱动到单独的通道(日志信息、电子邮件致命等)。但是,更直接的方法是为此设置一个外观,正如下面的答案所指出的那样。 这个功能应该是slf4j 2.0的一部分。 jira.qos.ch/browse/SLF4J-124 查看我的答案以了解详细信息以及可能的slf4j 1.x-workaround。 【参考方案1】:

slf4j 无法做到这一点。

我认为缺少此功能的原因是几乎不可能为 slf4j 构造一个 Level 类型,该类型可以有效地映射到所有可能使用的 Level(或等效)类型记录立面后面的实现。或者,设计师决定使用your use-case is too unusual 来证明支持它的开销是合理的。

关于@ripper234 的use-case(单元测试),我认为实用的解决方案是将单元测试修改为硬连线了解 slf4j 外观背后的日志系统......当运行单元测试。


更新

他们打算在 slf4j 2.0 中实现日志事件的零碎构造(具有动态日志级别);见https://jira.qos.ch/browse/SLF4J-124。

【讨论】:

真的不需要映射。 org.slf4j.Logger 中的方法已经隐式定义了五个级别:调试、错误、信息、跟踪、警告。 问题因无效而关闭。据我了解,这是一个深思熟虑的设计选择。 @ripper234 - 我不认为您的错误解决了与 scompt.com 的原始问题相同的问题。您询问了通过 SLF4J API 配置底层日志系统的级别。 scompt.com 所追求的是 SLF4J API 中的通用“日志”方法,它将消息的日志记录级别作为参数。 +1 @RichardFearn 而且 60 秒后无法撤消评论投票,meh。同时存在功能请求:bugzilla.slf4j.org/show_bug.cgi?id=133 RFE 链接不再解析。相关链接现在是:jira.qos.ch/browse/SLF4J-124 和 jira.qos.ch/browse/SLF4J-197 ... 并且都已关闭。阅读 cmets 了解基本原理。【参考方案2】:

Richard Fearn 的想法是正确的,所以我根据他的骨架代码编写了完整的课程。希望它足够短,可以在这里发布。复制和粘贴享受。我可能也应该添加一些魔法咒语:“此代码已发布到公共领域”

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;
    

【讨论】:

这将更容易与可变参数 (Object...) args 参数一起使用。 "org.slf4j.Logger" 有相当多的日志记录方法签名没有在上述类中处理,因此可能需要扩展:slf4j.org/api/org/slf4j/Logger.html 我认为这个实现会增加一个不需要的改变。当您使用调用 logger.info(...) 时,记录器可以访问调用者类和方法,并且可以自动将其添加到日志条目中。现在,通过这个实现,调用 log(logger, level, txt) 将生成一个日志条目,该条目将始终具有相同的调用者:Loglevel.log。我说的对吗? @Domin 嗨,你的意思是,记录器可以检查当前调用堆栈,然后提取最后一个条目以进行自动记录,这里不是这样吗?原则上是的,但事实上,即使在此之后,堆栈也会变得更多,直到实际消息被写出(特别是,必须在某个时候调用 logback,然后才是实际的 appender)。我认为 appender 的角色应该是丢弃不感兴趣的堆栈行,因此您可以调整它以丢弃所有内容,包括对该 Loglevel 类的调用。 @David,是的,你是对的 :-)。我不确定这是 appender 的任务,因为在这种情况下,您正在定义 appender 和 logger 之间的硬依赖......但是......这是一个解决方案。谢谢大卫【参考方案3】:

尝试切换到 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 配置文件。

请记住在完成后重新设置日志级别。

【讨论】:

我已经在使用 Logback 支持的 slf4j,这立即让我能够清理我的单元测试。谢谢! 这是我的第一个-1,谢谢。我相信你错了。 Logback 使用 SLF4J,所以答案是相关的。 @AlexandrosGelbessis 你应该重新阅读这个问题。有人要求提供一种可以以编程方式在任何级别记录一条日志消息的方法。您正在更改所有消息的根记录器的级别,而不仅仅是一条消息。【参考方案4】:

您可以使用 Java 8 lambda 来实现这一点。

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);
    

【讨论】:

好吧...但是现在您需要修改您的代码库以使用此 API 以及或代替 slf4j。如果您使用它而不是 slf4j 1) 它可能需要更丰富,2) 需要更改大量(至少)导入,以及 3) slf4j 前面的这个新层会增加额外的日志记录开销。 还要注意,当您使用此解决方案时,不会记录执行实际日志记录的类(因为记录器是用LevelLogger 初始化的),这不是一件好事,因为它是通常非常有用的信息。【参考方案5】:

这可以通过enum 和辅助方法来完成:

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/等的通用等效项。方法。

【讨论】:

是的,但 slf4j 的目的是不必编写日志包装器。 SLF4J 的目的是为不同的日志框架提供一个抽象。如果该抽象不能提供您真正需要的东西,那么您别无选择,只能编写一个辅助方法。唯一的另一种选择是贡献一种方法,就像我对 SLF4J 项目的回答中的方法一样。 我同意,但是在这种情况下有一些警告,例如您将无法再提供文件和行号,除非您为此实施了另一种解决方法。在这种情况下,我会坚持使用 log4j,直到框架支持该功能 - 最终通过扩展发生,请参阅 Robert Elliot 的最新答案。【参考方案6】:

任何想要一个完全兼容 SLF4J 的解决方案来解决这个问题的人都可能想查看 Lidalia SLF4J Extensions - 它位于 Maven Central。

【讨论】:

【参考方案7】:

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

@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);
    

用法:

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

Logger 在调用过程中被传递,所以类信息应该没问题,和@Slf4j lombok 注解配合得很好。

【讨论】:

非常感谢您采用这种很棒的方法-根据您的想法,我发布了类似的答案。 DEBUG 作为常量丢失。 此解决方案将始终将LogLevel 记录为类,将log 记录为方法,这使得日志意义不大。【参考方案8】:

不可能在 sjf4j 1.x 中指定一个开箱即用的日志级别。但是 slf4j 2.0 有希望修复 the issue。在 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);
    

那么你可以这样使用它:

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

值得吗?

Pro它保留源代码位置(类名、方法名、行号将指向你的代码) Pro 您可以轻松地将变量、参数和返回类型定义为LogLevel Pro 您的业务代码简短易读,不需要额外的依赖项

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

【讨论】:

注意:LogMethod 接口必须是公开的,它才能与包外的类一起工作。除此之外,它按预期工作。谢谢!【参考方案9】:

确认答案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-14 14:01:16,644 TRACE [] [o.a.k.c.m.Metrics] 测试工作者 已注册的指标名为 MetricName [name=bufferpool-wait-time-total, group=producer-metrics, description=appender 等待的总时间 用于空间分配。tags=client-id=producer-2]

【讨论】:

没有这样的方法,setLevel【参考方案10】:

我使用的方法是导入 ch.qos.logback 模块,然后将 slf4j Logger 实例类型转换为 ch.qos.logback.classic.Logger。此实例包含一个 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 );

要找出可能的日志记录级别,您可以展开 ch.qos.logback 类以查看 Level 的所有可能值:

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;

【讨论】:

【参考方案11】:

slf4j API 无法动态更改日志级别,但您可以自己配置 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);

配置根记录器后(一次就够了),您可以委托获取新记录器

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

记得使用相同的loggerContext

使用loggerContext 提供的根记录器可以轻松更改日志级别。

root.setLevel(Level.DEBUG);

【讨论】:

【参考方案12】:

我刚刚遇到了类似的需求。 就我而言,slf4j 配置了 java 日志适配器(jdk14 之一)。 使用以下代码 sn-p 我设法在运行时更改了调试级别:

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");

【讨论】:

与其他答案一样,这并没有解决原始问题,而是另一个问题。【参考方案13】:

根据 massimo virgilio 的回答,我还设法通过自省使用 slf4j-log4j 来做到这一点。 HTH。

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());

【讨论】:

【参考方案14】:

这里有一个 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 allows a Throwable (whose stack trace should be logged) inside the varargs param,我认为除了(String, Object[])之外的其他消费者没有必要重载log辅助方法。

【讨论】:

【参考方案15】:

我可以通过首先请求 SLF4J Logger 实例并然后设置绑定级别来为 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);

【讨论】:

【参考方案16】:

SLF4J v2.0 中的 fluent API 引入了一种新方法,即Logger.makeLoggingEventBuilder(Level),用于实现预期的结果。

示例代码:

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

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

【讨论】:

【参考方案17】:

使用java introspection你可以做到,例如:

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);
        
    


【讨论】:

这明确指的是 log4j 而不是一般的 slf4j【参考方案18】:

不,它有许多方法,info()、debug()、warn() 等(这取代了优先级字段)

查看 http://www.slf4j.org/api/org/slf4j/Logger.html 以获取完整的 Logger api。

【讨论】:

对不起,我明白你现在在问什么。不,没有在运行时更改日志级别的通用方法,但是您可以轻松地使用 switch 语句实现一个辅助方法。 是的,但是你必须为每个“log”方法的重载版本执行一次。

以上是关于在 slf4j 运行时设置消息的日志级别的主要内容,如果未能解决你的问题,请参考以下文章

利用logback+slf4j日志采集整合到SBA

如何使用 Java 代码配置 Logback 以设置日志级别?

springboot日志开启SLF4J

删除启动消息以更改 Spark 日志级别

我可以在运行时更改 syslog 或 syslog-ng 日志级别吗?

在 JBoss 7.1.1 中使用 DEBUG 级别进行日志记录