CSU Log
Esta librería pretende ser una sustitución a diversos otros mecanismos de log que se utilizan en las aplicaciones, generalmente basados en macros. La arquitectura de CSU Log permite generar variables de log al estilo de los streams de la librería estándar de C++.
Motivación
Esta librería pretende ser una herramienta de log que ofrezca al programador un mayor control que las herramientas o tecnologías de log basadas en macros. Al basarse en objetos del lenguaje, se pueden aplicar ciertas optimizaciones y comprobaciones en tiempo de compilación y ejecución que no son posibles cuando se usan macros.
En particular las motivaciones de la librería son:
- Ofrecer un interfaz común y moderno basado en los streams de C++.
- Un sólo fichero de cabecera.
- Permitir optimizar completamente el log en el caso de que no se quiera incluir en la aplicación final.
- Permitir que varios hilos usen un mismo log sin mezclar la salida dentro de una misma línea.
- Asímismo, evitar que varias líneas de diferentes logs se mezclen incompletas en la salida.
- Permitir que un log haga su salida simultáneamente a varios ficheros, tuberías, salida estándar o conexiones de red.
- Ofrecer al programador un control total sobre el formato de la salida, activación y desactivación de log, filtrado de eventos de log, selección de niveles de error, grupos, etc.
- Ofrecer varios esquemas de threading: sin hilos, hilos pthreads, hilos Boost, Qt, etc. Esta implementación debe ofrecer la mínima contención en cuanto a los hilos.
- Permitir al usuario personalizar los niveles de log.
- Ofrecer un mecanismo extensible de modificadores del log para añadir nueva funcionalidad.
Uso Básico
Los diferentes logs creados por la aplicación pueden ser locales o globales. La globalidad no afecta al log, que tiene un ciclo normal de creación y destrucción de cualquier objeto C++.
#include <csu/log/log.hpp> #include <iostream> using namespace csu::log; typedef logger<true> my_logger; int main(void) { my_logger out(std::cout); out << "hello" << std::endl; }
Seleccionando true ó false como parámetro del log hace que se produzca salida o no en el log. En particular, al seleccionar false, el compilador no generará código para ninguna operación del log. Por último, el uso de endl es necesario para separar las distintas líneas del log.
Un logger<true> también se puede deshabilitar usando los modificadores:
out << off << "Esto no aparece en la salida" << on << "Esto sí" << std::endl;
Existen varios modificadores predefinidos, y se pueden definir más por el usuario:
- on : habilita el log
- off : deshabilita el log
- timestamp : muestra una marca de tiempo
- line_num : muestra la línea actual
- thread_id : muestra el identificador del hilo actual
- w_level : muestra el nivel de log actual
- group_name : muestra el nombre del grupo actual
A través del interfaz del log, se pueden añadir nuevas salidas a un logger:
std::ofstream ofile("log.txt"); out.add_stream(ofile); out << "Esto también sale por log.txt" << std::endl; out.remove_stream(ofile); ofile.close();
El stream debe estar disponible durante toda la vida del log.
También se puede especificar la forma en la que el log realiza la salida, configurando cada línea para, por ejemplo, añadirle una marca de tiempo, el identificador del hilo de ejecución, etc.
Si no se establece ningún formato, el log simplemente producirá el texto proporcionado por el programador.
El establecimiento del formato se puede realizar bien a través de la operación set_log_format o bien el modificador log_format :
out.set_log_format ("%t, %p"); out << "hello" << std::endl; // Salida: "[Mon Mar 30 17:53:53 CEST 2009], hello" out << log_format ("%t::: %p"); out << "goodbye" << std::endl; // Salida: "[Mon Mar 30 17:53:53 CEST 2009]::: goodbye"
Los formatos disponibles, seguidos de un carácter '%' son los siguientes:
- i: Identificador del hilo
- p: payload (lo escrito por el programador en el log)
- t: marca de tiempo
- n: número de línea de salida.
- l: (level ) representación en cadena de caracteres del nivel de log (INFO, DEBUG, etc.)
- g: nombre del grupo
Niveles y Grupos
Los niveles permiten clasificar las distintas salidas hacia uno o varios logs . Se pueden agrupar y ordenar en una jerarquía en la que los niveles superiores incluyen a los inferiores .
// Declarar los niveles level lComm ( "lComm" );
Los niveles se ordenan por orden de declaración:
level less_level () ; level more_level () ; level even_more_level () ;
Para identificar la salida con un nivel:
out << lComm << "output in log 'out', level 'lComm'" << std::endl;
Y para hacer un filtro de niveles (las expresiones de filtrado se explican después):
// Show levels that are only less than lComm out << level_filter("(< _ lComm)"); // Same thing. With set_level_filter out.set_level_filter("(< _ lComm)");
Grupos
Permiten clasificar las distintas salidas hacia uno o varios logs . No se pueden agrupar u ordenar : un grupo es totalmente independiente , y se aplican hasta el std::endl.
Crear nuevos grupos:
group gError(“gError”);
Para Identificar la salida con un grupo:
out << gError << “Error output” << std::endl;
Aplicar filtros en código:
// Show levels that are only equal than gError out << group_filter("(= _ gError)"); // gError or gWarn. With set_group_filter out.set_group_filter("(or (= _ gError) (= _ gWarn))");
Filtros
Se utilizan para seleccionar qué entradas se muestran y cuáles no . Partiendo de una clasificación previa en grupos o niveles, se escriben expresiones basadas en LISP. Se pueden construir expresiones lógicas del tipo "(or (> _ lev1) (< _ lev2))" . Los “_” representan el nivel/grupo que se está comprobando (las comillas no se incluyen ).
En las expresiones, se pueden usar los distintos operadores: !, <, >, >=, <=, =, not, or y and. También cualquier combinación en filtros más complejos:
- (or (> _ lev1) (= _ lev5))
- (and (>= _ lev1) (<= _ lev3))
Modificadores
Los modificadores definidos en la librería de log pueden usarse para producir la salida de diferentes elementos en línea:
- timestamp
- thread_id
- line_num
- w_level_name
- group_name
Y su uso:
out << "this is a timestamp: " << timestamp << std::endl; out << "thread " << thread_id << " is causing troubles" << std::endl;
Hilos
La librería de log permite su uso en sistemas multihilo evitando que diferentes logs mezclen su salida. El “commit” de cada salida de cada log ocurre al recibir el log la entrada std::endl. Para indicar qué sistema de hilos se usará, se pueden definir un conjunto de #defines de C++ antes de incluir a la librería de log y que dictarán su comportamiento con hilos:
- #define CSU_LOG_THREADING_PTHREADS (se usará pthreads)
- #define CSU_LOG_THREADING_BOOST (se usará boost.threads)
- #define CSU_LOG_THREADING_QT (se usará Qt Threads)
- Si no se define nada, el sistema de log no asume que haya hilos
Configuración desde fichero
Las diferentes opciones de un log pueden configurarse también desde fichero :
- si está habilitado o no
- formato del log
- filtros de niveles o grupos
- streams de salida de un log
Para leer la configuración de un fichero se escribe lo siguiente:
// create a log_configuration object csu::log::log_configuration lc; // apply a configuration file lc.read_config_from_file("path/to/config/file.ext");
El fichero de configuración puede incluir la configuración de uno o varios logs, y se pueden aplicar varios ficheros al objeto log_configuration.
Las opciones de configuración se muestran en el siguiente fichero de configuración de ejemplo:
# settings for the log "log_name" log "log_name" { # log enabled or not enabled = true # initial log format log_format = "%t %p" # initial level filter configuration level_filter = "#t" # initial group filter configuration group_filter = "(not (= _ g1))" # streams associated with this log streams = { std::cout file("some_file.txt") } }
Referencias
[1] Final Committee Draft of "ISO/IEC IS 14882 - Programming Languages – C++".
[2] Boost Library. http://www.boost.org.
