Установка уровня журнала сообщений во время выполнения в 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.