Fichier LOG : journalisation de messages

Expression du besoin

Les applications ont souvent besoin de consigner des messages, évènements, avertissements, erreurs dans des fichiers. Ce mécanisme se nomme la journalisation (log).

La journalisation (log)

Le terme log est pour désigner un historique d’événements et par extension le fichier contenant cet historique.

Le concept d’historique des événements (logging) désigne l’enregistrement séquentiel dans un fichier ou une base de données de tous les événements affectant un processus particulier (application, activité d’un réseau informatique …). Le journal (log file ou plus simplement log), désigne alors le fichier contenant ces enregistrements. Généralement datés et classés par ordre chronologique, ces derniers permettent d’analyser pas à pas l’activité interne du processus et ses interactions avec son environnement. Ce sont le plus souvent de simples fichiers textes.

Syslog est un protocole définissant un service de journaux d’événements d’un système informatique développé dans les années 1980. C’est aussi le nom du format qui permet ces échanges. Syslog est depuis devenu la solution de journalisation standard sur les systèmes Unix et Linux, il y a également une variété d’implémentations syslog sur d’autres systèmes d’exploitation (Windows notamment) et est généralement trouvé dans les périphériques réseau tels que les commutateurs ou routeurs.

Les fichiers de log d’un système Unix/Linux sont généralement stockés dans le répertoire /var/log/. Ils sont habtituellement réservés au système d’exploitation, aux daemons (des services qui s’exécutent en tâche de fond) et aux processus serveurs.

Exemple de fichier de log :

$ tail /var/log/syslog
Nov 24 17:27:09 alias kernel: [941318] scsi 18:0:0:0: Direct-Access TOSHIBA USB 3.0
Nov 24 17:27:09 alias kernel: [941318] sd 18:0:0:0: Attached scsi generic sg12 type 0
Nov 24 17:27:09 alias kernel: [941318] sd 18:0:0:0: [sdk] (500 GB/465 GiB)
Nov 24 17:27:09 alias kernel: [941318] sd 18:0:0:0: [sdk] Write Protect is off
Nov 24 17:27:09 alias kernel: [941318] sd 18:0:0:0: [sdk] Mode Sense: 2b 00 00 00
Nov 24 17:27:09 alias kernel: [941318] sd 18:0:0:0: [sdk] Write cache: disabled, ...
Nov 24 17:27:09 alias kernel: [941318]  sdk: sdk1
Nov 24 17:27:09 alias kernel: [941318] sd 18:0:0:0: [sdk] Attached SCSI disk
Nov 24 17:27:09 alias ata_id[24491]: HDIO_GET_IDENTITY failed for '/dev/sdk'

La journalisation dans syslog (local ou distant) se fait via :

  • logger (commande Unix) pour les scripts shell
  • vsyslog() ou syslog() pour les programmes compilés

Voici un exemple de programme test-log.cpp qui journalise dans syslog :

#include <syslog.h>
#include <iostream>

using namespace std;

int main( int argc, char **argv )
{
    // Fixe les priorités qui seront journalisés
    // Les huit priorités sont LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, LOG_WARNING, 
    // LOG_NOTICE, LOG_INFO et LOG_DEBUG.
    // Certains systèmes fournissent aussi une macro LOG_UPTO(p) pour le masque 
    // de toutes les priorités jusqu'à p incluses.
    setlogmask (LOG_UPTO (LOG_NOTICE)); // ici seulement de LOG_EMERG à LOG_NOTICE

    // Quelques options :
    //LOG_PID        : inclure le PID dans chaque message.
    //LOG_LOCAL0 à 7 : réservé pour des utilisations locales.
    openlog("test-log", LOG_CONS | LOG_NDELAY, LOG_LOCAL1);

    //LOG_NOTICE     : Événement normal méritant d'être signalé
    syslog(LOG_NOTICE, "demarrage par utilisateur %d", getuid ());
    // ce qui donne dans /var/log/syslog
    // Nov 24 21:20:14 alias test-log: demarrage par utilisateur 1026
    
    //LOG_INFO       : Message d'information simple
    syslog(LOG_INFO, "le soleil brille ce matin");
    // non journalisé

    cout << "un programme comme un autre\n";

    closelog();    
    
    return 0;
}

Code source complet : test-mo-fichier-log.zip

API Qt

Qt ne fournit pas de classe capable de gérer directement la journalisation. On utilisera donc la classe QFile qui permet de manipuler des fichiers.

Objectifs

Être capable d’écrire dans un fichier journal.

Séquence 0 : le flux d’erreur en action

Pour bien comprendre la différence entre le flux normal de sortie (stdout) et le flux d’erreur (stderr), on va réaliser un programme d’exemple cerr.cpp :

#include <iostream>

using namespace std;

int main( int argc, char **argv )
{
    cout << "un message sur cout (stdout)\n";
    cerr << "un message sur cerr (stderr)\n";
    
    return 0;
}

Voici les commandes qui permettent de comprendre la séparation des flux :

// on fabrique l'exécutable (par défaut a.out)
$ g++ cerr.cpp

// on exécute
$ ./a.out 
un message sur cout (stdout)
un message sur cerr (stderr)

// Les deux messages s'affichent sur l'écran ... mais par des flux différents

// On vérifie en redirigeant le flux d'erreur
$ ./a.out 2> messages.log
un message sur cout (stdout)

// Seul le flux stdout s'affiche sur l'écran ... 
// et le flux d'erreur a été redirigé vers le fichier de log
$ cat messages.log 
un message sur cerr (stderr)

// On peut tester la situation inverse ... 
// on redirige maintenant le flux stdout
$ ./a.out 1> sorties.txt 
un message sur cerr (stderr)

// Seul le flux stderr s'affiche sur l'écran ... 
// et le flux de sortie a été redirigé vers le fichier de résultat
$ cat sorties.txt 
un message sur cout (stdout)

// Les flux sont bien séparés par le système

// Si on ne veut journaliser le flux d'erreur ni qu'il s'affiche sur l'écran,
// il faut alors l'éliminer
$ ./a.out 2> /dev/null

Code source complet : test-mo-fichier-log.zip

Séquence 1 : écrire dans un fichier journal

On va créer une application GUI qui permet de gérer une liste d’interfaces USB affichée dans une liste déroulante crée au démarrage. On pourra supprimer une interface ou ajouter une interface et la liste déroulante sera mise à jour. Chaque action sera consignée dans le fichier journal du même nom que l’exécutable avec l’extension .log.

Un exemple de fichier journal obtenu après une exécution :

24/11/2015 20:52 test-mo-fichier-log : MaFenetre::MaFenetre() démarrage
24/11/2015 20:52 test-mo-fichier-log : MaFenetre::selectionner() /dev/ttyUSB0
24/11/2015 20:52 test-mo-fichier-log : MaFenetre::creerListe() /dev/ttyUSB0
24/11/2015 20:52 test-mo-fichier-log : MaFenetre::creerListe() /dev/ttyUSB1
24/11/2015 20:52 test-mo-fichier-log : MaFenetre::creerListe() /dev/ttyUSB2
24/11/2015 20:52 test-mo-fichier-log : MaFenetre::creerListe() /dev/ttyUSB3
24/11/2015 20:52 test-mo-fichier-log : MaFenetre::selectionner() /dev/ttyUSB3
24/11/2015 20:52 test-mo-fichier-log : MaFenetre::selectionner() /dev/ttyUSB2
24/11/2015 20:52 test-mo-fichier-log : MaFenetre::~MaFenetre() arrêt

Pour gérer ce journal, on décide de créer une classe Log qui fournira une méthode journaliser() pour tous les objets qui en auront besoin. Cela pose un problème technique car les objets désirant journaliser risquent d’entrer en conflit sur le fichier physique (accès concurrent).

Il faudrait s’assurer que l’on ne puisse créer qu’une seule instance de la classe Log (puisqu’il n’y a qu’un seul fichier physique). En POO, ceci est réalisé facilement en appliquant le design pattern Singleton à notre classe.

Rappel : Un design pattern (ou motif de conception) est un document qui décrit une solution générale à un problème connu et récurrent. Lien : Patron de conception.

Le Singleton vise à assurer qu’il n’y a toujours qu’une seule instance d’un objet en fournissant une interface pour la manipuler. C’est un des patrons les plus simples. La classe fournit une méthode statique pour obtenir cette unique instance (getInstance()) et un mécanisme pour empêcher la création d’autres instances (en placant le constructeur en private).

class Log
{
public:
    static Log* getInstance();
    static void detruireInstance();
    
    void        journaliser(const QString &message);

private:
    static Log* _log;
    static int  nbAcces;
    QFile       *log;
    QMutex      mutex; // une protection contre le multitâche

    Log();
};
// Intialisation des membres statiques
Log* Log::_log = NULL;
int Log::nbAcces = 0;

// La méthode que tout le monde appelle pour obtenir l'instance unique
Log* Log::getInstance()
{
    // l'instance n'a pas été créée ?
    if(_log == NULL)
        _log = new Log(); // on la crée (une fois)

    nbAcces++;

    return _log; // on retourne l'instance
}

// Le constructeur
Log::Log()
{
    // Préparation du log
    QString fileLog = QCoreApplication::instance()->applicationDirPath() + "/" 
    + QCoreApplication::instance()->applicationName() + ".log";

    log = new QFile(fileLog, this);
    log->open(QIODevice::WriteOnly);
    if(!log->isOpen())
    {
        qDebug() << "<Log::Log()> impossible d'ouvrir le fichier de log !";
    }
}

// La méthode `journaliser()`
void Log::journaliser(const QString &message)
{
    QMutexLocker verrou(&mutex); // une protection contre le multitâche

    if(log->isOpen())
    {
        QTextStream monLog(log);
        
        // un horodatage du message
        QDateTime maintenant = QDateTime::currentDateTime();
        QString dateEtHeureActuelle = maintenant.toString("dd/MM/yyyy hh:mm");

        // un message de log personnalisé
        monLog << dateEtHeureActuelle << " " << qApp->applicationName() << " : " 
               << message << endl;
    }
}

Pour journaliser, il suffit maintenant de faire :

log = Log::getInstance();

QString message = "un message !";
log->journaliser(message);

Log::detruireInstance();    

Code source complet : test-mo-fichier-log.zip

Retour au sommaire