Attention: ce document est daté et n'aborde vraiment que des aspects en relation avec le système de log.
Ce document décrit l'architecture générale choisie pour agencer les modules et classes de la bibliothèque Ceylan. Cette vue d'ensemble peut servir de guide pour explorer la documentation complète de l'API Ceylan et des développements correspondants.
La structure générale de Ceylan est celle d'un paquetage qui fédère un ensemble de services. Chaque service se définit comme une unité fonctionnelle prise en charge par un ensemble de modules et classes. Bien que quelques services observent une logique autosuffisante, la plupart s'appuient sur d'autres services, soit en provenance directe de Ceylan, soit de travaux tiers.
La bibliothèque Ceylan est notamment utilisée par la bibliothèque OSDL [lien direct vers la description de son architecture].
Le panorama esquissé ici décompose la bibliothèque en services, eux-même rassemblant un ou plusieurs modules. Cette description retrace à grands traits ce qu'il serait idéal d'implémenter, sachant que ce recensement, à mi-chemin entre une liste à la Prévert et une lettre au Père Noël, sera douloureusement mis à mal par sa réalisation pratique, qui nous contraindra à réduire de beaucoup nos ambitions et à ne développer que les modules strictement nécessaires.
Rien n'interdit toutefois d'imaginer ce que pourrait être la vue d'ensemble avant d'affronter le monde réel et ses inévitables concessions.
LogService: service de log et de trace d'exécution
|
LogService: service de log et de trace d'exécution |
Il s'agit de donner la capacité à des objets, lors de l'exécution, d'envoyer des messages concernant leur état, messages qui seront enregistrés pour être consultables en cours d'exécution (en quasi temps réel) ou a posteriori (post mortem).
En l'occurence, afin d'assurer une certaine convivialité, il sera possible d'étudier ces compte-rendus d'exécution à partir d'un simple navigateur web, puisqu'ils pourront prendre la forme d'un ensemble de pages hypertexte (html).
L'adjonction de deux notions renforce l'utilisabilité de ce service:
Chaque message est émis sur un canal. Les canaux peuvent être d'un type parmi trois:
Loggable
, qui est un cas particulier de LogSource
. Cet espace de communication est dédié à une instance particulière, elle peut donc utiliser à sa guise. Un canal privé est nommé d'après son instance, son nom effectif sera fonction de l'hôte (ordinateur) où a été instancié l'objet correspondant à ce canal, du numéro du processus (PID) qui l'héberge, du nom de sa classe, et de l'adresse mémoire qu'il occupe. Ce quadruplet doit permettre d'identifier de manière unique, à un instant donné, n'importe quelle instance particulière. Un exemple, prenant la forme d'une URI (Uniform Resource Identifier, dont les classiques URL sont des sous-classes),
serait:
log://<nom complet de l'hôte de l'instance>/<PID-numéro du PID-hôte>/<nom de la classe>/<adresse de l'instance>
|
log://osdl.dnsalias.com/PID-9100/Lockable/0xA13F3004F
|
LogSource
.
A chaque message est rattaché en outre un niveau de détail, qui quantifie son importance: plus le niveau de détail sera petit, plus le message devra être significatif, porteur de sens.
Identiquement, chaque canal dispose de son propre niveau de détail, qui détermine le seuil maximal au delà duquel les messages trop détaillés qui lui sont adressés seront ignorés. Fort logiquement, un message sera répercuté par un canal si et seulement si le niveau de détail du message est inférieur au seuil maximal d'intérêt du canal.
Enfin, à leur création, tant les messages que les canaux disposent d'un niveau de détail par défaut, ce qui rend leur positionnement explicite facultatif.
Ce module, en dépit de son nom, n'a donc pas l'ambition de calculer le moindre logarithme !
Ce service de log est d'une grande utilité pour le débogage et la mise au point de systèmes complexes, notamment multi-agents et/ou multitâches. Il permet aux développeurs d'avoir la chronologie des informations émises par chacun des objets utilisant ce service, et de croiser ces informations de manière à se faire une idée des changements d'états successifs ayant affecté un ensemble d'objets, alors que ces changements sont trop nombreux ou trop rapides pour être interprétés en temps réel par un humain: les logs sont souvent le seul moyen rapide de savoir ce qu'il se passe lors de l'exécution d'un programme, et leur exhaustivité compléte idéalement un débogueur. La succession des messages permet donc de connaître leur ordre temporel, et il est possible en outre à un canal de dater chacun des messages qu'il reçoit, au moins d'horodatage (time-stamps). Cela permet ainsi de reconstituer l'ordre de messages envoyés à des canaux différents.
Un cas typique d'utilisation d'un service de log est celui de la mise au point d'un système multi-agent proies/prédateurs, dans lequel sur un terrain de jeu des créatures vivent et meurent selon les règles présidant à leurs destinées qui ont été implémentées. D'une superposition de comportements élémentaires résulte un comportement collectif complexe, pour l'analyse duquel un compte-rendu détaillé des messages envoyés par chacune des créatures artificielles en présence est précieux: il devient possible de suivre l'histoire de n'importe laquelle d'entre elles, et de s'assurer qu'elle se comporte conformément aux règles prévues.
Les objets souhaitant disposer de la possibilité d'envoyer des messages doivent hériter de la classe mère LogSource
. Comme on l'a vu, chaque instance implémentant cette interface est alors identifiée par la pseudo-URI (Uniform Resource Identifier) de type
log://machine/PID-pid/classe/adresse
|
log://aranor/PID-22541/Beholder/0x4A10FE94 |
LogSource
est alors raccordé à un LogListener
, qui se charge d'être le réceptacle des messages envoyés et de les transmettre à un LogAggregator
, dont la tâche est de générer le document html correspondant à ces messages afin de les rendre consultables de manière conviviale.
Au moins deux implémentations de la paire d'interfaces LogListener
/ LogAggregator
sont envisageables.
Cette forme basique est assurée par une seule classe qui implémente elle-même, directement, les deux interfaces. Cette version du système de log est destinée à être complètement contenue dans le programme qui en fait usage et à en faire partie, par exemple sous la forme d'une instance statique: chaque objet LogSource
peut s'y référer pour envoyer ses messages, et cette même instance accomplit les deux fonctions, celle de collecter les messages (LogListener
) et celle d'en générer une synthèse hypertexte (LogAggregator
).
Ce système a l'avantage de la simplicité, mais souffre de quelques carences. L'implémentation des logs dispose notamment de la même espérance de vie que le programme qui l'utilise, ce qui veut dire que si le programme en cours de développement connaît un crash, le système de log subira le même sort. Les inscriptions dans des fichiers pourront certes subsister (en s'asssurant de vider les tampons d'entrée/sortie à chaque écriture), mais les éventuelles opérations d'agrégation de sources et d'achèvement de la génération des pages html ne pourront être menées à bien. Néanmoins ce système reste en tout point supérieur à la méthode la plus usuelle, qui consiste à truffer son code d'appels de macros, qui en mode mise au point se transforment en fprintf
, cout
ou cerr
.
La deuxième implémentation du système de log demande un investissement plus important, mais qui en vaut la peine. Elle consiste à écrire un LogListener
, qui peut prendre lui aussi la forme d'un objet statique, mais dont le rôle est uniquement de collecter les messages et, par le réseau, de les transmettre à un LogAggregator
indépendant.
Cette instance de LogAggregator
peut fort bien se trouver sur la même machine ou sur une autre, dans tous les cas elle ne dépendra de la durée de vie d'un processus l'utilisant, fonctionnant à la manière d'un démon UNIX. Qui plus est, une seule instance de LogAggregator
peut servir autant de d'instances LogListener
que voulu, c'est-à-dire autant de processus sur autant de machines que l'on veut: on dispose ainsi d'un système de log distribué particulièrement pratique et complet, qui à notre connaissance est le nec plus ultra en ce domaine.
Couplé à un simple serveur web, ce système rend possible la consultation des logs distribués depuis un nombre quelconque de navigateurs sur n'importe quelle(s) machine(s).
Un système de log est un outil précieux mais qui induit un certain coût en ressources: chaque envoi de message est un appel de méthode, et la constitution des chaînes de caractères transmises en tant qu'identifiant d'un Loggable
ou en tant que message consomme de la puissance de traitement et de la mémoire. Comme, pour la mise au point, il n'est pas envisageable de se passer d'un tel outil et comme, pour une version testée et diffusée, il est souhaitable de faire l'économie de ce système, il est important de pouvoir le désactiver totalement, à moindre effort, tout en se réservant la possibilité de le réactiver à loisir quand le besoin s'en fait sentir.
Une possibilité, pour contrôler l'activation du système de log, serait de décider de la constitution et de l'envoi du message en évaluant à l'exécution une expression, qui pourrait être aussi simple qu'un booléen logActivated
que l'on positionnerait, au gré des besoins, à vrai ou à faux. C'est une solution fonctionnelle, mais pour laquelle un coût subsiste, celui de l'appel de méthode et du test. Il serait loisible de pouvoir totalement éviter ce coût, car sa fréquence de survenue peut être très élevée.
Une solution pourrait être aussi de recourir à une macro, qui serait évaluée par le préprocesseur, et qui pourrait se traduire en la constitution effective du message et son envoi, ou en une instruction vide, selon qu'une variable est positionnée ou non à la compilation. Ainsi, le code de l'application changerait automatiquement et le système serait débrayable. C'est une solution tout-à-fait viable, mais on incline à considérer que l'utilisation de macros est à éviter, dans la mesure où elle aboutit à compiler du code sans que forcément le développeur en soit conscient ou le connaisse, qui plus en induisant quelques chausse-trappes.
Une solution au principe proche serait de recourir à une paire de scripts qui se charge de manipuler le code source pour respectivement l'instrumenter ou le désinstrumenter, en enlevant (par la mise en commentaire) les appels aux logs ou en les remplaçant. C'est un sous-cas de préprocesseur, qui a pour avantage de laisser le code qui sera réellement compilé immédiatement lisible. C'est à l'heure actuelle la moins mauvaise des solutions que l'on connaisse, et que l'on essaiera d'implémenter.
Elle est simple: chaque LogSource
, une fois créée, peut envoyer des messages, soit implicitement à son canal privé, soit, en le précisant, sur un canal standard ou dynamiquement créé.
A nouveau deux choix étaient possibles: l'envoi de messages uniquement sous forme de chaîne de caractères, ou la possibilité de fournir un flux, au sens des iostreams
. Ceylan proposant par ailleurs des opérateurs conviviaux de conversion en chaînes de caractères et encourageant la généralisation de l'implémentation de l'interface TextDisplayable (qui propose la méthode toString
), il a été décidé, au moins dans un premier temps, de se cantonner à l'envoi simple de chaînes de caractères.
Si vous disposez d'informations plus détaillées ou plus récentes que celles présentées dans ce document, si vous avez remarqué des erreurs, oublis, ou points insuffisamment traités, envoyez-nous un courriel !