Logging em Java e facade

Nesse post vou falar sobre logging em Java, mais especificamente de SLF4J e comentar sobre o design pattern facade.

SLF4J

O SLF4J (abreviação de simple logging facade for Java) é um bom exemplo do design pattern facade (do francês fachada).

O padrão facade consiste basicamente de uma interface que abstrai a implementação. Isso é interessante para questões de manutenabilidade. Outra vantagem é que podemos utilizar diferentes implementações dessa interface sem ter que modificar o código que a utiliza.

Junto com a api do SLF4J, vem uma implementação padrão chamada slf4j-simple que simplesmente joga o logging para a saída padrão. Há outras implementações para essa api tais como:

  • log4j
  • logback
  • commons-logging
  • java.util.logging

Desses quatro, apenas o logback implementa nativamente as interfaces do SLF4J. Para as outras implementações, é necessária uma camada de adaptação. O diagrama de classes a seguir, retirado do manual do SLF4J, explica bem isso:

Fonte: http://www.slf4j.org/images/concrete-bindings.png

Diagrama (clique para ampliar)

Note que no caso da commons-logging, existe outra camada de adaptação. Isso porque na verdade, essa é uma implementação da api JLC (jakarta commons logging) da Apache. Assim, a primeira camada de abstração é uma conversão da SLF4J para a JLC, ficando a escolha da implementação da JCL a cargo da mesma.

A escolha da implementação do SLF4J é feita com base em qual delas é encontrada primeiro no classpath. Um exemplo básico de uso dessa api é:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = 
      LoggerFactory.getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}

Se nenhuma implementação for encontrada no classpath, a seguinte mensagem de erro é exibida:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

e nesse caso, como podemos ver pelo diagrama, toda a saída é descartada.

Logback

A documentação do SLF4J é bem esparsa e não descreve a maior parte de suas funcionalidades. Como o logback implementa essa api, aproveitei que sua documentação é mais detalhada para entender melhor quais os principais aspectos do SLF4J.

Na verdade, o logback possui três módulos, o logback-core, o logback-classic e o logback-access. A implementação do SLF4J é dada pelo logback-classic que depende do logback-core.

Configuração

Embora a ideia da interface permita abstrair a implementação, pode haver particularidades de cada implementação que não são cobertas pela interface. Para isso, o logback utiliza arquivos de configuração.

O logbak utiliza uma configuração padrão, mas é possível sobrescrevê-la através de um arquivo xml que deve ser chamado logback.xml (ou logback-test.xml).

Para uma referência completa sobre o arquivo de configuração do logback, refira-se ao manual.

Níveis de logging

Há os seguintes níveis de logging: TRACE, DEBUG, INFO, WARN e ERROR. Existe um método na classe Logger para cada um desses níveis.

Quando uma mensagem é enviada através de um desses métodos, ela só é gravada se o modo no qual o logger foi setado for menor ou igual ao modo especificado pelo método. A ordem dos níveis é TRACE < DEBUG < INFO < WARN < ERROR.

Por exemplo, se escolhermos o modo do logger como INFO no arquivo de configurações,

<configuration>
    ...
    <logger name="main.App" level="INFO"/>
</configuration>

só as mensagens enviadas para métodos com nível acima ou igual a INFO serão gravadas, no caso .info(...) e .error(...).

package main;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
    
    public static void main(String[] args) {
        Logger logger = 
            LoggerFactory.getLogger(App.class);
        logger.debug("A"); // não aparece
        logger.info("B"); // aparece
        logger.error("C"); // aparece
    }
}

Appender e Layout

Um appender basicamente diz aonde as mensagens de log deverão ser gravadas. Algumas saídas possíveis são console (stdout ou stderr), arquivo, banco de dados, servidor remoto, etc.

Já o layout define o formato de cada mensagem gravada. No logback é possível escrever seu próprio layout, mas a implementação padrão, o PatternLayout, é bem flexível. Ela utiliza variáveis (padrões) que são substituídas de acordo com a ocasião. Essas variáveis são precedidas pelo caractere %. Uma lista resumida de padrões dessa classe é a seguinte:

* %class – imprime o nome da classe de onde o método foi chamado
* %date (%d) – data atual (possível formatar)
* %level – o nível de logging
* %logger – imprime o nome do logger
* %line – número da linha de onde o método foi chamado
* %message (%msg) – a mensagem passada como parâmetro
* %thread – nome da thread
* %n – quebra de linha. Equivalente ao \n

Modificadores:

* %<valor> – padding de <valor> espaços à direita (ex. %-5msg)
* %<valor> – padding de <valor> espaços à esquerda (ex. %5msg)

Com isso podemos interpretar o que o trecho a seguir significa:

%d [%thread] %-5level %logger - %msg%n

Uma mensagem formatada com esse padrão consiste da data de quando a mensagem foi enviada, seguida do nível de logging do método, seguido do nome do logger e então a mensagem em si.

Para uma referência completa sobre os padrões e modificadores, consulte o manual.

Agora podemos configurar o logback para imprimir o log na saída de erro por exemplo,

<configuration>

  <appender name="my-apender" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <target>
      System.err
    </target>
  </appender>

  <logger name="main.App" level="TRACE">
    <appender-ref ref="my-apender" />
  </logger>

</configuration>

Basicamente a classe ch.qos.logback.core.ConsoleAppender refere-se ao console e a saída de erro é especificada pelo atributo target (padrão é stdout). Depois o appender é associado ao log “main.App”, que é o log que criamos num exemplo anterior. Para mais informações sobre o appender, veja o manual.

Mensagens parametrizadas

Uma funcionalidade provida pela SLF4J é o uso de mensagens parametrizadas para a melhoria de desempenho. Considere o seguinte caso:

logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

Embora a mensagem seja somente logada no modo de debug, o custo de construí-la (conversão do objeto para string e concatenação) será pago em qualquer modo. Uma alternativa é colocar um if, como no exemplo a seguir

if(logger.isDebugEnabled()) {
  logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

Além de deixar a sintaxe mais carregada, ainda temos um overhead adicional por conta da condicional. Podemos então usar mensagens parametrizadas da seguinte forma:

logger.debug("The entry is {}.", entry);

Nesse caso a substituição só é feita se o estiver no modo debug.

Leitura adicional

[1] História de logging em Java
[2] Manual do logback
[3] Reasons to prefer logback over log4j

Uma resposta a Logging em Java e facade

  1. Roger Santos diz:

    Muito útil! Valeu!

%d bloggers like this: