Logs en Java con Java.util.logging

Estándar

cap_hash

Cuando se esta preparando un programa para un entorno de producción tener un log donde se reporten los eventos y errores puede ser la diferencia entre pasar una semana tratando de replicar un error o solo leer un archivo y saber en que linea ocurrió el error y si bien hay todo un mundo de librerias y frameworks para este propósito no hay que olvidar que el propio Java ya contiene las clases para hacer esto y que nunca esta de mas ahorrarse dependencias.

Java Logging Api

La forma en que opera el framework de logging de Java es la siguiente:

  1. Creamos un objeto estático de tipo Logger desde el cual enviaremos los mensajes a registrar en la bitácora
  2. Creamos un objeto ConsoleHandler y se lo agregamos al Logger, de modo que los mensajes aparezcan automáticamente en la consola
  3. Creamos un objeto FileHandler y se lo agregamos al Logger, este Handler en particular enviara los mensajes al archivo que le indiquemos.
  4. Creamos un objeto SimpleFormatter y lo establecemos en el FileHandler, de este modo los logs se escriban como texto plano simple, de no indicarlo se escribiran en formato XML por defecto
  5. Para registrar algo en bitácora llamamos al método log del Logger indicamos el nivel del log y el mensaje que deseamos registrar, esto automáticamente reportara la fecha, hora, el nombre completo de la clase y el método y en el caso de mensajes de nivel grave la linea de código donde se genero el reporte

Como muchas cosas del Java esto suena muy feo a sobre ingeniera, pero hay que reconocer que en cada paso del proceso puede modificar y extender el comportamiento del sistema para que se ajuste a sus necesidades.

Niveles de log

Como se menciono en el paso 5 al momento de registrar un evento en bitácora necesitamos indicar el “nivel” de ese evento, estos nos permiten diferenciar entre meras notificaciones de que todo esta bien a errores de diferente severidad esto no solo se reflejara en el el archivo sino que también podremos filtrar a partir de que nivel de error queremos que se registre de modo que no se llene el archivo de meras notificaciones, este filtrado se indica en el objeto Handler.

Jerarquía del los Logger

Una de las grandes preguntas que tenia al investigar el uso del framework de logging era ¿Como envío los logs de varias clases al mismo archivo?, esto puede sonar trivial, simplemente cada clase necesita su propio FileHandler apuntando al mismo archivo ¿no?, pero esto, aparte de hacer que reusar una clase se haga difícil, solo le generara un montón de archivos .lck de los intentos de acceder al mismo archivo, la forma correcta es aprovechar la jerarquía propia de los paquetes de clases en java.

La idea de esto es la siguiente, creamos un Logger para el paquete principal de la aplicación y un Logger extra para cada subpaquete que exista escribiendo el nombre completo del paquete (esto es la ruta completa del mismo, javax.swing por ejemplo) en los métodos Logger.getLogger con los que se crea cada Logger, hecho esto al Logger del paquete principal le agregamos el Handler y Formatter que deseemos.

Lo siguiente es crear el objecto Logger adecuado en cada clase y registrar los eventos que deseemos, al correr el programa veremos que los logs de todo el programa están en el archivo del FileHandler del Logger del paquete principal.

Esto se debe a que los logs se propagan hacia arriba por toda la jerarquía de paquetes del programa hasta hallar un Handler que los maneje, como el único handler en todo el programa es el FileHandler del paquete principal (el cual esta en la cima de la jerarquía), pues todos los logs acabaran en ese archivo.

La ventaja de organizar los logs de este modo es que si deseamos cambiar el archivo o formato solo hay que modificar una parte del programa en lugar de cada clase del programa y si deseamos reusar una clase solo falta ajustar levemente la ruta en la declaración del Logger.

Imprimiendo el StackTrace en el Log.

Algo que puede serle muy útil para lidiar con los errores catastróficos capaces de tumbar el programa completo son los stackTrace, estos son están contenidos en los objetos Exception y son básicamente descripciones muy detalladas del estado del programa al momento en que ocurriera el error

Escribir los stackTrace en un log tiene truco, esto debido que los objetos Exception no tienen un método .getStackTrace() que le regrese un String, sino un método .printStackTrace() el cual envia la información directo a la salida, pero existe un método para redireccionar esa salida, el cual se detallara en el ejemplo.

Ejemplo.

Todo esto puede sonar demasiado complicado por lo cual se presentara un ejemplo que demuestre todo lo dicho, un log general para un programa compuesto de muchas clases organizadas en varios paquetes como se ve en la figura 1.

Figura 01 - Proyecto

Figura 01 – Proyecto

Para comenzar cree un proyecto nuevo en NetBeans y cree los paquetes bitacora, bitacora.subnivel y bitacora.subnivel.under, como se ve en la figura.

El código de cada archivo se presenta a continuación

Control.java

package bitacora.subnivel;

import java.util.logging.Level;
import java.util.logging.Logger;

public class Control {
    private final static Logger LOGGER = Logger.getLogger(“bitacora.subnivel.Control”);
    
    public void controlar(){
        LOGGER.log(Level.INFO, “Proceso exitoso”);
    }

}

Utilidades.java

package bitacora.subnivel;

import java.util.logging.Level;
import java.util.logging.Logger;

public class Utilidades {
    private final static Logger LOGGER = Logger.getLogger(“bitacora.subnivel.Utilidades”);

    public void funcionDudosa(){
        LOGGER.log(Level.SEVERE, “ERROR MASIVO”);
    }
}

InternalSys.java

package bitacora.subnivel.under;

import java.util.logging.Level;
import java.util.logging.Logger;

public class InternalSys {
    private final static Logger LOGGER = Logger.getLogger(“bitacora.subnivel.under.InternalSys”);

    public void llamadaSistema(){
        LOGGER.log(Level.WARNING, “Ocurrio un error de acceso en 0xFF”);
    }
}

Bitacora.java

package bitacora;

import bitacora.subnivel.Control;
import bitacora.subnivel.Utilidades;
import bitacora.subnivel.under.InternalSys;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

/**
*
* @author David
*/
public class Bitacora {

    // Preparamos el log para cada paquete del proyecto, esto con el fin de capturar cada log
// que se genere e irlo pasando al nivel superior hasta que encuentren un handler que los
    // maneje

    private final static Logger LOG_RAIZ = Logger.getLogger(“bitacora”);
    private final static Logger LOG_SUBNIVEL = Logger.getLogger(“bitacora.subnivel”);
    private final static Logger LOG_UNDER = Logger.getLogger(“bitacora.subnivel.under”);

    // El log para ESTA clase en particular
    private final static Logger LOGGER = Logger.getLogger(“bitacora.Bitacora”);
    public static void main(String[] args) {
        try {
            // Los handler (manejadores) indican a donde mandar la salida ya sea consola o archivo
// En este caso ConsoleHandler envia los logs a la consola

            Handler consoleHandler = new ConsoleHandler();
            // Con el manejador de archivo, indicamos el archivo donde se mandaran los logs
            // El segundo argumento controla si se sobre escribe el archivo o se agregan los logs al final
            // Para sobre escribir pase un true para agregar al final, false para sobre escribir
            // todo el archivo
            Handler fileHandler = new FileHandler(“./bitacora.log”, false);

            // El formateador indica como presentar los datos, en este caso usaremos el formaro sencillo
            // el cual es mas facil de leer, si no usamos esto el log estara en formato xml por defecto

            SimpleFormatter simpleFormatter = new SimpleFormatter();

            // Se especifica que formateador usara el manejador (handler) de archivo
            fileHandler.setFormatter(simpleFormatter);

            // Asignamos los handles previamente declarados al log *raiz* esto es muy importante ya que
// permitira que los logs de todas y cada una de las clases del programa que esten en ese paquete

            // o sus subpaquetes se almacenen en el archivo y aparescan en consola
            LOG_RAIZ.addHandler(consoleHandler);
            LOG_RAIZ.addHandler(fileHandler);

            // Indicamos a partir de que nivel deseamos mostrar los logs, podemos especificar un nivel en especifico
// para ignorar informacion que no necesitemos

            consoleHandler.setLevel(Level.ALL);
            fileHandler.setLevel(Level.ALL);

            LOGGER.log(Level.INFO, “Bitacora inicializada”);

            // Creamos los objetos de las otras clases
            Utilidades util = new Utilidades();
            Control control = new Control();
            InternalSys internalSys = new InternalSys();

            // Estas llamadas se registraran en el log
            LOGGER.log(Level.INFO, “Llamadas a los componentes del sistema”);

            util.funcionDudosa();
            control.controlar();
            internalSys.llamadaSistema();

            LOGGER.log(Level.INFO, “Probando manejo de excepciones”);

            try{
                     throw new Exception(“ERROR DE CONTROL DE FLUJO DE PROGRAMA”);
            }catch(Exception e){
            // Mediante el metodo getStack obtenemos el stackTrace de la excepcion en forma de un objecto String

            // de modo que podamos almacenarlo en bitacora para su analisis posterior
            LOGGER.log(Level.SEVERE, Bitacora.getStackTrace(e) );
    }
} catch (IOException ex) {
            LOGGER.log(Level.SEVERE, “Error de IO”);
} catch (SecurityException ex) {
            LOGGER.log(Level.SEVERE, “Error de Seguridad”);
}
}

/**
* Esta funcion nos permite convertir el stackTrace en un String, necesario para poder imprimirlos al log debido a
* cambios en como Java los maneja internamente
* @param e Excepcion de la que queremos el StackTrace
* @return StackTrace de la excepcion en forma de String
*/
public static String getStackTrace(Exception e) {
    StringWriter sWriter = new StringWriter();
    PrintWriter pWriter = new PrintWriter(sWriter);
    e.printStackTrace(pWriter);
    return sWriter.toString();
}
}

Creo que esta es la entrada mas larga hasta la fecha, si algo por que el ejemplo es mas elaborado de lo usual, las clases Control, Utilidades e InternalSys son bastante simples, solo crean un Logger y generan un log en la unica funcion que tienen.

Por su parte la clase Bitacora contiene el código mas relevante, como es usual en este blog las lineas mas importantes aparecen en rojo y los comentarios en verde detallan que ocurre en dichas lineas.

Ya que ejecute el programa vera en la consola una salida similar a la mostrara en la Figura 2

Figura 02 - Logs en Consola

Figura 02 – Logs en Consola

Y llendo a la pestaña de archivos podra ver un archivo “bitacora.log” el cual al abrirlo le mostrara los logs del sistema a detalle, como se ve en la Figura 3

Figura 03 - Archivo de bitacora

Figura 03 – Archivo de bitacora

Como un ultimo detalle comente la linea fileHandler.setFormatter(simpleFormatter); y ejecute el programa de nuevo, notara que el archivo bitacora.log a cambiado y el log ahora se encuetra en formato xml, lo cual puede ser util ya que este formato puede ser procesado y analizado por un programa de computadora con facilidad.

Figura 04 - log XML

Figura 04 – log XML

Espero que esta entrada fuera de utilidad y mas comprensible que otros tutoriales que he visto en linea, si hay preguntas con gusto las respondere en los comentarios y nos vemos en la proxima entrada.

Anuncios