Generando logs hacia archivos en Joomla 1.5 con JLog

Joomla provee una clase utilitaria para loguear  información hacia archivos llamada JLog. Esta clase provee las funcionalidades para crear archivos en common log format para virtualmente cualquier propósito como:

  • Almacenar información de depuración
  • Loguear llamadas AJAX para ayudarnos a encontrar problemas
  • Loguear las cosas que un visitante de un sitio web esté haciendo

La clase JLog se encuentra en /libraries/error/log.php y se puede incluir con el siguiente código:

jimport('joomla.error.log');

Si bien loguear errores es ciertamente una práctica muy común (de hecho la propia clase se encuentra como un sub-paquete del paquete error), no se limita a loguear información de errores solamente.

Usando JLog directamente

Generalmente NO se creará una nueva instancia de JLog. En su lugar se deberá usar el método getInstance para obtener una instancia del log para un archivo y opciones específicas de un log dado.

jimport('joomla.error.log');
$log = &JLog::getInstance();

Por defecto esto creará un archivo de log llamado error.php en el directorio temporal de Joomla (referido en la variable $tmp_path en el archivo configuration.php). El archivo tiene una extensión .php como medida de seguridad para prevenir accesos directos a los contenidos del mismo. Para incrementar la privacidad posteriormente, se podría considerar el cambio del directorio temporal de Joomla a una localización externa al raíz del web.

Cambiando el nombre del archivo de log

En la práctica, es recomendable crear nuestros propios archivos de log incluyendo espacios de nombres (namespaces) como por ejemplo:

jimport('joomla.error.log');
$log = &JLog::getInstance('com_ajax.log.php');

Por defecto esto ofrece un archivo de log básico con las siguientes columnas:

  • date
  • time
  • level
  • c-ip (la dirección IP del usuario)
  • status
  • comment

Las columnas date, time y c-ip son populadas automáticamente si es que no se proveen. Las columnas level, status y comment pueden ser usadas a discreción del programador. La columna level es generalmente orientada a alguna clase de nivel como warning o error. La columna status generalmente indicará algún estado como ok ó fail (probablemente un valor de 0 ó 1 o incluso algo como un código de estado HTTP). Se puede dejar en blanco también. Al nivel más simple se proporcionará únicamente la columna comment y se dejará manejar el resto a los valores por defecto de la clase, como se muestra en el uso del método addEntry:

jimport('joomla.error.log');
$log = &JLog::getInstance('com_ajax.log.php');

// Just adding a comment
$log->addEntry(array('comment' => 'Este es mi mensaje');

// Adding the comment and the status code
$log->addEntry(array('comment' => 'Un extraño error ha ocurrido', 'status' => 500));

Se puede ver que se le pasó un vector asociativo a addEntry, donde cada elemento del array apunta a una columna en el archivo de log. Si no se provee una columna esperada, el log simplemente agrega un guión. Si se proveen más columnas de las que la clase conoce, estas simplemente serán ignoradas.

Cambiando el formato del archivo de log

Se puede fácilmente cambiar el formato del archivo de log (las columnas que están disponibles) para ajustarse a la información que se desea loguear. Esto se puede hacer de dos maneras. La primera es pasarle el formato en un vector de opciones.

jimport('joomla.error.log');
$options = array(
  'format' => "{DATE}\t{TIME}\t{USER_ID}\t{COMMENT}";
);

$log = &JLog::getInstance('com_ajax.log.php', $options);
$user = &JFactory::getUser();
$userId = $user->get('id');
$log->addEntry(array('user_id' => $userId, 'comment' => 'Este es mi mensaje'));

Entonces, lo que se hizo aquí es crear un vector asociativo en la variable $options. El vector tiene un elemento format.  El formato es simplemente una cadena de los nombres de columna deseados en mayúsculas, cada uno encerrado entre llaves y separados por un caracter de tabulación. Nótese que esta cadena está encerrada entre comillas (no apóstrofes) para que \t sea reconocido como un tabulador (no como un backslash con la letra t).

La segunda forma de personalizar el formato del log es usar el método setOptions así:

jimport('joomla.error.log');
$options = array(
    'format' => "{DATE}\t{TIME}\t{USER_ID}\t{COMMENT}";
);

$log = &JLog::getInstance('com_ajax.log.php');
$log->setOptions($options);
$user = &JFactory::getUser();
$userId = $user->get('id');
$log->addEntry(array('user_id' => $userId, 'comment' => 'Este es mi mensaje'));

Cualquiera de los dos métodos es aceptable. Si se está usando esto en una extensión, sólo se debe asignar el formato una vez por archivo de log. En un componente por ejemplo, se puede inicializar el objeto en el archivo de entrada del componente (dispatcher), pero no usarlo realmente, como se sugiere en el ejemplo anterior. Entonces, luego, dentro de la ejecución del componente, se podrá llamar a una instancia del objeto de logs, así:

// Anywhere, deep inside the component
$log = &JLog::getInstance('com_notes.log.php');
$log->addEntry(array('user_id' => 0, 'comment' => 'Algo que requiere de tu atención'));

Esto funciona siempre que se mantenga constante el nombre del archivo de log. Por tal motivo, se podría preferir la definición del nombre de archivo de log como una constante, así::

// This is the code in the dispatcher file (components/ajax.php)
// Define the file name as a PHP constant
define('AJAX_ERROR_LOG', 'com_ajax.log.php');

// Include the library dependancies
jimport('joomla.error.log');
$options = array(
    'format' => "{DATE}\t{TIME}\t{USER_ID}\t{COMMENT}";
);
// Create the instance of the log file in case we use it later
$log = &JLog::getInstance(AJAX_ERROR_LOG, $options);
....
....
// Then, deep in the heart of the component
$log = &JLog::getInstance(AJAX_ERROR_LOG);
$user = &JFactory::getUser();
$userId = $user->get('id');
$log->addEntry(array('user_id' => $userId, 'comment' => 'Este es mi mensaje'));

Definir el nombre de archivo como una constante significa que no se tendrá que recordar este nombre a través del código y si se necesitara cambiarlo, se puede hacer en un solo lugar.

Se puede, por supuesto, crear múltiples archivos de log para una sola extensión. No existe límite en el número de archivos que se pueden crear y se pueden usar diferentes logs para recolectar diversa información. Alternativamente, se puede loguear la misma información pero creando un log por cada día. Para logar esto, sólo deberemos incluir la fecha en el nombre del archivo, así:

// Define the file name as a PHP constant
define('AJAX_ERROR_LOG', 'com_notes.log.'.date('Y_m_d').'.php');

Esta es una buena técnica si se trata de ubicar errores intermitentes y que se reporten en una fecha específica. Nos ahorrará el trabajo de buscar dentro de archivos de log potencialmente muy largos. Se podrá descartar los logs luego de un número de días, de ser el caso.

Cambiar la ubicación de los archivos de log

El método JLog::getInstance toma un argumento más. Se puede opcionalmente indicar la ruta (path) donde queremos almacenar el archivo de log dentro de nuestro servidor. Si se usa este argumento para una extensión que se esté distribuyendo, entonces se debería setear como una opción de configuración de la misma, dado que es posible que la ruta (path) no exista en todos los servidores (sin mencionar las diferencias en las rutas entre sistemas operativos como Windows, Linux, etc).

Para especificar una ruta fija, se instancia el log así:

// Create the instance of the log file in case we use it later
$log = &JLog::getInstance(AJAX_ERROR_LOG, $options, '/tmp/ajax');

Creación de ayudantes (helpers) para JLog

Usar JLog directamente está bien, pero podría resultar algo tedioso. Para hacernos la vida un poco más fácil se puede crear una clase ayudante (helper) que haga todo el trabajo duro, configurando el log, pero ofreciendo una API muy simple para usarse dentro de la extensión. Una clase helper podría verse algo así:

/**
 * Helper class for logging
 * @package    Ajax
 * @subpackage com_ajax
 */
class AjaxHelperLog
{
    /**
     * Simple log
     * @param string $comment  The comment to log
     * @param int $userId      An optional user ID
     */
    function simpleLog($comment, $userId = 0)
    {
        // Include the library dependancies
        jimport('joomla.error.log');
        $options = array(
            'format' => "{DATE}\t{TIME}\t{USER_ID}\t{COMMENT}";
        );
        // Create the instance of the log file in case we use it later
        $log = &JLog::getInstance(AJAX_ERROR_LOG, $options);
        $log->addEntry(array('comment' => $comment, 'user_id' => $userId));
    }
}

Ahora, todo lo que se necesita hacer es cargar el archivo en el dispatcher y dentro de nuestro componente todo lo que deberíamos hacer es ejecutar una llamada estática al método simpleLog, así:

AjaxHelperLog::simpleLog('Esta es mi historia, esta es mi canción');

Adicionalmente se puede agregar el ID de usuario si fuese apropiado. La lección principal aquí es la de que se puede crear un método para servir a múltiples propósitos según lo que el programador requiera.