2. Sistema de registros#

Un aspecto fundamental en cualquier aplicación es que incluya un sistema sólido de registros (logs como se dice habitualmente en inglés), a fin de que el usuario pueda comprobar su funcionamiento. Téngase presente que, además de fundamental, es transversal y cualquier código (lo que incluye librerías de terceros que podamos usar en nuestro programa) es probable que necesite usarlo. A pesar de ello y de lo que ocurre con otros aspectos (véase el uso de ORM), Java no presenta una especificación general a la que puedan acogerse las distintas librerías de implementación.

En vez de la especificación, desde su versión 1.4, JDK incluye una librería de implementación comúnmente llamada JUL que podría ser una solución… si fuera lo suficientemente buena. En cambio, el hecho de cosechar muchísimas críticas ha provocado la aparición de librerías de terceros, lo que ha fragmentado la generación de registros y ha provocado que una aplicación pueda acabar usando múltiples librerías de implementación distintas, ya que su desarrollador ha podido escoger una para su código, pero necesitar librerías de terceros que hagan uso de implementaciones distintas.

En cualquier caso, antes de proseguir, convendría aclarar qué queremos decir con especificación e implementación.

Especificación

Es simplemente una API que define las clases y los métodos que deben usarse para expresar todas las operaciones que resuelven un problema.

Librería de implementación (o framework)

Es una librería que realmente desarrolla la solución al problema. Puede presentar su propia API o acogerse a una especificación definida o ambas cosas, por supuesto.

Java, a través de terceros, tiene definidas algunas especificaciones y algunas librerías de implementación[1], pero dado el carácter eminentemente práctico de este apéndice nos centraremos en aconsejar sin más una posible elección:

  • SLF4J como especificación.

  • Logback como implementación o, en la terminología de SLF4J, como proveedor. La librería la ha desarrollado el mismo programador que SLF4J con lo que no presenta una interfaz alternativa.

Prudencia

La especificación se limita a definir cómo se envían mensajes al sistema de registros, pero no entra en aspectos de configuración. Por tanto:

Aspecto

Definición

Envío de mensajes

Común (definida por la implemtación)

Nivel registrable

Propio de cada implementación

Soporte de salida

Formato de mensaje

Ver también

Es interesantísimo y esclarecedor el artículo How To Do Logging in Java incluido en las Guías poco convencionales de Marco Behler. El presente texto lo ha tomado inicialmente como base.

2.1. Preliminares#

Antes de empezar de lleno conviene saber cómo empezar a usar el sistema de registros.

2.1.1. Dependencias#

En nuestro proyecto deberemos enlazar al menos con:

  • La interfaz de la especificación de SLF4J, esto es, slf4j-api.

  • El proveedor que proporciona una implementación: logback-classic, log4j-core, etc. También podemos usar slf4j-simple que es una implementación muy básica que sólo permite mostrar los mensajes por la salida de errores y no nos permite cambiar el nivel, por lo que siempre se mostrarán errores hasta el nivel INFO. Si usamos como implementación JUL no necesitaremos cargar ninguna librería de terceros, puesto que ya está incluida en JDK.

  • La librería que permite vincular la implementación a SLF4J: slf4j-reload4j para log4j-core, jul-to-slf4j para JUL. Ni Logback ni slf4j-simple necesitan ninguna vinculación adicional.

En resumen:

Solución

Especificación

Implementación

Vínculo

SLF4J + Simple

slf4j-api

slf4j-simple

-

SLF4J + Logback

slf4j-api

logback-classic

-

SLF4J + Log4J

slf4j-api

log4j-core

slf4j-reload4j

SLF4J + JUL

slf4j-api

Incluida en JDK

jul-to-slf4j

2.1.2. Clasificación de mensajes#

En estos sistemas los mensajes se envían para su registro clasificándolos con dos criterios distintos:

  • El asunto del mensaje, que en los sistemas de registros de Java se asimila a la clase que origina el mensaje. Ya profundizaremos en ello cuando veamos cómo enviar mensajes.

  • La gravedad del mensaje, que en SLF4J puede ser de menor a mayor:

    Nombre

    Descripción

    TRACE

    Nivel de detalle muy, muy fino.

    DEBUG

    Nivel propio de la depuración.

    INFO

    Mensajes sobre el funcionamiento habitual.

    WARN

    Mensajes que indican circunstancias problemáticas.

    ERROR

    Mensajes de error que requieren atención inmediata.

    En ausencia de configuración adicional, el sistema muestra mensajes a partir del nivel DEBUG.

2.2. Envío de mensajes#

Para enviar mensajes lo primero es obtener una instancia Logger a partir de LoggerFactory. La convención es que se cree una instancia por clase y pasar como parámetro la propia clase para identificar lo que hemos llamado antes el asunto del mensaje:

public class CentroSqlDao implements Crud<Centro> {
   // logger servirá para registrar todos los mensajes
   // que se envían desde la clase.
   private static final Logger logger = LoggerFactory.getLogger(CentroSqlDao.class);

   // Implementación de la clase.
}

Nota

Obtener el logger proporcionando la clase es equivalente a pasar una cadena con el nombre completo de la clase. Dicho de otra forma:

LoggerFactory.getLogger(CentroSqlDao.class) == LoggerFactory.getLogger(CentroSqlDao.class.getName());  // true

En realidad, lo que identifica a los logger es la cadena; y podríamos hacer que varias clases compartieran un mismo logger simplemente generando para todas ellas loggers que comparten la misma cadena (aunque la práctica habitual es definir un logger por clase):

package edu.acceso.test.backend.sql;

public class CentroSqlDao implements Crud<Centro> {
   // Se usa para definir el logger el nombre del paquete, no de la clase.
   private static final Logger logger = LoggerFactory.getLogger(CentroSqlDao.class.getPackageName());

   // Implementación...
}
package edu.acceso.test.backend.sql;

public class EstudianteSqlDao implements Crud<Estudiante> {
   // Este logger es el mismo que el de CentroSqlDao.
   private static final Logger logger = LoggerFactory.getLogger(EstudianteSqlDao.class.getPackageName());

   // Implementación...
}

Una vez que disponemos de un objeto Logger, podemos registrar los mensajes con métodos que reproducen el nivel de gravedad:

logger.error("Este es un mensaje fatal");
logger.warn("Esta es una advertencia que puede ser importante conocer");
logger.info("Este mensaje informa de que la aplicación ha hecho algo")
logger.debug("Este mensaje sirve para depurar el comportamiento de la aplicación")
logger.trace("Este mensaje permite seguir muy concienzudamente la ejecución de la aplicación");

Ha de tenerse en cuenta que, dependiendo del nivel que se haya definido como registrable, los mensajes se registrarán de modo efectivo o no lo harán. Es importante tenerlo presente porque muy habitualmente los mensajes no son meras frases como las de arriba, sino que incluyen valores:

logger.debug("Se ha registrado el centro con código {} y nombre {}", centro.getId(), centro.getNombre());

Como se ve, se incluye una sintaxis para poder incluir valores dentro del mensaje sin recurrir a String.format.

En todos los casos anteriores, si se quiere proporcionar un error para que se registre, puede proporcionarse como argumento adicional al final:

// e es una Excepción.
logger.debug("Se ha registrado el centro con código {} y nombre {}", centro.getId(), centro.getNombre(), e);

Ahora bien, en el ejemplo dado obtener ambos valores es muy económico ya que simplemente invocamos dos getters, por lo que el hecho de que se evalúen esos dos parámetros, aunque luego no acabe por escribirse el mensaje, no penaliza demasiado el rendimiento. Ahora bien, si la obtención del valor es costosa, ¿cómo evitaríamos la merma improductiva de rendimiento? Para ello, a partir de la versión 2 de SLF4J, existe una API fluida que permite usar una expresión lambda como argumento:

// Se supone que lo que se guarda es la fecha de nacimiento y la edad es calculada.
logger.atWarn().log(() -> String.format(
   "Se evita el registro porque el estudiante ID=%d tiene %d años",
   estudiante.getId(),
   estudiante.getEdad()
));

Una variante a esto último que permite pasar los parámetros como argumento, en vez de recurrir a String.format es:

logger.atWarn()
      .addArgument(estudiante.getId())
      .addArgument(() -> estudiante.getEdad()) // Este es costoso.
      .setCause(e)   // Opcional, si quisiéramos pasar un error.
      .log("Se evita el registro porque el estudiante ID={} tiene {} años");

Con todo esto, ya sabemos sobradamente enviar mensajes al registro indicando qué nivel de gravedad tienen.

Nota

En versiones anteriores a la 2, la única forma de sortear la evaluación gratuita de expresiones costosas era usar condiciones:

if(logger.isWarnEnabled()) {
   logger.warn(
      "Se evita el registro porque el estudiante ID={} tiene {} años",
      estudiante.getId(),
      estudiante.getEdad()
   );
}

2.3. Configuración adicional#

Prudencia

SLF4J se limita a especificar el envío de mensajes, por lo que toda la configuración adicional que queramos hacer a ese envío depende de la librería de implementación. En nuestro caso, este apartado se centra e cómo configurar Logback.

Pese a que ya sabemos cómo enviar mensajes, no es aún suficiente. Es muy común que, además, necesitemos al menos:

  • Definir sobre qué soporte se registrarán los mensajes.

  • Definir a partir de qué nivel los mensajes se registrarán.

  • Definir el formato de los mensajes.

La configuracíón predeterminada es la siguiente:

  1. Los mensajes se escriben en la salida estándar (o sea, en System.out).

  2. El nivel registrable, esto es, el nivel a partir del cual los mensajes se registran en el soporte, es DEBUG.

  3. El formato de salida tiene este formato:

    %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
    

    Lo que generaría un mensaje como:

    14:23:45.678 [main] DEBUG edu.acceso.test.MiClase - Mensaje de depuración.
    

Por otro lado, estas configuraciones pueden ser estáticas o dinámicas. Las primeras consisten en leer un archivo, mientras que las segundas permiten definir la configuración dentro del código en tiempo de ejecución.

2.3.1. Estática#

La configuración estática se realiza nativamente a través de un archivo XML o Groovy. Debe colocarse en el directorio resources (en un proyecto Maven o Gradle) y denominarse logback.xml.

Un ejemplo sencillo de configuración podría ser este:

logback.xml#
<configuration debug="true">
   <!-- Definición de un soporte -->
   <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
      <target>System.err</target>
      <encoder>
         <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
      </encoder>
   </appender>

   <!-- El logger raíz (y todos sus descendiente) tienen esta configuración -->
   <root level="INFO">
      <appender-ref ref="CONSOLE"/>
   </root>
</configuration>

Truco

El atributo debug provoca que se vuelquen en la consola mensajes relacionados con el procesamiento inicial del archivo de configuración. Para una depuración más extensa puede usarse en cambio:

<configuration>
   <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />

   <!-- Resto de configuración -->
</configuration>

Puede consultarse ch.qos.logback.core.status para ver otras posibles salidas distintas a la consola.

En caso de querer definir archivos como soporte de salida, podemos hacer:

Ejemplo de registros en archivo#
<!-- Appender para archivo simple -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>logs/app.log</file>
    <append>true</append> <!-- Conserva logs anteriores -->
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

<!-- Appender para rotación automática -->
<appender name="ROLLING_FILE" class="ch.qos.logback.core.RollingFileAppender">
    <file>logs/app.log</file>
    <!-- Este conserva registros anteriores sin necesidad de append: true -->
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <!-- Formato de archivos rotados: fecha + índice -->
        <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <!-- Máximo 10MB por archivo -->
        <maxFileSize>10MB</maxFileSize>
        <!-- Conserva 30 días de logs -->
        <maxHistory>30</maxHistory>
        <!-- Tamaño máximo total de todos los archivos -->
        <totalSizeCap>1GB</totalSizeCap>
    </rollingPolicy>
</appender>

Debe tenerse presente que las rutas son relativas al directorio de trabajo. Las rutas podrían ser absolutas, pero en ese caso la configuración dependería de cuál fuera el sistema en el que corre la operación. Para evitarlo tenemos alternativas:

  • Pasar variables a Java al ejecutar la aplicación usando la opción -D:

    <file>${LOG_DIR}/app.log</file>
    

    En este caso, si hubiéramos ejecutado así la aplicación:

    $ java -DLOG_DIR=/var/log -jar app.jar
    

    La ruta del archivo sería /var/log/app.log. Ahora bien corremos el riesgo de que la aplicación se ejecute sin pasar ningún valor a la variable. Para evitarlo podemos definir un valor predeterminado dentro del archivo de configuración:

    <configuration>
        <property name="LOG_DIR" value="logs">
    
        <!-- Resto de configuración -->
    
    </configuration>
    
  • Variables de ambiente (que dependen del sistema, por cierto):

    <file>${env.HOME}/app.log</file>
    

    Para las variables de ambiente, sin embargo, no es tan fácil fijar un valor predeterminado, porque no se les puede dar valor con <property> como en el caso anterior.

  • Valores de propiedades del sistema de Java:

    <!-- En un sistema UNIX esto equivale a /tmp/app.log -->
    <file>${java.io.tmpdir}/app.log</file>
    

Truco

En un sistema UNIX también existe la posibilidad de integrar los mensajes en los registros del sistema:

Ejemplo de registros con syslog#
<!-- Appender para Syslog clásico -->
<appender name="SYSLOG" class="ch.qos.logback.classic.net.SyslogAppender">
    <syslogHost>localhost</syslogHost>
    <port>514</port>
    <facility>LOCAL0</facility>
    <suffixPattern>[%thread] %logger{36} - %msg</suffixPattern>
    <stackTracePattern>   %ex{full}</stackTracePattern>
    <tag>MiApp</tag>
</appender>

<!-- Appender para Journald con syslog -->
<appender name="JOURNAL_SYSLOG" class="ch.qos.logback.classic.net.SyslogAppender">
    <syslogHost>/run/systemd/journal/syslog</syslogHost>
    <port>-1</port>  <!-- Usa -1 para sockets Unix -->
    <facility>LOCAL0</facility>
    <suffixPattern>[%thread] %logger{36} - %msg</suffixPattern>
    <stackTracePattern>   %ex{full}</stackTracePattern>
    <tag>MiApp</tag>
</appender>

Por supuesto, si hemos definido múltiples soportes, podemos indicar que se registros los mensajes en varios de ellos:

<root level="INFO">
   <appender-ref ref="CONSOLE">
   <appender-ref ref="FILE">
</root>

Queda un último aspecto básico por revisar y es cómo configurar un logger específico, ya que recordemos que, cuando hemos definido los logger en las clases los referíamos con la propia clase. Supongamos que en aquel ejemplo la clase CentroSqlDao está dentro del paquete edu.acceso.test.backend.sql. En ese caso:

<!-- appenders, etc. -->

<root level="INFO">
    <appender-ref ref="CONSOLE" />
</root>

<!-- Loggers específicos -->
<logger name="edu.acceso.test.backend.sql" level="DEBUG" additivity="false">
    <appender-ref ref="FILE" />
</logger>

<logger name="edu.acceso.test.backend.sql.sqlite" />

<logger name="jakarta.persistence" level="WARN" />

En esta configuración, los logs de la aplicación se han aplicado para que se registren los mensajes a partir del nivel INFO en la consola. Pero, además, se han hecho tres configuraciones adicionales. Tengase presente que, en principio, los valores de level y appender-ref se heredan de padres a hijos. En el caso de level un redefinición, cambia el valor; pero en el caso de appender-ref, una redefinición tiene carácter acomulativo. Por tanto:

  • El código incluido dentro de una librería externa (jakarta.persistence) sólo registrará mensajes de advertencia y de error (redefinición de level); en la consola (no se ha redefinido appender-ref).

  • El código incluido en el paquete edu.acceso.test.backend.sql (dentro del cual está la clase CentroSqlDao) registra mensajes a partir del nivel DEBUG (redefinición de level) y en principio debería registrarlos tanto en la consola como en el archivo. Pero se ha añadido additivity con valor false. Esta propiedad no es heredable y afecta al valor de appender-ref. Implica que el logger sólo usa los soportes que tenga definidos. Por tanto, los registros se escriben exclusivamente en el archivo.

  • El paquete edu.acceso.test.backend.sql.sqlite hereda el valor level del padre; y por tanto, registra mensajes a partir de DEBUG. En cuanto a los soportes, como no tiene definido additivity su valor es true y hereda: registrará los mensajes en consola, porque hereda de root. Sin embargo, no registrará en el archivo, porque el padre tenía su additivity a false y eso implica que no comparte los soportes con los hijos.

2.3.2. Dinámica#

La configuración estática que acabamos de ver permite configurar el sistema de registros a priori. Sin embargo, es posible que deseemos realizar alguna configuración adicional en tiempo de ejecución debido, por ejemplo, a que permitamos incluir un argumento que indique el nivel a partir del cuál queremos registrar mensajes u otro que defina su soporte (p.e. cuál es la ruta del archivo).

Para lograrlo necesitaremos establecer el nivel registrable o los soportes de salida para los distintos loggers, pero en vez de en un archivo de configuración, a través de instrucción de Java. Comencemos por aprender cómo se establece el nivel a partir del cual se registran mensajes.

Definición del nivel

// ¡Ojo! Los de Logback, no los de SLF4J
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.Level;

// ...

// Raíz.
Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.WARN);

De igual modo, se puede redefinir el nivel registrable para cualquier otro logger particular.

Definición de soportes de salida.

La definición de soportes de salida es más trabajosa, pero sigue la misma filosofía: debemos definir las características del appender (para lo cual nos es muy útil conocer cómo se configura estáticamente) y añadirlo al logger que decidamos. En los ejemplos usaremos root, pero puede ser cualquier otro particular.

ConsoleAppender

Para definir la salida por consola:

Configurar ConsoleAppender#
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

// Definición del formato del mensaje
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(context);
encoder.setPattern("%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n");
encoder.start();

// Creamos el soporte
ConsoleAppender console = new ConsoleAppender();
console.setContext(context);
console.setName("CONSOLE");
console.setEncoder(encoder);
console.setTarget("System.err");  // Por defecto es System.out
console.start();

// Añadimos el appender a un logger
root.addAppender(console);
FileAppender

La definición de un archivo de salida se hace así:

Configurar FileAppender#
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

// Definición del formato del mensaje
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(context);
encoder.setPattern("%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n");
encoder.start();

Path archivo = Path.of(System.getProperty("java.io.tmpfile"), "app.log");

// Creamos el soporte
FileAppender file = new FileAppender();
file.setContext(context);
file.setName("FILE");
file.setEncoder(encoder);
file.setAppend(true);
file.setFile(archivo.toString());
file.start();

// Añadimos el appender a un logger
root.addAppender(file);
RollingFileAppender

Si deseamos rotación de archivos:

Configurar RollingFileAppender#
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

// Definición del formato del mensaje
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(context);
encoder.setPattern("%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n");
encoder.start();

Path ruta = Path.of(System.getProperty("java.io.tmpfile"));

SizeAndTimeBasedRollingPolicy rollingPolicy = new SizeAndTimeBasedRollingPolicy();
rollingPolicy.setContext(context);
rollingPolicy.setFileNamePattern(ruta.toString() + "app.%d{yyyy-MM-dd}.%i.log");
rollingPolicy.setMaxFileSize("10MB");
rollingPolicy.setMaxHistory(30);
rollingPolicy.setTotalSizeCap("1GB");
rollingPolicy.start();


// Creamos el soporte
RollingFileAppender rollingFile = new RollingFileAppender();
rollingFile.setContext(context);
rollingFile.setName("ROLLING_FILE");
rollingFile.setEncoder(encoder);
rollingFile.setFile(ruta.toString() + "app.log");
rollingFile.setRollingPolicy(rollingPolicy);
rollingFile.start();

// Añadimos el appender a un logger
root.addAppender(rollingFile);

El nombre que se le da al appender (p.e. «CONSOLE»), nos permite rescatarlo en otra parte del código siempre que se haya adjuntado a algún logger. Por ejemplo:

Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
ConsoleAppender<?> console = (ConsoleAppender<?>) root.getAppender("CONSOLE");

// Lo manipulamos, si es nuestra intención
console.setTarget("System.out");

// También se puede eliminar de un logger.

Logger logger = (Logger) LoggerFactory.getLogger("edu.acceso.test.backend.sql");
logger.detachAppender("FILE");

Nota

Por supuesto, si CONSOLE se definió en el archivo de configuración y se usó en algún logger, se podrá obtener en el código sin tener que definirlo.

Otro aspecto importante es el aportado por additivity que ya se discutió anteriormente. Programáticamente, también se puede, definir para un logger particular:

Logger logger = (Logger) LoggerFactory.getLogger("edu.acceso.test.backend.sql");
logger.setAdditivity(false);

2.4. Particularidades#

Con lo tratado hasta aquí, podemos hacer una configuración sólida del sistema de registros de nuestra aplicación. Hay, sin embargo, algunas particularidades que en algún momento nos pueden resultar interesantes.

2.4.1. Filtrado por severidad#

En ocasiones nos interesa escoger el soporte en que se registrarán los mensajes dependiendo de su gravedad. Por ejemplo, separar en dos archivos los registros: un archivo para mensajes de error (ERROR y WARN) y otro para el resto. Esto se logra definiendo dos appenders distintos y definiendo filtros en cada uno de ellos. Por ejemplo:

Soporte según la severidad el mensaje#
<configuration>
    <!-- Appender para errores (WARN y superiores en System.err) -->
    <appender name="CONSOLE_ERROR" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.err</target>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
        <encoder>
            <pattern>%d{HH:mm:ss} [%thread] %highlight(%-5level) - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- Appender para INFO y DEBUG (en System.out) -->
    <appender name="CONSOLE_INFO" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
            <!-- Configuración inversa: DENY en coincidencia (INFO y superiores), ACCEPT en no coincidencia (DEBUG) -->
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
        <encoder>
            <pattern>%d{HH:mm:ss} [%thread] %highlight(%-5level) - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- Logger raíz -->
    <root level="DEBUG">
        <appender-ref ref="CONSOLE_ERROR" />
        <appender-ref ref="CONSOLE_INFO" />
    </root>
</configuration>

Si deseamos hacer una consulta dinámica, es necesario conocer cómo definir el filtro y añadirlo al appender:

ThresholdFilter filter = new Thresholdfilter();
filter.setContext(context);
filter.setLevel(Level.INFO);
filter.setOnMatch(FilterReply.DENY);
filter.setOnMismatch(FilterReply.ACCEPT);
filter.start();

// Añadir el filtro al appender
console.setFilter(filter);

2.4.2. Puenteo de librerías#

Un problema muy recurrente con el que nos podemos encontrar se da cuando una librería de terceros que queremos integrar en nuestra aplicación, usa una implementación distinta (p.e. Log4J o JUL) a la que hemos escogido nosotros (SLF4J + Logback). Para evitar el registro en distintos sistemas, existen librerías que puentean entre la implementación que escogió la librería y SLF4J.

Framework

Librería de puenteo

JUL

jul-to-slf4j

Log4J v1

log4j-over-slf4j

Log4J v2

log4j-to-slf4j

JLC

jcl-over-slf4j

El modo de actuación en este caso sería:

  1. Incluir entre las dependencias mi sistema de registro y la librería de puenteo. En Maven sería:

    <!-- API de SLF4J -->
    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
       <version>2.1.0-alpha1</version>
    </dependency>
    
    <!-- Framework: Logback -->
    <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
       <version>1.5.18</version>
    </dependency>
    
    <!-- Librería de puenteo  -->
    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>jul-to-slf4j</artifactId>
       <version>2.1.0-alpha1</version>
    </dependency>
    
  2. Al incluir como dependencia la librería que usa, a su vez, la librería de implementación que quiero evitar (JUL en nuestro caso), debe decírsele al gestor de proyectos (Maven en nuestro ejemplo), que evite instalar tal librería de implementación:

    <!-- Librería de terceros que quiero usar -->
    <dependency>
       <groupId>com.libreria.util</groupId>
       <artifactId>que-usa-jul</artifactId>
       <version>0.2.5</version>
       <exclusions>
          <exclusion>
             <groupId>log4j</groupId>
             <artifactId>log4j</artifactId>
          </exclusion>
       </exclusions>
    </dependency>
    
  3. Con lo anterior ya bastaría para engañar a la librería de terceros, excepto si la librería de terceros usa JUL, porque JUL está integrado en JDK. Si lo que queremos evitar es registrar con JUL, hay que añadir un código adicional:

    import org.slf4j.bridge.SLF4JBridgeHandler;
    
    public class Main {
       static {
          // Remueve los handlers de JUL
          SLF4JBridgeHandler.removeHandlersForRootLogger();
          // Añade el bridge de SLF4J
          SLF4JBridgeHandler.install();
       }
    }
    

Notas al pie