Configuración del nivel de registro del mensaje en tiempo de ejecución en slf4j


100

Cuando se usa log4j, el Logger.log(Priority p, Object message)método está disponible y se puede usar para registrar un mensaje en un nivel de registro determinado en tiempo de ejecución. Estamos usando este hecho y este consejo para redirigir stderr a un registrador en un nivel de registro específico.

slf4j no tiene un log()método genérico que pueda encontrar. ¿Eso significa que no hay forma de implementar lo anterior?


4
Parece que hay una discusión sobre agregar esto a slf4j 2.0 en la lista de correo de desarrolladores: qos.ch/pipermail/slf4j-dev/2010-March/002865.html
Edward Dale

1
eche un vistazo a Marker, estos son datos personalizados que puede pasar a la cadena de registros.
tuxSlayer

1
@tuxSlayer, ¿puede explicar cómo usar Marker en este caso?
Variable miserable

Probablemente no sea la mejor idea para el "registro", pero puede usar varios marcadores para la "prioridad" de la entrada del registro (alta | baja | normal, información | advertir | fatal) y usar el filtrado en el registro o el appender personalizado para consumir marcadores y controlar las entradas del registro. en canales separados (información de registro, correo electrónico fatal, etc.). Sin embargo, la forma más directa es tener una fachada para esto, como se señaló en las respuestas a continuación.
tuxSlayer

2
Se supone que esta función forma parte de slf4j 2.0. jira.qos.ch/browse/SLF4J-124 Consulte mi respuesta para obtener detalles y una posible slf4j 1.xsolución alternativa.
Slartidan

Respuestas:


47

No hay forma de hacer esto con slf4j.

Me imagino que la razón por la que falta esta funcionalidad es que es casi imposible construir un Leveltipo slf4jque se pueda asignar de manera eficiente al tipo Level(o equivalente) utilizado en todas las posibles implementaciones de registro detrás de la fachada. Alternativamente, los diseñadores decidieron que su caso de uso es demasiado inusual para justificar los gastos generales de soportarlo.

En cuanto a @ ripper234 's de casos de uso (prueba de la unidad), creo que la solución pragmática es modificar la unidad de prueba (s) para el conocimiento de alambre duro de lo que es el sistema de registro detrás de la fachada ... slf4j al ejecutar las pruebas unitarias.


9
Realmente no es necesario un mapeo. Hay cinco niveles ya definidos implícitamente por los métodos en org.slf4j.Logger: debug, error, info, trace, warn.
Edward Dale

1
Y los problemas se cerraron como no válidos. Por lo que entiendo esto, es una elección de diseño deliberada.
ripper234

9
@ ripper234 - No creo que su error haya abordado el mismo problema que la pregunta original de scompt.com. Preguntó acerca de cómo configurar el nivel del sistema de registro subyacente a través de la API SLF4J. Lo que buscaba scompt.com era un método de 'registro' genérico en la API SLF4J, que toma el nivel de registro del mensaje como parámetro.
Richard Fearn

1
+1 @RichardFearn Y no se puede deshacer el comentario a favor después de 60 segundos, meh . Mientras tanto, la solicitud de función existe: bugzilla.slf4j.org/show_bug.cgi?id=133
enero

3
Los enlaces RFE ya no se resuelven. Los enlaces relevantes ahora son: jira.qos.ch/browse/SLF4J-124 y jira.qos.ch/browse/SLF4J-197 ... y ambos se han cerrado. Lea los comentarios para conocer la justificación.
Stephen C

27

Richard Fearn tiene la idea correcta, así que escribí la clase completa basándome en su código esqueleto. Es de esperar que sea lo suficientemente corto para publicarlo aquí. Copie y pegue para disfrutar. Probablemente también debería agregar algún encantamiento mágico: "Este código es de dominio público"

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

Esto sería más fácil de usar con un parámetro args variadic (Object ...).
Anonymoose

"org.slf4j.Logger" tiene bastantes firmas de métodos de registro que no se manejan en la clase anterior, por lo que probablemente se justifique una extensión: slf4j.org/api/org/slf4j/Logger.html
David Tonhofer

1
Creo que esta implementación agregará un cambio no deseado. Cuando usa la llamada logger.info (...), el registrador tiene acceso a la clase y al método de la persona que llama y podría agregarse a la entrada del registro automáticamente. Ahora, con esta implementación, el registro de llamadas (logger, level, txt) producirá una entrada de registro que siempre tendrá la misma persona que llama: Loglevel.log. Estoy en lo cierto?
Domingo

@Domin Hola, quiere decir que el registrador podría examinar la pila de llamadas actual y luego extraer la última entrada para el registro automático, ¿cuál no es el caso aquí? En principio, sí, pero de hecho, la pila crecerá un poco más incluso después de esto hasta que se escriba el mensaje real (en particular, se debe llamar al logback en algún momento, y luego al appender real). Creo que debería ser el papel del appender deshacerse de las líneas de pila no interesantes, de modo que pueda adaptarlo para deshacerse de todo, incluida la llamada a esta clase Loglevel.
David Tonhofer

@David, Sí, tienes razón :-). No estoy seguro de que sea una tarea para el appender porque en ese caso está definiendo una dependencia dura entre el appender y el registrador ... pero ... es una solución. Gracias David
Domin

14

Intente cambiar a Logback y use

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

Creo que esta será la única llamada a Logback y el resto de su código permanecerá sin cambios. Logback usa SLF4J y la migración será sencilla, solo los archivos de configuración xml deberán cambiarse.

Recuerde volver a configurar el nivel de registro una vez que haya terminado.


Ya estaba usando slf4j respaldado por Logback, y esto me permitió instantáneamente limpiar mis pruebas unitarias. ¡Gracias!
Lambart

2
Este fue mi primer -1, gracias. Creo que estás equivocado. Logback usa SLF4J, por lo que la respuesta ES relevante.
Αλέκος

3
@AlexandrosGelbessis Deberías releer la pregunta. Se solicitó un método que pudiera registrar programáticamente un mensaje de registro en cualquier nivel. Está cambiando el nivel del registrador raíz para todos los mensajes, no solo para uno.
enero

12

Puede implementar esto usando 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);
    }
}

Bueno, sí ... pero ahora necesita modificar su base de código para usar esta API, así como o en lugar de slf4j. Si lo usa en lugar de slf4j 1) probablemente necesite ser más rico, 2) muchas (al menos) importaciones deben cambiarse y 3) esta nueva capa frente a slf4j agrega una sobrecarga de registro adicional.
Stephen C

4
También tenga en cuenta que cuando opte por esta solución, la clase que realiza el registro real no se registrará (porque el registrador se inicializa con LevelLogger), lo cual no es bueno porque generalmente es información muy útil.
Lirón

6

Esto se puede hacer con un enummétodo auxiliar y uno:

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

Puede agregar otras variantes de log, digamos, si desea equivalentes genéricos de 1 parámetro o 2 parámetros de SLF4J warn/ error/ etc. métodos.


3
Es cierto, pero el propósito de slf4j es no tener que escribir contenedores de registro.
djjeck

5
El propósito de SLF4J es proporcionar una abstracción para diferentes marcos de registro. Si esa abstracción no proporciona exactamente lo que necesita, no tiene más remedio que escribir un método auxiliar. La única otra alternativa es contribuir con un método como el de mi respuesta al proyecto SLF4J.
Richard Fearn

Estoy de acuerdo, pero en este caso hay salvedades, como que ya no podrá proporcionar el número de archivo y línea, a menos que haya implementado otra solución alternativa para eso. En este caso, me habría quedado con log4j, hasta que el marco admitiera la función, lo que finalmente sucedió a través de una extensión, consulte la respuesta más reciente de Robert Elliot.
djjeck


3

Solo necesitaba algo así y se me ocurrió:

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

uso:

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

El registrador se pasa durante la invocación, por lo que la información de la clase debería estar bien y funciona bien con la anotación @ Slf4j lombok.


Muchas gracias por este enfoque increíble. Publiqué una respuesta similar, basada en su idea.
Slartidan

DEBUGfalta como constante.
Slartidan

Esta solución siempre se registrará LogLevelcomo clase y logcomo método, lo que hace que los registros sean menos significativos.
Slartidan

2

Es no es posible especificar un nivel de registro en sjf4j 1.xfuera de la caja. Pero hay esperanzas de que slf4j 2.0solucione el problema . En 2.0, podría verse así:

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

Mientras tanto, para slf4j 1.x, puede utilizar esta solución alternativa:

Copia esta clase en tu classpath:

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

Entonces puedes usarlo así:

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

Esto generará un registro como este:

[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

¿Vale la pena?

  • ProMantiene la ubicación del código fuente (nombres de clases, nombres de métodos, números de línea apuntarán a su código)
  • ProPuede definir fácilmente variables , parámetros y tipos de retorno comoLogLevel
  • ProSu código comercial es breve y fácil de leer, y no se requieren dependencias adicionales.

El código fuente como ejemplo mínimo está alojado en GitHub .


Nota: la LogMethodinterfaz debe ser pública para que funcione con clases fuera de su paquete. Aparte de eso, funciona según lo previsto. ¡Gracias!
andrebrait

1

No es posible con la API slf4j cambiar dinámicamente el nivel de registro, pero puede configurar el logback (si lo usa) por su cuenta. En ese caso, cree una clase de fábrica para su registrador e implemente el registrador raíz con la configuración que necesita.

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

Después de configurar el registrador raíz (solo una vez es suficiente), puede delegar la obtención de un nuevo registrador por

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

Recuerda usar el mismo loggerContext.

Cambiar el nivel de registro es fácil de hacer con el registrador raíz proporcionado por loggerContext.

root.setLevel(Level.DEBUG);

1

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

Obtendrás resultado:

2020-05-14 14: 01: 16,644 TRACE [] [oakcmMetrics] Trabajador de prueba Métrica registrada denominada MetricName [nombre = bufferpool-wait-time-total, group = producer-metrics, description = El tiempo total que un appender espera la asignación de espacio ., etiquetas = {id-cliente = productor-2}]


0

Acabo de encontrar una necesidad similar. En mi caso, slf4j está configurado con el adaptador de registro de Java (el jdk14). Usando el siguiente fragmento de código, logré cambiar el nivel de depuración en tiempo de ejecución:

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
Como otras respuestas, esto no aborda la pregunta original, es un problema diferente.
E-Riz

0

Basado en la respuesta de massimo virgilio, también logré hacerlo con slf4j-log4j usando introspección. 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

Aquí hay una solución lambda no tan fácil de usar como la de @Paul Croarkin en un sentido (el nivel se pasa efectivamente dos veces). Pero creo que (a) el usuario debería pasar el Logger; y (b) AFAIU, la pregunta original no pedía una forma conveniente para todas las partes de la aplicación, solo una situación con pocos usos dentro de una biblioteca.

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

Dado que slf4j permite un Throwable (cuyo seguimiento de pila debe registrarse) dentro del parámetro varargs , creo que no hay necesidad de sobrecargar el logmétodo auxiliar para otros consumidores que no sean (String, Object[]).


0

Pude hacer esto para el enlace JDK14 solicitando primero la instancia de SLF4J Logger y luego estableciendo el nivel en el enlace; puede probar esto para el enlace 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

El método que utilizo es importar los módulos ch.qos.logback y luego convertir la instancia de slf4j Logger en un ch.qos.logback.classic.Logger. Esta instancia incluye un método 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 );

Para averiguar los posibles niveles de registro, puede explotar la clase ch.qos.logback para ver todos los valores posibles para el nivel :

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

Los resultados son los siguientes:

{
   // ...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

usando la introspección de java puede hacerlo, por ejemplo:

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
Esto se refiere explícitamente a log4j y no a slf4j genéricamente
Thorbjørn Ravn Andersen

-6

no, tiene varios métodos, info (), debug (), warn (), etc. (esto reemplaza el campo de prioridad)

eche un vistazo a http://www.slf4j.org/api/org/slf4j/Logger.html para obtener la API completa de Logger.


lo siento, veo lo que estás preguntando ahora. no, no hay una forma genérica de cambiar el nivel de registro en tiempo de ejecución, pero podría implementar fácilmente un método auxiliar con una declaración de cambio.
chris

Sí, pero debe hacerlo una vez por cada versión sobrecargada del método "log".
Andrew Swan
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.