Установка уровня журнала сообщений во время выполнения в slf4j


102

При использовании log4j Logger.log(Priority p, Object message)метод доступен и может использоваться для регистрации сообщения на уровне журнала, определяемом во время выполнения. Мы используем этот факт и этот совет для перенаправления stderr в средство ведения журнала на определенном уровне журнала.

У slf4j нет универсального log()метода, который я могу найти. Означает ли это, что невозможно реализовать вышеизложенное?


4
Похоже, что в списке рассылки разработчиков обсуждается добавление этого в slf4j 2.0: qos.ch/pipermail/slf4j-dev/2010-March/002865.html
— Эдвард Дейл,

1
взгляните на Маркер, это пользовательские данные, которые вы можете передать в цепочку журналов.
— tuxSlayer

1
@tuxSlayer, не могли бы вы рассказать, как использовать маркер в этом случае?
— Miserable Variable

Вероятно, это не лучшая идея для «ведения журнала», но вы можете использовать несколько маркеров для «приоритета» записи журнала (высокий | низкий | нормальный, информация | предупреждение | фатальный) и использовать фильтрацию в журнале регистрации или настраиваемом приложении для использования маркеров и записей журнала дисков. в отдельные каналы (информация журнала, фатальный адрес электронной почты и т. д.). Однако более простой способ - иметь фасад для этого, как было указано в ответах ниже.
— tuxSlayer

2
Эта функция должна быть частью slf4j 2.0. jira.qos.ch/browse/SLF4J-124 См. мой ответ для получения подробностей и возможного slf4j 1.xобходного пути.
— slartidan

Ответы:


47

Нет возможности сделать это с помощью slf4j.

Я полагаю, что причина отсутствия этой функциональности заключается в том, что практически невозможно построить Levelтип, slf4jкоторый можно эффективно сопоставить с Level(или эквивалентным) типом, используемым во всех возможных реализациях ведения журнала за фасадом. С другой стороны, дизайнеры решили, что ваш вариант использования слишком необычен, чтобы оправдать затраты на его поддержку.

Что касается @ ripper234 «s потребительного случая (модульное тестирования), я думаю , что прагматичное решение изменить модульный тест (ы) с трудом проволочного знания о том , что система регистрации находится за SLF4J фасада ... при выполнении модульных тестов.


10
На самом деле нет необходимости в картировании. Существует пять уровней, уже неявно определенных методами в org.slf4j.Logger: отладка, ошибка, информация, трассировка, предупреждение.
— Эдвард Дейл,

1
И вопросы закрылись как Недействительные. Насколько я понимаю, это осознанный выбор дизайна.
— ripper234

9
@ ripper234 - Я не думаю, что ваша ошибка решала ту же проблему, что и исходный вопрос scompt.com. Вы спрашивали о настройке уровня базовой системы ведения журнала через SLF4J API. Scompt.com был после общего метода журнала в SLF4J API, который принимает уровень ведения журнала сообщения в качестве параметра.
— Ричард Фирн,

1
+1 @RichardFearn И никто не может отменить комментарий upvote через 60 секунд, Мех . Запрос функции пока существует: bugzilla.slf4j.org/show_bug.cgi?id=133
— янв,

3
Ссылки RFE больше не разрешаются. Соответствующие ссылки сейчас: jira.qos.ch/browse/SLF4J-124 и jira.qos.ch/browse/SLF4J-197 ... и обе закрыты. Прочтите комментарии для обоснования.
— Stephen C

28

Ричард Фирн имеет правильную идею, поэтому я написал полный класс на основе его скелетного кода. Надеюсь, он достаточно короткий, чтобы разместить его здесь. Копируйте и вставляйте для удовольствия. Я, наверное, тоже должен добавить какое-нибудь магическое заклинание: «Этот код передан в общественное достояние»

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

Это было бы проще использовать с параметром args с переменным числом аргументов (Object ...).
— Anonymoose

"org.slf4j.Logger" имеет довольно много сигнатур методов ведения журнала, которые не обрабатываются в указанном выше классе, поэтому, вероятно, требуется расширение: slf4j.org/api/org/slf4j/Logger.html
— Дэвид Тонхофер

1
Я думаю, что эта реализация внесет нежелательное изменение. Когда вы используете call logger.info (...), регистратор имеет доступ к вызывающему классу и методу, и он может быть автоматически добавлен в запись журнала. Теперь, с этой реализацией, журнал вызовов (регистратор, уровень, txt) будет создавать запись журнала, в которой всегда будет один и тот же вызывающий объект: Loglevel.log. Я прав?
— Domin

@Domin Привет, вы имеете в виду, что регистратор может проверить текущий стек вызовов, а затем извлечь последнюю запись для автоматического ведения журнала, что здесь не так? В принципе да, но на самом деле стек будет немного расти даже после этого, пока не будет записано фактическое сообщение (в частности, в какой-то момент должен быть вызван логбэк, а затем фактический аппендер). Я думаю, что роль appender - отбрасывать неинтересные строки стека, чтобы вы могли адаптировать его, чтобы выбросить все, вплоть до вызова этого класса Loglevel.
— Дэвид Тонхофер,

@David, да, ты прав :-). Я не уверен, что это задача для приложения, потому что в этом случае вы определяете жесткую зависимость между приложением и регистратором ... но ... это решение. Спасибо, Дэвид
— Домин

13

Попробуйте переключиться на 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 с поддержкой Logback, и это мгновенно позволило мне очистить свои модульные тесты. Спасибо!
— Lambart

2
Это был мой первый -1, спасибо. Я считаю, что вы ошибаетесь. Logback использует SLF4J, поэтому ответ актуален.
— Αλέκος

4
@AlexandrosGelbessis Вы должны перечитать вопрос. Его попросили разработать метод, который мог бы программно регистрировать одно сообщение журнала на любом уровне. Вы меняете уровень корневого регистратора для всех сообщений, а не только для одного.
— январь

12

Вы можете реализовать это, используя лямбды Java 8.

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 добавляет дополнительные накладные расходы на ведение журнала.
— Stephen C

4
Также имейте в виду, что, когда вы выбираете это решение, класс, который выполняет фактическое ведение журнала, не будет регистрироваться (потому что регистратор инициализируется LevelLogger), что не очень хорошо, потому что обычно это очень полезная информация.
— Соня

7

Это можно сделать с 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, скажем, если вам нужны общие эквиваленты 1-параметрического или 2-параметрического warn/ error/ и т.д. SLF4J . методы.


3
Верно, но цель slf4j - не писать обертки журналов.
— djjeck

5
Цель SLF4J - предоставить абстракцию для различных фреймворков журналирования. Если эта абстракция не предоставляет именно то, что вам нужно, у вас нет другого выбора, кроме как написать вспомогательный метод. Единственная другая альтернатива - внести в проект SLF4J метод, подобный тому, что был в моем ответе.
— Ричард Фирн,

Я согласен, но в этом случае есть предостережения, например, что вы больше не сможете предоставить номер файла и строки, если вы не реализовали еще один обходной путь для этого. В этом случае я бы придерживался log4j, пока фреймворк не поддерживал эту функцию, что в конечном итоге произошло через расширение, см. Более свежий ответ Роберта Эллиота.
— djjeck


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 невозможно динамически изменять уровень журнала, но вы можете настроить логбэк (если вы его используете) самостоятельно. В этом случае создайте фабричный класс для вашего регистратора и внедрите корневой регистратор с необходимой вам конфигурацией.

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

1

Подтвердить ответ Ондрей Скопек

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 = Общее время, в течение которого приложение ожидает выделения пространства ., теги = {идентификатор клиента = производитель-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

Вот решение лямбда, не такое удобное для пользователя, как @Paul Croarkin с одной стороны (уровень фактически проходит дважды). Но я думаю: (а) пользователь должен передать Регистратор; и (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 позволяет использовать Throwable (трассировка стека которого должна регистрироваться) внутри параметра varargs , я думаю, что нет необходимости перегружать logвспомогательный метод для других потребителей, кроме (String, Object[]).


0

Я смог сделать это для привязки JDK14, сначала запросив экземпляр SLF4J Logger, а затем установив уровень привязки - вы можете попробовать это для привязки 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, чтобы увидеть все возможные значения для уровня :

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ørn Ravn Andersen

-6

нет, у него есть несколько методов, info (), debug (), warn () и т. д. (это заменяет поле приоритета)

посмотрите http://www.slf4j.org/api/org/slf4j/Logger.html для получения полного API Logger.


извините, теперь я понимаю, о чем вы спрашиваете. нет, нет универсального способа изменить уровень журнала во время выполнения, но вы можете легко реализовать вспомогательный метод с помощью статуса переключения.
— Крис

Да, но тогда вы должны сделать это один раз для каждой перегруженной версии метода «журнала».
— Эндрю Свон
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.