
Desafio
Suponha que desejássemos registrar cada método de uma determinada classe Java de maneira diferente:
- Cada método possui seu próprio arquivo de log,
- ... seu formato de log,
- ... seu nível mínimo de registro,
- expandimos o formato do log com nossas próprias
%
, - capacidade de atualizar essa configuração em tempo real.
Este artigo mostra como atender a esses requisitos. Para manter a simplicidade, a separação do log é realizada apenas por métodos; na realidade, você pode querer ter uma configuração hierárquica qualificada, como
→
→
→
... Um link para o código fonte completo estará abaixo.
Código do cliente
class ThingService { log = LoggerFactory.getLogger(); getThing() { log.debug("getThing...");
Logback
Para a implementação, uma sólida biblioteca de log "logback" foi selecionada, o que fornece possibilidades interessantes para personalização:
ch.qos.logback:logback-classic:1.2.3
Ele é configurado a partir de uma configuração XML e diretamente de Java. As abordagens podem ser combinadas:
public void configureLogback() throws JoranException { LoggerContext lc = LoggerFactory.getILoggerFactory(); lc.reset();
Brevemente sobre o log:
- O programador puxa o logger ,
- O logger puxa os anexos atribuídos a ele,
- O appender pensa e chama o codificador ,
- O codificador formata exatamente uma linha do log,
- Para fazer isso, ele puxa uma cadeia de conversores , cada um dos quais revela sua
%
, - Sucesso.
Para simplificar, foi escolhida uma configuração Java pura. Tudo é bastante óbvio aqui, se você tiver em mente a configuração XML. A principal tarefa é criar seu próprio appender / codificador e registrá-los - eles serão chamados pelo logback de suas entranhas. Quase todos os objetos que você cria devem ser lembrados para começar a usar o método start()
. Exemplo abstrato:
Logger rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); LoggerContext lc = rootLogger.getLoggerContext(); lc.reset();
Separar métodos registrados um do outro
Para que o logback possa distinguir um método do outro, antes de chamar o método, salve seu nome no Contexto de Diagnóstico Mapeado ThreadLocal
. Além disso, durante a análise, não obtemos esses valores diretamente da classe MDC
, pois o código de log será executado em outro encadeamento e esses dados não estarão lá - os obtemos através de ILoggingEvent.getMDCPropertyMap()
.
No caso geral, como a vooft observou corretamente, você precisa dar suporte à pilha de chamadas e não substituir o valor MDC, mas retorná-lo ao quadro anterior, o que é feito através da introdução de um novo ThreadLocal
. Exemplo esquemático:
try { MDC.put(MDC_KEY_METHOD, currentMethod);
Arquivo de log próprio para cada método
Vamos criar e não se esqueça de registrar seu próprio appender:
class MultiAppender extends AppenderBase<ILoggingEvent> { @Override protected void append(ILoggingEvent event) { method = event.getMDCPropertyMap().get(MDC_KEY_METHOD); Appender appender = getOrCreateAppender(method); appender.doAppend(event); }
Ele próprio quase não faz nada, apenas delega o log para um pacote de anexadores de arquivos reais, um para cada método. Delegado a um, o mais adequado. Apêndices "reais" são criados sob demanda, da seguinte maneira:
fileAppender = new FileAppender<ILoggingEvent>(); fileAppender.setContext(lc); fileAppender.setAppend(false); fileAppender.setEncoder(getOrCreateEncoderByMethod(lc, method)); fileAppender.setFile(logFileByMethod.get(method)); fileAppender.start();
Para fazer isso, mantenha o cache de objetos criados automaticamente do tipo Encoder
:
Map<String, String> patternByMethod = new HashMap<>();
Cada método possui seu próprio nível de log
Adicionamos uma verificação à classe MultiAppender
: se o nível do evento MultiAppender
limite especificado para o método, apenas o registraremos:
Map<String, Level> levelByMethod = new HashMap<>(); protected void append(ILoggingEvent event) { Level minLevel = levelByMethod.get(methodName); if (event.getLevel().levelInt >= minLevel.levelInt) { appender.doAppend(event); }
Em princípio, essa lógica pode ser colocada no filtro.
Para não cercar seu jardim, mas para tirar proveito da infraestrutura comprovada de logback, você precisa definir sua própria classe de conversores, certifique-se de publicamente para que ele possa ser instanciado do lado de fora. Se você precisar do MDC
, tire-o do evento. O manipulador de variável %custom
começa aqui:
public class CustomConverter extends ClassicConverter { public String convert(ILoggingEvent event) {
Durante o processo de configuração geral, registre o manipulador:
void configurePatterns(LoggerContext lc) { Map<String, String> rules = lc.getObject(CoreConstants.PATTERN_RULE_REGISTRY); if (rules == null) { rules = new HashMap<String, String>(); lc.putObject(CoreConstants.PATTERN_RULE_REGISTRY, rules); } rules.put("custom", CustomConverter.class.getName()); }
E usaremos como codificador, por exemplo, PatternLayoutEncoder
, que coletará tudo. Nesse caso, a variável %custom
será expandida para a cadeia "variable-expanded"
.
Atualização de configuração em tempo real
Existe essa oportunidade LoggerContext::reset()
para LoggerContext::reset()
basta chamar a função configurador novamente, não esquecendo de fazer LoggerContext::reset()
e limpar o cache acumulado.
Multithreading
Se o método configurado por nós reviver novos encadeamentos, é claro que as regras de log especificadas não se aplicarão a eles - os locais de encadeamentos não aparecerão sozinhos no novo encadeamento. Portanto, se você deseja aplicar as configurações do método a um novo fluxo, é necessário copiar o MDC
lá:
Map<String, String> mdcOrig = MDC.getCopyOfContextMap(); ExecutorService es = Executors.newFixedThreadPool(1); es.submit(() -> threadWorker(mdcOrig)); void threadWorker(Map<String, String> parentMdc) { MDC.setContextMap(parentMdc); log.error("expected to appear in method2*.log"); }
Exemplo inteiro
https://github.com/zencd/logback-setup
Literatura
Manual Oficial de Logback