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


100

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

slf4j没有log()我可以找到的通用方法。这是否意味着无法实现上述目的?


4
似乎有一些讨论将其添加到开发邮件列表中的slf4j 2.0中:qos.ch/pipermail/slf4j-dev/2010-March/002865.html
Edward Dale 2010年

1
看一下Marker,这是可以传递到日志链的自定义数据。
tuxSlayer

1
@tuxSlayer您能否详细说明在这种情况下如何使用Marker?
悲惨变量

可能不是“记录”的最佳主意,但是您可以对日志条目“优先级”使用多个标记(高|低|正常,信息|警告|致命),并在回溯或自定义附加程序中使用过滤器来消耗标记并驱动日志条目进入单独的渠道(日志信息,致命电子邮件等)。但是,更直接的方法是为此建立一个立面,如下面的答案所指出的。
tuxSlayer

2
该功能应该是的一部分slf4j 2.0jira.qos.ch/browse/SLF4J-124有关详细信息和可能的解决方法,请参阅我的答案slf4j 1.x
slartidan

Answers:


47

无法使用来执行此操作slf4j

我认为缺少此功能的原因是几乎无法构造一个Level类型,slf4j该类型可以有效地映射到Level幕墙后面所有可能的日志记录实现中使用的(或等效)类型。另外,设计人员认为您的用例太特殊了,无法证明支持它的开销。

关于@ ripper234用例(单元测试),我认为务实的解决方案是修改单元测试,以便在运行单元测试时对slf4j幕后的日志系统背后的知识有所了解。


9
确实没有必要映射。该方法中已经隐含定义了五个级别org.slf4j.Logger:调试,错误,信息,跟踪,警告。
爱德华·戴尔

1
问题因无效而关闭。据我了解,这是一个故意的设计选择。
ripper234

9
@ ripper234-我认为您的错误与scompt.com的原始问题没有解决相同的问题。您询问有关通过SLF4J API 配置基础日志系统的级别的问题。scompt.com所追求的是SLF4J API中的通用“ log”方法,该方法将消息的日志记录级别作为参数。
理查德·费恩

1
+1 @RichardFearn,没有人可以在60秒后撤销的评论给予好评,。同时存在功能请求:bugzilla.slf4j.org/show_bug.cgi?id=133
2014年1

3
RFE链接不再解析。现在相关的链接为:jira.qos.ch/browse/SLF4J-124jira.qos.ch/browse/SLF4J-197 ...且都已关闭。阅读评论以了解基本原理。
史蒂芬·C

27

理查德·费恩(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参数一起使用会更容易。
Anonymoose

“ org.slf4j.Logger”具有许多上述类中未处理的日志记录方法签名,因此可能需要扩展:slf4j.org/api/org/slf4j/Logger.html
David Tonhofer

1
我认为此实现将添加一个不需要的更改。当使用呼叫logger.info(...)时,记录器可以访问调用者类和方法,并且可以自动将其添加到日志条目中。现在,通过此实现,调用log(logger,level,txt)将产生一个日志条目,该条目始终具有相同的调用方:Loglevel.log。我对吗?
明2015年

@Domin您好,您的意思是,记录器可以检查当前的调用堆栈,然后提取最后一个条目以进行自动记录,这里不是这种情况吗?原则上是,但是实际上,即使在此之后,堆栈仍会更多一些,直到写出实际的消息为止(尤其是必须在某个时候调用logback,然后再调用实际的附加程序)。我认为,丢弃无趣的堆栈行应该是附加程序的角色,因此您可以对其进行调整,以丢弃所有内容,包括对该Loglevel类的调用。
David Tonhofer

@David,是的,你是对的:-)。我不确定这是追加程序的任务,因为在这种情况下,您正在定义追加程序和记录器之间的硬依赖性...但是...这是一个解决方案。感谢David
Domin 2015年

14

尝试切换到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,这立即使我可以清理单元测试。谢谢!
兰巴特2014年

2
这是我的第一个-1,谢谢。我相信你错了。Logback使用SLF4J,因此答案很重要。
Αλέκος

3
@AlexandrosGelbessis您应该重新阅读问题。有人要求一种可以以编程方式记录任何级别的一条日志消息的方法。您正在更改所有消息的root记录程序级别,而不仅仅是更改一条消息。
2014年1

12

您可以使用Java 8 lambdas来实现。

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前面的新层增加了额外的日志记录开销。
斯蒂芬·C

4
还请注意,当您使用该解决方案时,将不会记录进行实际日志记录的类(因为logger初始化为LevelLogger),这不是一件好事,因为它通常是非常有用的信息。
睡鼠

6

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


3
没错,但是slf4j的目的是不必编写日志包装器。
djjeck 2014年

5
SLF4J的目的是为不同的日志记录框架提供一种抽象。如果该抽象不能完全满足您的需求,那么您别无选择,只能编写一个辅助方法。唯一的替代方法是在我对SLF4J项目的回答中提供与我类似的方法。
理查德·费恩

我同意,但是在这种情况下,有一些警告,例如,您将无法再提供文件和行号,除非您实施了另一种解决方法。在这种情况下,我会一直使用log4j,直到框架支持该功能-最终通过扩展实现,请参阅Robert Elliot的最新答案。
djjeck 2014年


3

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

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

记录器是在调用期间传递的,因此类信息应该没问题,并且可以与@ Slf4j lombok批注很好地配合使用。


非常感谢您采用这种出色的方法-根据您的想法,我也发布了类似的答案。
slartidan

DEBUG缺少常量。
slartidan

该解决方案将始终LogLevel以类和log方法的形式进行日志记录,这将使日志的意义降低。
slartidan

2

开箱即用无法在sjf4j中指定日志级别1.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);
    }
}

然后,您可以像这样使用它:

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您的业​​务代码简短易读,不需要其他依赖项

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


注意:LogMethod接口必须是公共的,才能与包外部的类一起使用。除此之外,它可以按预期工作。谢谢!
andrebrait

1

slf4j API不可能动态更改日志级别,但是您可以自己配置日志备份(如果使用此选项)。在这种情况下,请为您的记录器创建工厂类,并使用所需的配置来实现root记录器。

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

使用给出的root logger可以轻松更改日志级别loggerContext

root.setLevel(Level.DEBUG);

1

确认答案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 [] [oakcmMetrics]测试人员注册的度量标准,名为MetricName [name = bufferpool-wait-time-total,group = producer-metrics,description =追加程序等待空间分配的总时间。,tags = {client-id = producer-2}]


0

我刚遇到类似的需求。在我的情况下,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");

1
像其他答案一样,这并没有解决原始问题,而是一个不同的问题。
E-Riz

0

基于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());
}

0

这是一个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(应该记录其堆栈跟踪),因此我认为不需要log为别的使用者重载helper方法(String, Object[])


0

通过首先请求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);
}

0

我使用的方法是导入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;
}

-2

使用Java自省可以做到这一点,例如:

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

}

5
这明确地指的是log4j,而不是一般的slf4j –ThorbjørnRavn
Andersen

-6

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

有关完整的Logger API ,请访问http://www.slf4j.org/api/org/slf4j/Logger.html


对不起,我明白你现在在问什么。不,没有在运行时更改日志级别的通用方法,但是您可以轻松地使用switch语句实现辅助方法。
克里斯,2010年

是的,但是您必须为“ log”方法的每个重载版本执行一次。
Andrew Swan 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.