La journalisation NodeJS simplifiée


Combien de fois avez-vous écrit logger.info('ServiceName.methodName.') Et logger.info('ServiceName.methodName -> done.') Pour chaque méthode de votre service que vous vouliez enregistrer? Souhaitez-vous qu'il soit automatisé et qu'il ait la même signature constante sur l'ensemble de votre application? Si tel est le cas, nous nous ressemblons beaucoup, nous avons trop souvent souffert de la même douleur et nous pouvons enfin essayer de la résoudre. Ensemble. Mesdames et messieurs, permettez-moi de vous présenter ... enregistreur de classe !


"Le pourquoi" du classeur


Les ingénieurs sont souvent perfectionnistes. Les perfectionnistes à l'extrême. Nous aimons les abstractions soignées. Nous aimons le code propre. Nous voyons la beauté dans des langues artificielles que d'autres ne savent même pas lire. Nous aimons fabriquer de petits univers numériques, en respectant les règles que nous fixons. Nous aimons tout cela, probablement, parce que nous sommes très paresseux. Non, nous n'avons pas peur du travail, mais nous détestons faire tout travail pouvant être automatisé.


Après avoir écrit quelques milliers de lignes de code de journalisation uniquement, nous proposons généralement certains modèles, normalisant ce que nous voulons consigner. Pourtant, nous devons encore appliquer ces modèles manuellement. L'idée centrale de l' enregistreur de classe est donc de fournir un moyen normatif déclaratif hautement configurable de consigner les messages avant et après l'exécution d'une méthode de classe.


Démarrage rapide


Commençons et voyons à quoi ressemble le code réel.


 import { LogClass, Log } from 'class-logger' @LogClass() class ServiceCats { @Log() eat(food: string) { return 'purr' } } 

Ce service va se connecter trois fois:


  • A sa création avec une liste d'arguments passés au constructeur.
  • Avant de eat est exécuté avec une liste de ses arguments.
  • After eat est exécuté avec une liste de ses arguments et son résultat.

En mots de code:


 // Logs before the actual call to the constructor // `ServiceCats.construct. Args: [].` const serviceCats = new ServiceCats() // Logs before the actual call to `eat` // `ServiceCats.eat. Args: [milk].` serviceCats.eat('milk') // Logs after the actual call to `eat` // `ServiceCats.eat -> done. Args: [milk]. Res: purr.` 

Démo en direct .


Que pourrions-nous enregistrer d'autre? Voici la liste complète des événements:


  • Avant la construction de la classe.
  • Avant les méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.
  • Après des méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.
  • Erreurs des méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.

La propriété fonctionnelle est une fonction fléchée affectée à une propriété ( class ServiceCats { private meow = () => null } ).

L'adapter à nos besoins


Jusqu'ici tout va bien, mais on nous a promis "personnalisable", non? Alors, comment pouvons-nous le modifier?


L'enregistreur de classe fournit trois couches de configuration hiérarchique:


  • Global
  • Classe
  • La méthode

À chaque appel de méthode, tous les trois sont évalués et fusionnés de haut en bas. Il y a une configuration globale par défaut saine, vous pouvez donc utiliser la bibliothèque sans aucune configuration.


Configuration globale


C'est la configuration à l'échelle de l'application. Peut être défini avec l'appel setConfig .


 import { setConfig } from 'class-logger' setConfig({ log: console.info, }) 

Configuration de classe


Il a un effet sur toutes les méthodes de votre classe. Il pourrait remplacer la configuration globale.


 import { LogClass } from 'class-logger' setConfig({ log: console.info, }) @LogClass({ // It overrides global config for this service log: console.debug, }) class ServiceCats {} 

Configuration de la méthode


Elle affecte uniquement la méthode elle-même. Remplace la configuration de classe et, par conséquent, la configuration globale.


 import { LogClass } from 'class-logger' setConfig({ log: console.info, }) @LogClass({ // It overrides global config for this service log: console.debug, }) class ServiceCats { private energy = 100 @Log({ // It overrides class config for this method only log: console.warn, }) eat(food: string) { return 'purr' } // This method stil uses `console.debug` provided by class config sleep() { this.energy += 100 } } 

Démo en direct


Options de configuration


Eh bien, nous avons appris à modifier les valeurs par défaut, mais cela ne ferait pas de mal de couvrir ce qu'il y a à configurer, hein?


Ici vous pouvez trouver de nombreux remplacements de configuration utiles .

Voici le lien vers l'interface de l'objet config au cas où vous parleriez mieux TypeScript que l'anglais :)

L'objet de configuration a ces propriétés:


journal


C'est une fonction qui enregistre le message formaté final. Il est utilisé pour enregistrer ces événements:


  • Avant la construction de la classe.
  • Avant les méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.
  • Après des méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.

Par défaut: console.log


logError


C'est une fonction qui enregistre réellement le message d'erreur formaté final. Il est utilisé pour enregistrer ce seul et unique événement:


  • Erreurs des méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.

Par défaut: console.error


formateur


C'est un objet avec deux méthodes: start et end . Il formate les données de journalisation dans la chaîne finale.


start formate les messages pour ces événements:


  • Avant la construction de la classe.
  • Avant les méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.

end formate les messages pour ces événements:


  • Après des méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.
  • Erreurs des méthodes et propriétés fonctionnelles statiques et non statiques synchrones et asynchrones.

Par défaut: new ClassLoggerFormatterService()


inclure


La configuration de ce qui doit être inclus dans le message.


args

Il peut s'agir d'un booléen ou d'un objet.


S'il s'agit d'un booléen, il définit s'il faut inclure la liste des arguments (rappelez-vous que Args: [milk] ?) Dans les deux, commencez (avant la construction et avant l'appel de méthode) et fin (après l'appel de méthode, l'appel de méthode d'erreur), les messages.


S'il s'agit d'un objet, il doit avoir deux propriétés booléennes: start et end . start inclut / exclut la liste des arguments pour les messages de début, end fait de même pour les messages de fin.


Par défaut: true


construire

Un paramètre booléen définissant s'il faut consigner la construction de classe ou non.


Par défaut: true


résultat

Un autre paramètre d'indicateur booléen indiquant s'il faut inclure une valeur de retour d'un appel de méthode ou une erreur générée par celui-ci. Rappelez-vous Res: purr ? Si vous définissez ce drapeau sur false il n'y aura pas de Res: purr .


Par défaut: true


classInstance

Encore une fois, soit un booléen ou un objet.
Si vous l'activez, une représentation chaîne de votre instance de classe sera ajoutée aux journaux. En d'autres termes, si votre instance de classe possède certaines propriétés, elles seront converties en chaîne JSON et ajoutées au message de journal.


Toutes les propriétés ne seront pas ajoutées. class-logger suit cette logique:


  • Prendre ses propres propriétés (non prototypes) d'une instance.
    • Pourquoi? C'est un cas rare lorsque votre prototype change de manière dynamique, il n'est donc guère logique de l'enregistrer.
  • Supprimez tous ceux qui ont function type de function .
    • Pourquoi? La plupart du temps function propriétés des function sont que des fonctions fléchées immuables utilisées à la place des méthodes de classe normales pour préserver this contexte. Cela n'a pas beaucoup de sens de gonfler vos journaux avec des corps stringifiés de ces fonctions.
  • Supprimez tous ceux qui ne sont pas des objets simples.
    • Quels objets sont simples? ClassLoggerFormatterService considère un objet comme un objet simple si son prototype est strictement égal à Object.prototype .
    • Pourquoi? Souvent, nous incluons des instances d'autres classes en tant que propriétés (injectez-les en tant que dépendances). Nos journaux deviendraient extrêmement lourds si nous incluions des versions strictes de ces dépendances.
  • Stringify ce qui reste.

 class ServiceA {} @LogClass({ include: { classInstance: true, }, }) class Test { private serviceA = new ServiceA() private prop1 = 42 private prop2 = { test: 42 } private method1 = () => null @Log() public method2() { return 42 } } // Logs to the console before the class' construction: // 'Test.construct. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}.' const test = new Test() // Logs to the console before the method call: // 'Test.method2. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}.' test.method2() // Logs to the console after the method call: // 'Test.method2 -> done. Args: []. Class instance: {"prop1":42,"prop2":{"test":42}}. Res: 42.' 

Par défaut: false


Prendre le contrôle du formatage


Que se passe-t-il si vous aimez l'idée générale, mais que vous souhaitez que vos messages se présentent différemment? Vous pouvez prendre le contrôle total du formatage en passant votre propre formateur personnalisé.


Vous pouvez écrire votre propre formateur à partir de zéro. Totalement. Pourtant, nous n'allons pas couvrir cette option ici (si cela vous intéresse vraiment, consultez la section "Formatage" du fichier README).


La chose la plus rapide et, probablement, la plus simple à faire est de sous- ClassLoggerFormatterService formateur par défaut ClassLoggerFormatterService - ClassLoggerFormatterService .


ClassLoggerFormatterService a ces méthodes protégées, servant de blocs de construction du message final:


  • base
    • Renvoie le nom de la classe avec le nom de la méthode. Exemple: ServiceCats.eat .
  • operation
    • Renvoie -> done ou -> error selon qu'il s'agit d'une exécution réussie d'une méthode ou d'une erreur.
  • args
    • Renvoie une liste d'arguments chaîne de caractères. Exemple:. . Args: [milk] . Il utilise une chaîne de sécurité rapide pour les objets sous le capot.
  • classInstance
    • Renvoie une instance de classe chaîne. Exemple:. . Class instance: {"prop1":42,"prop2":{"test":42}} . Si vous choisissez d'inclure une instance de classe, mais qu'elle n'est pas disponible (c'est comme cela pour les méthodes statiques et la construction de classe), elle renvoie N/A
  • result
    • Renvoie un résultat chaîne de l'exécution (même s'il s'agissait d'une erreur). Utilise fast-safe-stringify pour sérialiser des objets. Une erreur stringifiée sera composée des propriétés suivantes:
    • Nom de la classe (fonction) avec laquelle l'erreur a été créée ( error.constructor.name ).
    • Code d'erreur ( error.code ).
    • Message d'erreur ( error.message ).
    • Nom de l'erreur ( error.name ).
    • Trace de pile ( error.stack ).
  • final
    • Retours . Justement . .

Le message de start compose de:


  • base
  • args
  • classInstance
  • final

Le message end compose de:


  • base
  • operation
  • args
  • classInstance
  • result
  • final

Vous pouvez remplacer n'importe laquelle de ces méthodes de blocs de construction. Voyons comment ajouter un horodatage. Je ne dis pas que nous devrions. pino , winston et de nombreux autres enregistreurs sont capables d'ajouter eux-mêmes des horodatages. Donc l'exemple est purement éducatif.


 import { ClassLoggerFormatterService, IClassLoggerFormatterStartData, setConfig, } from 'class-logger' class ClassLoggerTimestampFormatterService extends ClassLoggerFormatterService { protected base(data: IClassLoggerFormatterStartData) { const baseSuper = super.base(data) const timestamp = Date.now() const baseWithTimestamp = `${timestamp}:${baseSuper}` return baseWithTimestamp } } setConfig({ formatter: new ClassLoggerTimestampFormatterService(), }) 

Démo en direct


Conclusion


N'oubliez pas de suivre les étapes d'installation et de vous familiariser avec les exigences avant de décider d'utiliser cette bibliothèque.


J'espère que vous avez trouvé quelque chose d'utile pour votre projet. N'hésitez pas à me faire part de vos retours! J'apprécie très certainement toute critique et question.

Source: https://habr.com/ru/post/fr447358/


All Articles