Definindo o nível de registro da mensagem em tempo de execução em slf4j


100

Ao usar log4j, o Logger.log(Priority p, Object message)método está disponível e pode ser usado para registrar uma mensagem em um nível de registro determinado no tempo de execução. Estamos usando esse fato e esta dica para redirecionar stderr para um logger em um nível de log específico.

slf4j não tem um log()método genérico que eu possa encontrar. Isso significa que não há como implementar o acima?


4
Parece que há alguma discussão sobre como adicionar isso ao slf4j 2.0 na lista de e-mails de desenvolvimento: qos.ch/pipermail/slf4j-dev/2010-March/002865.html
Edward Dale

1
dê uma olhada no Marker, são dados personalizados que você pode passar para a cadeia de log.
tuxSlayer

1
@tuxSlayer você pode explicar como usar o Marker neste caso?
Variável miserável de

Provavelmente não é a melhor ideia para "registro", mas você pode usar vários marcadores para a entrada de registro "prioridade" (alta | baixa | normal, informação | aviso | fatal) e usar a filtragem no logback ou anexador personalizado para consumir marcadores e gerar entradas de registro em canais separados (informações de log, e-mail fatal etc.). Porém, a maneira mais direta é ter uma fachada para isso, como foi apontado nas respostas abaixo.
tuxSlayer

2
Este recurso deve fazer parte do slf4j 2.0. jira.qos.ch/browse/SLF4J-124 Veja minha resposta para detalhes e para uma possível slf4j 1.xsolução.
Slartidan

Respostas:


47

Não há como fazer isso slf4j.

Eu imagino que a razão pela qual essa funcionalidade está faltando é que é quase impossível construir um Leveltipo para slf4jque possa ser mapeado com eficiência para o tipo Level(ou equivalente) usado em todas as implementações de log por trás da fachada. Como alternativa, os designers decidiram que seu caso de uso é muito incomum para justificar as despesas gerais de suportá-lo.

Em relação @ ripper234 's de casos de uso (teste de unidade), eu acho que a solução pragmática é modificar o teste de unidade (s) ao conhecimento hard-wire do que sistema de registro está por trás da fachada slf4j ... ao executar os testes de unidade.


9
Não há realmente nenhum mapeamento necessário. Existem cinco níveis já definidos implicitamente pelos métodos em org.slf4j.Logger: debug, error, info, trace, warn.
Edward Dale

1
E os problemas foram encerrados como inválidos. Pelo que eu entendo, é uma escolha de design deliberada.
ripper234

9
@ ripper234 - Não acho que seu bug abordou o mesmo problema da pergunta original de scompt.com. Você perguntou sobre a configuração do nível do sistema de registro subjacente por meio da API SLF4J. O que o scompt.com queria era um método genérico de 'log' na API SLF4J, que leva o nível de registro da mensagem como parâmetro.
Richard Fearn

1
+1 @RichardFearn E não se pode desfazer o upvote do comentário após 60 segundos, meh . A solicitação de recurso já existe: bugzilla.slf4j.org/show_bug.cgi?id=133
janeiro

3
Os links RFE não resolvem mais. Os links relevantes agora são: jira.qos.ch/browse/SLF4J-124 e jira.qos.ch/browse/SLF4J-197 ... e ambos foram fechados. Leia os comentários para a justificativa.
Stephen C

27

Richard Fearn teve a ideia certa, então escrevi a classe completa com base em seu código de esqueleto. Espero que seja curto o suficiente para postar aqui. Copie e cole para se divertir. Eu provavelmente deveria adicionar algum encantamento mágico também: "Este código foi lançado para o domínio 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;
    }
}

Isso seria mais fácil de usar com um parâmetro args variadic (Object ...).
Anonymoose

"org.slf4j.Logger" tem algumas assinaturas de método de registro que não são manipuladas na classe acima, portanto, uma extensão provavelmente é garantida: slf4j.org/api/org/slf4j/Logger.html
David Tonhofer

1
Eu acho que esta implementação irá adicionar uma mudança não desejada. Quando você usa o call logger.info (...), o logger tem acesso à classe e ao método do chamador e pode ser adicionado à entrada de registro automaticamente. Agora, com esta implementação, o registro de chamadas (logger, nível, txt) produzirá uma entrada de registro que terá sempre o mesmo chamador: Loglevel.log. Estou certo?
Domin

@Domin Oi, você quer dizer que o logger pode examinar a pilha de chamadas atual e extrair a última entrada para o registro automático, o que não é o caso aqui? Em princípio, sim, mas na verdade, a pilha aumentará um pouco mais mesmo depois disso, até que a mensagem real seja gravada (em particular, logback deve ser chamado em algum ponto, e então o appender real). Acho que deve ser função do anexador descartar linhas de pilha não interessantes, para que você possa adaptá-lo para descartar tudo, incluindo a chamada para essa classe Loglevel.
David Tonhofer

@David, Sim, você está certo :-). Não tenho certeza se é uma tarefa para o anexador porque, nesse caso, você está definindo uma dependência rígida entre o anexador e o logger ... mas ... é uma solução. Obrigado David
Domin

14

Tente mudar para Logback e 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"));

Acredito que esta será a única chamada para Logback e o resto do seu código permanecerá inalterado. O Logback usa SLF4J e a migração será fácil, apenas os arquivos de configuração xml terão que ser alterados.

Lembre-se de definir o nível de registro de volta depois de terminar.


Eu já estava usando slf4j apoiado por Logback, e isso instantaneamente me permitiu limpar meus testes de unidade. Obrigado!
Lambart

2
Este foi meu primeiro -1, obrigado. Eu acredito que você está errado. O Logback usa SLF4J, então a resposta é relevante.
Αλέκος

3
@AlexandrosGelbessis Você deve reler a pergunta. Foi solicitado um método que pudesse registrar programaticamente uma mensagem de registro em qualquer nível. Você está alterando o nível do logger raiz para todas as mensagens, não apenas para uma.
janeiro,

12

Você pode implementar isso 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);
    }
}

Bem, sim ... mas agora você precisa modificar sua base de código para usar esta API tão bem como ou em vez de slf4j. Se você usá-lo em vez de slf4j 1) provavelmente precisa ser mais rico, 2) muitas (pelo menos) importações precisam ser alteradas e 3) essa nova camada na frente de slf4j adiciona uma sobrecarga de registro extra.
Stephen C,

4
Esteja ciente também de que quando você escolher essa solução, a classe que faz o log real não será registrada (porque o logger é inicializado com LevelLogger), o que não é bom porque geralmente é uma informação muito útil.
Leirão,

6

Isso pode ser feito com um enummétodo auxiliar e:

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

Você poderia adicionar outras variantes de log, digamos, se você quisesse equivalentes genéricos de 1 parâmetro ou 2 parâmetro de SLF4J warn/ error/ etc. métodos.


3
Verdade, mas o objetivo do slf4j não é escrever invólucros de log.
djjeck de

5
O objetivo do SLF4J é fornecer uma abstração para diferentes estruturas de registro. Se essa abstração não fornecer exatamente o que você precisa, você não tem escolha a não ser escrever um método auxiliar. A única outra alternativa é contribuir com um método como o da minha resposta ao projeto SLF4J.
Richard Fearn de

Eu concordo, mas neste caso há ressalvas, como a de que você não seria mais capaz de fornecer o número do arquivo e da linha, a menos que implementasse outra solução alternativa para isso. Nesse caso, eu teria ficado com o log4j, até que o framework suportasse o recurso - o que acabou acontecendo por meio de uma extensão, veja a resposta mais recente de Robert Elliot.
djjeck de


3

Eu só precisava de algo assim e vim com:

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

O logger é passado durante a invocação, então as informações da classe devem estar corretas e funciona bem com a anotação @ Slf4j lombok.


Muito obrigado por essa abordagem incrível - postei uma resposta semelhante, com base na sua ideia.
Slartidan

DEBUGestá faltando como constante.
Slartidan

Esta solução sempre logará LogLevelcomo classe e logcomo método, o que torna os logs menos significativos.
Slartidan

2

É não possível especificar um nível de log em sjf4j 1.xfora da caixa. Mas há esperança para o slf4j 2.0corrigir o problema . Na versão 2.0, pode ser assim:

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

Enquanto isso, para slf4j 1.x, você pode usar esta solução alternativa:

Copie esta classe em seu 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);
    }
}

Então você pode usá-lo assim:

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

Isso produzirá um 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 a pena?

  • ProEle mantém a localização do código-fonte (nomes de classes, nomes de métodos, números de linha apontam para o seu código)
  • ProVocê pode facilmente definir variáveis , parâmetros e tipos de retorno comoLogLevel
  • ProSeu código de negócios permanece curto e fácil de ler, sem a necessidade de dependências adicionais.

O código-fonte como exemplo mínimo está hospedado no GitHub .


Nota: a LogMethodinterface precisa ser pública para funcionar com classes fora de seu pacote. Fora isso, funciona conforme o planejado. Obrigado!
andrebrait

1

Não é possível com a API slf4j alterar dinamicamente o nível de log, mas você pode configurar o logback (se usar) por conta própria. Nesse caso, crie uma classe de fábrica para seu logger e implemente o logger root com a configuração necessária.

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

Depois de configurar o logger root (apenas uma vez é suficiente), você pode delegar a obtenção de um novo logger

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

Lembre-se de usar o mesmo loggerContext.

Alterar o nível de log é fácil de fazer com o logger root fornecido em loggerContext.

root.setLevel(Level.DEBUG);

1

Confirme a resposta 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);

Você obterá o resultado:

2020-05-14 14: 01: 16.644 TRACE [] [oakcmMetrics] Test worker Métrica registrada chamada MetricName [name = bufferpool-wait-time-total, group = producer-metrics, description = O tempo total que um anexador espera pela alocação de espaço ., tags = {id-cliente = produtor-2}]


0

Acabo de encontrar uma necessidade semelhante. No meu caso, slf4j é configurado com o adaptador de registro java (o jdk14). Usando o seguinte snippet de código, consegui alterar o nível de depuração em tempo de execução:

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 outras respostas, esta não responde à pergunta original, é um problema diferente.
E-Riz

0

Com base na resposta de massimo virgilio, também consegui fazer isso com slf4j-log4j usando introspecção. 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

Aqui está uma solução lambda não tão amigável quanto a de @Paul Croarkin de uma maneira (o nível é efetivamente passado duas vezes). Mas acho que (a) o usuário deve passar no Logger; e (b) AFAIU a questão original não era pedir uma maneira conveniente para todos os lugares do aplicativo, apenas uma situação com poucos usos dentro de uma 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);
    }
}

Como slf4j permite um Throwable (cujo rastreamento de pilha deve ser registrado) dentro do parâmetro varargs , acho que não há necessidade de sobrecarregar o logmétodo auxiliar para outros consumidores além de (String, Object[]).


0

Consegui fazer isso para a ligação JDK14 solicitando primeiro a instância SLF4J Logger e, em seguida, definindo o nível na ligação - você pode tentar isso para a ligação 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

O método que utilizo é importar os módulos ch.qos.logback e, em seguida, fazer o cast da instância slf4j Logger em um ch.qos.logback.classic.Logger. Esta instância inclui um 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 descobrir os níveis de Logging possíveis, você pode explodir a classe ch.qos.logback para ver todos os valores possíveis para Nível :

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

Os resultados são os seguintes:

{
   // ...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 a introspecção java, você pode fazer isso, por exemplo:

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
Isto se refere explicitamente a log4j e não slf4j genericamente
Thorbjørn Ravn Andersen

-6

não, tem vários métodos, info (), debug (), warn (), etc (isso substitui o campo de prioridade)

dê uma olhada em http://www.slf4j.org/api/org/slf4j/Logger.html para a API Logger completa.


desculpe, eu vejo o que você está perguntando agora. não, não há uma maneira genérica de alterar o nível de log em tempo de execução, mas você pode facilmente implementar um método auxiliar com uma instrução switch.
Chris

Sim, mas você precisa fazer isso uma vez para cada versão sobrecarregada do método "log".
Andrew Swan
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.