web-dev-qa-db-fra.com

Définition du niveau de journalisation du message lors de l'exécution dans slf4j

Lorsque vous utilisez log4j, la méthode Logger.log(Priority p, Object message) est disponible et peut être utilisée pour consigner un message à un niveau de journalisation déterminé lors de l'exécution. Nous utilisons ce fait et ce conseil pour rediriger stderr vers un enregistreur à un niveau de journalisation spécifique.

slf4j n'a pas de méthode générique log() que je puisse trouver. Cela signifie-t-il qu'il n'y a aucun moyen de mettre en œuvre ce qui précède?

78
scompt.com

Il n'y a aucun moyen de faire cela avec slf4j.

J'imagine que cette fonctionnalité est manquante parce qu'il est pratiquement impossible de construire un type Level pour slf4j pouvant être mappé efficacement vers le type Level (ou équivalent) utilisé dans toutes les implémentations de journalisation possibles derrière la façade. Sinon, les concepteurs ont décidé que votre cas d'utilisation est trop inhabituel pour justifier les frais généraux liés à sa prise en charge.

Concernant @ ripper234 's use-case (test unitaire), je pense que la solution pragmatique consiste à modifier le (s) test (s) unitaire (s) de manière à ce que le système d’exploitation forestière se cache derrière la façade slf4j. lors de l'exécution des tests unitaires.

38
Stephen C

Richard Fearn a la bonne idée, alors j'ai écrit la classe complète en fonction de son code squelette. J'espère que c'est assez court pour poster ici. Copier et coller pour le plaisir. Je devrais probablement ajouter une incantation magique aussi: "Ce code est rendu public."

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;
    }
}
25
David Tonhofer

Vous pouvez implémenter cela en utilisant 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);
    }
}
11
Paul Croarkin

Essayez de passer à Logback et utilisez

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

Je crois que ce sera le seul appel à Logback et le reste de votre code restera inchangé. Logback utilise SLF4J et la migration se fera sans peine. Seuls les fichiers de configuration XML devront être modifiés.

N'oubliez pas de définir le niveau de journalisation une fois que vous avez terminé.

11
Αλέκος

Cela peut être fait avec une méthode enum et une méthode auxiliaire:

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

Vous pouvez ajouter d'autres variantes de log, par exemple si vous voulez des équivalents génériques du paramètre 1 ou 2 paramètres de SLF4J warn/error/etc. méthodes.

6
Richard Fearn

Quiconque souhaite une solution entièrement compatible SLF4J totalement intégrée à ce problème peut vouloir vérifier Extensions Lidalia SLF4J - c’est sur Maven Central.

5
Robert Elliot

Je viens de rencontrer un besoin similaire . Dans mon cas, slf4j est configuré avec l'adaptateur de journalisation Java (celui de jdk14) . À l'aide de l'extrait de code suivant, j'ai réussi à modifier le niveau de débogage à l'exécution:

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
Yair Zaslavsky

J'ai juste besoin de quelque chose comme ça et je suis venu avec:

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

L'enregistreur est passé lors de l'appel, donc les informations sur la classe devraient être correctes, et cela fonctionne bien avec l'annotation @ Slf4j lombok.

1
Kamil Nowak

Il est impossible de spécifier un niveau de journalisation dans sjf4j 1.x prêt à l'emploi. Mais il y a de l'espoir pour slf4j 2.0 de corriger le problème . En 2.0, cela pourrait ressembler à ceci:

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

En attendant, pour slf4j 1.x, vous pouvez utiliser cette solution de contournement:

Copiez cette classe dans votre chemin de classe:

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

Ensuite, vous pouvez l'utiliser comme ceci:

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

Cela produira un journal comme celui-ci:

[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

Est-ce que ça vaut le coup?

  • Pro Il conserve l'emplacement du code source (noms de classe, noms de méthodes, numéros de ligne pointant vers votre code )
  • Pro Vous pouvez facilement définir des variables , des paramètres et des types de retour comme LogLevel
  • Pro Votre code d'entreprise reste court et facile à lire, et aucune dépendance supplémentaire n'est requise.

Le code source en exemple minimal est hébergé sur GitHub .

1
slartidan

Il n'est pas possible avec l'API slf4j de modifier le niveau de journalisation de manière dynamique, mais vous pouvez configurer la journalisation (si vous l'utilisez) par vous-même. Dans ce cas, créez une classe d'usine pour votre consignateur et implémentez un consignateur racine avec la configuration dont vous avez besoin.

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

Après avoir configuré l’enregistreur racine (une seule fois suffit), vous pouvez déléguer l’obtention d’un nouvel enregistreur en

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

N'oubliez pas d'utiliser le même loggerContext.

Il est facile de modifier le niveau de journalisation avec l’enregistreur racine fourni à partir de loggerContext.

root.setLevel(Level.DEBUG);
0
pablo127

Basé sur la réponse de massimo virgilio, j'ai également réussi à le faire avec slf4j-log4j en utilisant l'introspection. 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
Guido

Voici une solution lambda pas aussi conviviale que celle de @Paul Croarkin (le niveau est effectivement dépassé deux fois). Mais je pense (a) que l’utilisateur devrait passer le Logger; et b) AFAIU, la question initiale ne demandait pas un moyen pratique de se déplacer partout dans l’application, mais une situation avec peu d’usages dans une bibliothèque.

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

Etant donné que slf4j autorise un Throwable (dont la trace de pile doit être journalisée) dans le paramètre varargs , je pense qu’il n’est pas nécessaire de surcharger la méthode auxiliaire log pour des consommateurs autres que (String, Object[]).

0
EndlosSchleife

La méthode que j'utilise consiste à importer les modules ch.qos.logback, puis à transtyper l'instance de Slf4j Logger en type ch.qos.logback.classic.Logger. Cette instance inclut une méthode 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 );

Pour connaître les niveaux de journalisation possibles, vous pouvez décomposer la classe ch.qos.logback pour afficher toutes les valeurs possibles pour Level :

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

Les résultats sont les suivants:

{
   // ...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;
}
0
Glenn Inn

J'ai pu le faire pour la liaison JDK14 en demandant d'abord l'instance de l'enregistreur SLF4J et ensuite en définissant le niveau de la liaison - vous pouvez essayer ceci pour la liaison 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
youurayy