Gestionnaires de log


1Présentation

Un gestionnaire de log est un objet qui reçoit les messages de log. Cela permet de traiter les logs de manière plus avancée que simplement en les écrivant dans un fichier (tel que le fait le framework par défaut).

L'utilisation typique est d'envoyer les logs sur un serveur centralisé (Graylog, LogStash, Fluentd, rsyslog, syslog-ng, Nagios, ELK, Datadog…), en plus − ou à la place − de l'écriture dans le fichier log/temma.log.


2Configuration

Dans le fichier de configuration etc/temma.php, la directive logManager permet de définir un ou plusieurs objets qui seront appelés pour prendre en charge les logs générés par l'application.
Les objets peuvent être placés dans un namespace.

Voici un fragment de configuration, qui définit un gestionnaire de log :

<?php

return [
    'application' => [
        'logManager' => 'MonLogManager'
    ]
];

Il est aussi possible de fournir une liste d'objets :

<?php

return [
    'application' => [
        'logManager' => ['MonLogManager', '\Mon\Autre\Log\Manager']
    ]
];

Si vous souhaitez utiliser uniquement des gestionnaires de log, et désactiver l'écriture dans le fichier log/temma.log, il faut mettre la directive logFile à false, null ou lui affecter une chaîne vide :

<?php

return [
    'application' => [
        'logFile'    => false,
        'logManager' => 'MonLogManager'
    ]
];

3Développement simple

Un gestionnaire de log est un objet qui implémente l'interface \Temma\Web\LogManager. Une instance de l'objet est créée au démarrage du framework.

Le gestionnaire de log doit contenir une méthode log(), qui sera appelée à chaque nouveau message de log, en recevant quatre paramètres :

  1. Un identifiant de trace. C'est une chaîne alphanumérique de 4 caractères qui sert à identifier les logs générés par la même requête.
  2. Le texte du message de log.
  3. La priorité du message (DEBUG, NOTE, INFO, …). Peut être nul.
  4. La "classe de log" du message. Peut être nul.

Exemple :

/** Gestionnaire de log qui écrit les messages dans un fichier. */
class FileLogManager implements \Temma\Web\LogManager {
    /**
     * Écrit les logs applicatifs dans un fichier.
     * @param  string   $traceId  Identifiant de trace.
     * @param  string   $text     Texte du message.
     * @param  ?string  $prio     Priorité du message.
     * @param  ?string  $class    Classe de log du message.
     */
    public function log(string $traceId, string $text, ?string $prio, ?string $class) : void {
        $path = "/var/log/temma/$traceId.log";
        $message = "[$priority] ($class) $text";
        file_put_contents($path, $message, FILE_APPEND);
    }
}

Dans cet exemple, le gestionnaire de log va créer un nouveau fichier pour chaque requête traitée par le framework.

  • Ligne 2 : Définition de l'objet.
  • Ligne 10 : Définition de la méthode log(), qui sera appelée à chaque écriture de log.
  • Ligne 11 : Génération du chemin vers le fichier à créer.
  • Ligne 12 : Génération du message qui sera écrit dans le fichier.
  • Ligne 13 : Écriture du message dans le fichier. Si le fichier n'existait pas, il est créé ; sinon, le message est ajouté à la fin du fichier.

4Injection de dépendances

Veuillez consulter la documentation de l'injection de dépendances pour comprendre son intérêt et son utilisation dans Temma.

Pour qu'un gestionnaire de log ait accès au composant d'injection de dépendances, il faut qu'il implémente l'interface \Temma\Base\Loadable (en plus de l'interface \Temma\Web\LogManager). Il devra alors implémenter un constructeur qui prend en paramètre un objet \Temma\Base\Loader, à travers lequel il pourra accéder à d'autres données et objets.
Le composant offre notamment l'accès aux données de configuration du framework.

Exemple :

/**
 * Gestionnaire de log qui envoie les logs à un service externe.
 * Une configuration étendue "x-external-log" doit contenir une clé "url",
 * contenant l'URL vers laquelle envoyer les messages de log.
 */
class ExternalLogManager implements \Temma\Base\Loader, \Temma\Web\LogManager {
    /** URL de connexion externe. */
    private ?string $_url;

    /** Constructeur. */
    public function __construct(\Temma\Base\Loader $loader) {
        // récupération de l'URL vers laquelle les logs vont être envoyés
        $this->_url = $loader->config->xtra('external-log', 'url');
    }
    /** Envoi les logs applicatifs vers le service externe. */
    public function log(string $traceId, string $text, ?string $prio, ?string $class) : void {
        // vérification des paramètres de connexion
        if (!$this->_url)
            return;
        // création de la requête
        $ctx = stream_context_create([
            'http' => [
                'method'  => 'POST',
                'header'  => 'Content-Type: application/x-www-form-urlencoded',
                'content' => http_build_query([
                    'message'  => "($traceId) [$priority] ($class) $text",
                    'hostname' => gethostname(),
                ]),
            ],
        ]);
        // envoi vers le service externe
        file_get_contents($this->_url, false, $ctx);
    }
}

Dans cet exemple, l'objet récupère l'URL de connexion au service externe depuis la configuration, puis l'utilise pour envoyer les messages de log.


5Accès aux bases de données

Il faut savoir que, dans la chaîne d'initialisation du framework, les gestionnaires de log sont créés avant que les objets de connexion aux sources de données soient créés (ceci afin qu'en cas d'erreur lors de la création de ces connexions, l'information remonte justement dans les logs).

Cela veut dire que si votre gestionnaire de log a besoin d'utiliser une connexion (par exemple pour écrire les logs dans une base de données), il ne pourra pas récupérer la connexion dans son constructeur ; il devra le faire dans sa méthode log(), et vérifier que la connexion est active.

Exemple :

/** Gestionnaire de log qui écrit en base de données. */
class DatabaseLogManager implements \Temma\Base\Loader, \Temma\Web\LogManager {
    /** Composant d'injection de dépendances. */
    private \Temma\Base\Loader $_loader;

    /** Constructeur. Récupère le composant d'injection de dépendances. */
    public function __construct(\Temma\Base\Loader $loader) {
        $this->_loader = $loader;
    }
    /** Écrit les logs applicatifs en base de données. */
    public function log(string $traceId, string $text, ?string $prio, ?string $class) : void {
        // récupération de la connexion à la base de données
        $db = $this->_loader->dataSources->db ?? null;
        if (!$db)
            return;
        // écriture du log
        $sql = "INSERT INTO Log
                SET log_date = NOW(),
                    host = " . $db->quote(gethostname()) . ",
                    message = " . $db->quote($text) . ",
                    traceId = " . $db->quote($traceId) . ",
                    priority = " . $db->quote($priority) . ",
                    class = " . $db->quote($class);
        $db->exec($sql);
    }
}