Flogger Fluent Logging
1. Visão geral
Neste tutorial, vamos falar sobre a estruturaFlogger, uma API de registro fluente para Java desenvolvida pelo Google.
2. Por que usar o Flogger?
Com todas as estruturas de registro atualmente disponíveis no mercado, como Log4j e Logback, por que precisamos de outra estrutura de registro?
Acontece que o Flogger tem várias vantagens sobre outros frameworks - vamos dar uma olhada.
2.1. Legibilidade
A natureza fluente da API do Flogger contribui muito para torná-la mais legível.
Vejamos um exemplo em que queremos registrar uma mensagem a cada dez iterações.
Com uma estrutura de registro tradicional, veríamos algo como:
int i = 0;
// ...
if (i % 10 == 0) {
logger.info("This log shows every 10 iterations");
i++;
}
Mas agora, com o Flogger, o acima pode ser simplificado para:
logger.atInfo().every(10).log("This log shows every 10 iterations");
Embora alguém possa argumentar que a versão do Flogger da declaração do logger parece um pouco mais prolixa do que as versões tradicionais,it does permit greater functionality and ultimately leads to more readable and expressive log statements.
2.2. atuação
Os objetos de registro são otimizados, desde que evitemos chamartoString nos objetos registrados:
User user = new User();
logger.atInfo().log("The user is: %s", user);
Se registrarmos, como mostrado acima, o back-end terá a oportunidade de otimizar o registro. Por outro lado, se chamarmostoString diretamente ou concatenarmos as strings, esta oportunidade será perdida:
logger.atInfo().log("Ths user is: %s", user.toString());
logger.atInfo().log("Ths user is: %s" + user);
2.3. Extensibilidade
A estrutura do Flogger já cobre a maioria das funcionalidades básicas que esperamos de uma estrutura de registro.
No entanto, há casos em que precisaríamos adicionar a funcionalidade. Nesses casos, é possível estender a API.
Currently, this requires a separate supporting class. Poderíamos, por exemplo, estender a API do Flogger escrevendo um sclassUserLogger :
logger.at(INFO).forUserId(id).withUsername(username).log("Message: %s", param);
Isso pode ser útil nos casos em que queremos formatar a mensagem de forma consistente. OUserLogger deve então fornecer a implementação para os métodos personalizadosforUserId(String id)ewithUsername(String username).
Para fazer isso,the UserLogger class will have to extend the AbstractLogger class and provide an implementation for the API. Se olharmos paraFluentLogger, é apenas um registrador sem métodos adicionais, podemos, portanto,start by copying this class as-is, and then build up from this foundation by adding methods to it.
2.4. Eficiência
Estruturas tradicionais usam extensivamente varargs. Esses métodos requerem que um novoObject[] seja alocado e preenchido antes que o método possa ser chamado. Além disso, todos os tipos fundamentais passados devem ter caixa automática.
This all costs additional bytecode and latency at the call site. É particularmente lamentávelif the log statement isn’t actually enabled. O custo se torna mais aparente em logs de nível de depuração que aparecem frequentemente em loops. Flogger reduz esses custos, evitando totalmente as variáveis.
Flogger works around this problem by using a fluent call chain from which logging statements can be built. Isso permite que a estrutura tenha apenas um pequeno número de substituições para o métodolog e, portanto, seja capaz de evitar coisas como varargs e auto-boxing. This means that the API can accommodate a variety of new features without a combinatorial explosion.
Uma estrutura típica de log teria estes métodos:
level(String, Object)
level(String, Object...)
ondelevel pode ser um dos cerca de sete nomes de nível de log (severe, por exemplo), bem como ter um método de log canônico que aceita um nível de log adicional:
log(Level, Object...)
Além disso, geralmente existem variantes dos métodos que levam uma causa (uma instância deThrowable) que está associada à instrução de log:
level(Throwable, String, Object)
level(Throwable, String, Object...)
É claro que a API está combinando três questões em uma chamada de método:
-
Ele está tentando especificar o nível de registro (escolha do método)
-
Tentando anexar metadados à instrução de log(Throwable causa)
-
E também, especificando a mensagem e os argumentos de log.
Essa abordagem multiplica rapidamente o número de diferentes métodos de registro necessários para satisfazer essas preocupações independentes.
Agora podemos ver por que é importante ter dois métodos na cadeia:
logger.atInfo().withCause(e).log("Message: %s", arg);
Vamos agora dar uma olhada em como podemos usá-lo em nossa base de código.
3. Dependências
É muito simples configurar o Flogger. Precisamos apenas adicionarfloggereflogger-system-backend ao nossopom:
com.google.flogger
flogger
0.4
com.google.flogger
flogger-system-backend
0.4
runtime
Com essas dependências configuradas, agora podemos explorar a API que está à nossa disposição.
4. Explorando a API Fluente
Primeiro, vamos declarar uma instânciastatic para nosso logger:
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
E agora podemos começar a registrar. Vamos começar com algo simples:
int result = 45 / 3;
logger.atInfo().log("The result is %d", result);
As mensagens de log podem usar qualquer um dos especificadores de formatoprintf do Java, como%s, %d ou%016x.
4.1. Evitando o trabalho em sites de log
Os criadores do Flogger recomendam que evitemos trabalhar no site de registro.
Digamos que temos o seguinte método de longa duração para resumir o estado atual de um componente:
public static String collectSummaries() {
longRunningProcess();
int items = 110;
int s = 30;
return String.format("%d seconds elapsed so far. %d items pending processing", s, items);
}
É tentador chamarcollectSummaries diretamente em nossa declaração de log:
logger.atFine().log("stats=%s", collectSummaries());
Independentemente dos níveis de log configurados ou da limitação de taxa, o métodocollectSummaries agora será chamado todas as vezes.
Tornar o custo das instruções de log desabilitadas praticamente gratuito é o cerne da estrutura de log. Isso, por sua vez, significa que mais deles podem permanecer intactos no código sem causar danos. Escrever a declaração de log como acabamos de tirar tira essa vantagem.
Instead, we should do use the LazyArgs.lazy method:
logger.atFine().log("stats=%s", LazyArgs.lazy(() -> collectSummaries()));
Now, almost no work is done at the log site - apenas criação de instância para a expressão lambda. Flogger will only evaluate this lambda if it intends to actually log the message.
Embora seja permitido guardar declarações de log usandoisEnabled:
if (logger.atFine().isEnabled()) {
logger.atFine().log("summaries=%s", collectSummaries());
}
Isso não é necessário e devemos evitá-lo porque o Flogger faz essas verificações para nós. Essa abordagem também protege apenas as declarações de log por nível e não ajuda nas declarações de log com taxa limitada.
4.2. Lidando com exceções
Que tal exceções, como lidamos com elas?
Bem, o Flogger vem com um métodowithStackTrace que podemos usar para registrar uma instânciaThrowable:
try {
int result = 45 / 0;
} catch (RuntimeException re) {
logger.atInfo().withStackTrace(StackSize.FULL).withCause(re).log("Message");
}
OndewithStackTrace toma como argumento o enumStackSize com valores constantesSMALL, MEDIUM, LARGE ouFULL. Um rastreamento de pilha gerado porwithStackTrace() aparecerá como uma exceçãoLogSiteStackTrace no back-end padrãojava.util.logging. Outros back-end podem optar por lidar com isso de maneira diferente.
4.3. Configuração e níveis de log
Até agora, usamoslogger.atInfo na maioria dos nossos exemplos, mas o Flogger suporta muitos outros níveis. Veremos isso, mas primeiro, vamos apresentar como configurar as opções de registro.
Para configurar o log, usamos a classeLoggerConfig.
Por exemplo, quando queremos definir o nível de registro paraFINE:
LoggerConfig.of(logger).setLevel(Level.FINE);
E o Flogger suporta vários níveis de registro:
logger.atInfo().log("Info Message");
logger.atWarning().log("Warning Message");
logger.atSevere().log("Severe Message");
logger.atFine().log("Finest Message");
logger.atFine().log("Fine Message");
logger.atFiner().log("Finer Message");
logger.atConfig().log("Config Message");
4.4. Limitação de taxa
E a questão da limitação de taxas? Como lidamos com o caso em que não queremos registrar todas as iterações?
Flogger comes to our rescue with the every(int n) method:
IntStream.range(0, 100).forEach(value -> {
logger.atInfo().every(40).log("This log shows every 40 iterations => %d", value);
});
Obtemos a seguinte saída quando executamos o código acima:
Sep 18, 2019 5:04:02 PM com.example.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 0 [CONTEXT ratelimit_count=40 ]
Sep 18, 2019 5:04:02 PM com.example.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 40 [CONTEXT ratelimit_count=40 ]
Sep 18, 2019 5:04:02 PM com.example.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 80 [CONTEXT ratelimit_count=40 ]
E se quisermos dizer log a cada 10 segundos? Então,we can use atMostEvery(int n, TimeUnit unit):
IntStream.range(0, 1_000_0000).forEach(value -> {
logger.atInfo().atMostEvery(10, TimeUnit.SECONDS).log("This log shows [every 10 seconds] => %d", value);
});
Com isso, o resultado passa a ser:
Sep 18, 2019 5:08:06 PM com.example.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 0 [CONTEXT ratelimit_period="10 SECONDS" ]
Sep 18, 2019 5:08:16 PM com.example.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 3545373 [CONTEXT ratelimit_period="10 SECONDS [skipped: 3545372]" ]
Sep 18, 2019 5:08:26 PM com.example.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 7236301 [CONTEXT ratelimit_period="10 SECONDS [skipped: 3690927]" ]
5. Usando o Flogger com outros back-ends
Então, e se quisermosadd Flogger to our existing application that is already using say Slf4j or Log4j, por exemplo? Isso pode ser útil nos casos em que gostaríamos de tirar proveito de nossas configurações existentes. O Flogger oferece suporte a vários back-ends, como veremos.
5.1 Flogger With Slf4j
É simples configurar um back-end Slf4j. Primeiro, precisamos adicionar a dependênciaflogger-slf4j-backend ao nossopom:
com.google.flogger
flogger-slf4j-backend
0.4
Em seguida, precisamos informar ao Flogger que gostaríamos de usar um back-end diferente do padrão. Fazemos isso registrando uma fábrica Flogger através das propriedades do sistema:
System.setProperty(
"flogger.backend_factory", "com.google.common.flogger.backend.slf4j.Slf4jBackendFactory#getInstance");
E agora nosso aplicativo usará a configuração existente.
5.1 Flogger With Log4j
Seguimos etapas semelhantes para configurar o back-end do Log4j. Vamos adicionar a dependênciaflogger-log4j-backend ao nossopom:
com.google.flogger
flogger-log4j-backend
0.4
com.sun.jmx
jmxri
com.sun.jdmk
jmxtools
javax.jms
jms
log4j
log4j
1.2.17
log4j
apache-log4j-extras
1.2.17
Também precisamos registrar uma fábrica de back-end da Flogger para o Log4j:
System.setProperty(
"flogger.backend_factory", "com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");
E é isso, nosso aplicativo agora está configurado para usar as configurações existentes do Log4j!
6. Conclusão
Neste tutorial, vimos como usar a estrutura Flogger como uma alternativa para as estruturas de registro tradicionais. Vimos alguns recursos poderosos dos quais podemos nos beneficiar ao usar a estrutura.
Também vimos como podemos alavancar nossas configurações existentes, registrando diferentes back-ends como Slf4j e Log4j.
Como de costume, o código-fonte deste tutorial está disponívelover on GitHub.