
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:
À 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
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
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
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.