Comment utiliser Winston pour journaliser les applications Node.js

introduction

Une solution de journalisation efficace est essentielle au succès de toute application. Dans ce guide, nous allons nous concentrer sur un package de journalisation appeléWinston, une bibliothèque de journalisation extrêmement polyvalente et la solution de journalisation la plus populaire disponible pour les applicationsNode.js, basée sur les statistiques de téléchargement NPM. Les fonctionnalités de Winston incluent la prise en charge de plusieurs options de stockage et niveaux de journal, des requêtes de journal et même d’un profileur intégré. Ce didacticiel vous montrera comment utiliser Winston pour consigner une application Node / http: //expressjs.com/ [Express] que nous allons créer dans le cadre de ce processus. Nous verrons également comment nous pouvons combiner Winston avec un autre enregistreur middleware de requêtes HTTP populaire pour Node.js appeléMorgan pour consolider les journaux de données de requêtes HTTP avec d'autres informations.

Une fois ce tutoriel terminé, un serveur Ubuntu exécutera une petite application Node / Express. Winston sera également implémenté pour enregistrer les erreurs et les messages dans un fichier et la console.

Conditions préalables

Avant de commencer ce guide, vous aurez besoin des éléments suivants:

Avec ces conditions préalables en place, nous pouvons créer notre application et installer Winston.

[[step-1 -—- creating-a-basic-node-express-app]] == Étape 1 - Création d'un nœud de base / application express

Winston utilise couramment les événements de journalisation à partir d’applications Web créées avec Node.js. Afin de démontrer pleinement comment incorporer Winston, nous allons créer une application Web simple Node.js à l’aide du cadre Express. Pour nous aider à faire fonctionner une application Web de base, nous utiliseronsexpress-generator, un outil de ligne de commande permettant de faire fonctionner rapidement une application Web Node / Express. Comme nous avons installé lesNode Package Manager dans le cadre de nos prérequis, nous pourrons utiliser la commandenpm pour installerexpress-generator. Nous utiliserons également l'indicateur-g, qui installe le package globalement afin qu'il puisse être utilisé comme outil de ligne de commande en dehors d'un projet / module Node existant. Installez le paquet avec la commande suivante:

sudo npm install express-generator -g

Une foisexpress-generator installé, nous pouvons créer notre application en utilisant la commandeexpress, suivie du nom du répertoire que nous voulons utiliser pour notre projet. Cela créera notre application avec tout ce dont nous avons besoin pour commencer:

express myApp

Ensuite, installezNodemon, qui rechargera automatiquement l'application chaque fois que nous apporterons des modifications. Une application Node.js doit être redémarrée chaque fois que des modifications sont apportées au code source pour que ces modifications prennent effet. Nodemon surveillera automatiquement les modifications et redémarrera l'application pour nous. Et puisque nous voulons pouvoir utilisernodemon comme outil de ligne de commande, nous allons l'installer avec l'indicateur-g:

sudo npm install nodemon -g

Pour terminer la configuration de l'application, accédez au répertoire de l'application et installez les dépendances comme suit:

cd myApp
npm install

Par défaut, les applications créées avecexpress-generator s'exécutent sur le port 3000, nous devons donc nous assurer que ce port n'est pas bloqué par le pare-feu. Pour ouvrir le port 3000, exécutez la commande suivante:

sudo ufw allow 3000

Nous avons maintenant tout ce dont nous avons besoin pour démarrer votre application Web. Pour ce faire, exécutez la commande suivante:

nodemon bin/www

Ceci démarre l'application en cours d'exécution sur le port 3000. Nous pouvons vérifier que cela fonctionne en accédant àhttp://your_server_ip:3000 dans un navigateur Web. Vous devriez voir quelque chose comme ça:

Default express-generator homepage

C’est une bonne idée à ce stade de démarrer une deuxième session SSH sur votre serveur à utiliser pour le reste de ce didacticiel, en laissant l’application Web que nous venons de lancer dans la session d’origine. Pour le reste de cet article, nous ferons référence à la session SSH que nous utilisions jusqu'à présent et qui exécute actuellement l'application en tant que session A. Nous allons utiliser la nouvelle session SSH pour exécuter des commandes et éditer des fichiers. Cette session est appelée Session B. Sauf indication contraire, toutes les commandes restantes doivent être exécutées dans la session B.

Étape 2 - Personnalisation de l'application Node.js

L'application par défaut créée parexpress-generator fait un excellent travail pour nous aider à démarrer, et elle inclut même le middleware de journalisation HTTP Morgan que nous utiliserons pour enregistrer les données concernant toutes les requêtes HTTP. De plus, étant donné que Morgan prend en charge les flux de sortie, il établit un bon couplage avec le support de flux intégré à Winston, ce qui nous permet de consolider les journaux de données des demandes HTTP avec tout ce que nous avons choisi de connecter avec Winston.

Par défaut, le passe-partoutexpress-generator utilise la variablelogger lors du référencement du packagemorgan. Puisque nous allons utilisermorgan etwinston, qui sont tous deux des packages de journalisation, il peut être déroutant d'appeler l'un ou l'autrelogger. Modifions donc cela en éditant le fichierapp.js à la racine du projet et en apportant quelques modifications.

Pour ouvrirapp.js pour modification, utilisez la commandenano:

nano ~/myApp/app.js

Recherchez la ligne suivante dans la partie supérieure du fichier:

~/myApp/app.js

...
var logger = require('morgan');
...

Changez-le comme suit:

~/myApp/app.js

...
var morgan = require('morgan');
...

Nous devons également trouver où la variablelogger a été référencée dans le fichier et la changer enmorgan. Pendant que nous y sommes, changeons le format de journal utilisé par le packagemorgan encombined, qui est le format de journal Apache standard et inclura des informations utiles dans les journaux telles que l'adresse IP distante et l'utilisateur -agent en-tête de requête HTTP.

Pour ce faire, recherchez la ligne suivante:

~/myApp/app.js

...
app.use(logger('dev'));
...

Changez-le comme suit:

~/myApp/app.js

...
app.use(morgan('combined'));
...

Ces modifications nous aideront à mieux comprendre le package de journalisation que nous référons à tout moment après l'intégration de notre configuration Winston.

Quittez et enregistrez le fichier en tapantCTRL-X, puisY, puisENTER.

Maintenant que notre application est configurée, nous sommes prêts à commencer à travailler avec Winston.

[[step-3 -—- installation-and-configuring-winston]] == Étape 3 - Installation et configuration de Winston

Nous sommes maintenant prêts à installer et configurer Winston. Dans cette étape, nous explorerons certaines des options de configuration disponibles dans le cadre du packagewinston et créerons un enregistreur qui enregistrera les informations dans un fichier et la console.

Pour installerwinston, exécutez la commande suivante:

cd ~/myApp
npm install winston

Il est souvent utile de conserver tout type de fichier de support ou de configuration d’utilitaire pour nos applications dans un répertoire spécial, alors créons un dossierconfig qui contiendra la configuration dewinston:

mkdir ~/myApp/config

Créons maintenant le fichier qui contiendra notre configurationwinston, que nous appelleronswinston.js:

touch ~/myApp/config/winston.js

Ensuite, créez un dossier qui contiendra vos fichiers journaux:

mkdir ~/myApp/logs

Enfin, installonsapp-root-path, un package utile lors de la spécification de chemins dans Node.js. Ce paquet n'est pas directement lié à Winston, mais aide énormément lors de la spécification des chemins d'accès aux fichiers dans le code Node.js. Nous allons l’utiliser pour spécifier l’emplacement des fichiers journaux Winston à partir de la racine du projet et éviter la syntaxe laide de chemin relatif:

npm install app-root-path --save

Tout ce dont nous avons besoin pour configurer la manière dont nous voulons gérer notre journalisation est en place, nous pouvons donc définir nos paramètres de configuration. Commencez par ouvrir~/myApp/config/winston.js pour l'édition:

 nano ~/myApp/config/winston.js

Ensuite, exigez les packagesapp-root-path etwinston:

~/myApp/config/winston.js

var appRoot = require('app-root-path');
var winston = require('winston');

Avec ces variables en place, nous pouvons définir les paramètres de configuration pour nostransports. Les transports sont un concept introduit par Winston qui fait référence aux mécanismes de stockage / sortie utilisés pour les journaux. Winston est livré avec trois transports principaux -console,file etHTTP. Nous allons nous concentrer sur les transports de console et de fichiers pour ce didacticiel: le transport de console enregistrera les informations sur la console et le transport de fichiers enregistrera les informations dans un fichier spécifié. Chaque définition de transport peut contenir ses propres paramètres de configuration, tels que la taille du fichier, les niveaux de journalisation et le format de journalisation. Voici un résumé rapide des paramètres que nous allons utiliser pour chaque transport:

  • level - Niveau des messages à consigner.

  • filename - Le fichier à utiliser pour écrire les données du journal.

  • handleExceptions - Récupère et consigne les exceptions non gérées.

  • json - Enregistre les données du journal au format JSON.

  • maxsize - Taille maximale du fichier journal, en octets, avant qu'un nouveau fichier ne soit créé.

  • maxFiles - Limite le nombre de fichiers créés lorsque la taille du fichier journal est dépassée.

  • colorize - Colorise la sortie. Cela peut être utile lorsque vous consultez les journaux de la console.

LesLogging levels indiquent la priorité du message et sont indiqués par un entier. Winston utilise les niveaux de journalisationnpm qui sont hiérarchisés de 0 à 5 (du plus élevé au plus bas):

  • 0: erreur

  • 1: avertir

  • 2: informations

  • 3: verbeux

  • 4: débogage

  • 5: idiot

Lorsque vous spécifiez un niveau de journalisation pour un transport particulier, tout élément de ce niveau ou supérieur sera consigné. Par exemple, en spécifiant un niveau deinfo, tout ce qui se trouve au niveauerror,warn ouinfo sera consigné. Les niveaux de journalisation sont spécifiés lors de l'appel de l'enregistreur, ce qui signifie que nous pouvons effectuer les opérations suivantes pour enregistrer une erreur:logger.error('test error message').

Nous pouvons définir les paramètres de configuration pour les transportsfile etconsole dans la configurationwinston comme suit:

~/myApp/config/winston.js

...
var options = {
  file: {
    level: 'info',
    filename: `${appRoot}/logs/app.log`,
    handleExceptions: true,
    json: true,
    maxsize: 5242880, // 5MB
    maxFiles: 5,
    colorize: false,
  },
  console: {
    level: 'debug',
    handleExceptions: true,
    json: false,
    colorize: true,
  },
};

Ensuite, instanciez un nouveau loggerwinston avec les transports de fichiers et de console en utilisant les propriétés définies dans la variableoptions:

~/myApp/config/winston.js

...
var logger = new winston.Logger({
  transports: [
    new winston.transports.File(options.file),
    new winston.transports.Console(options.console)
  ],
  exitOnError: false, // do not exit on handled exceptions
});

Par défaut,morgan sort uniquement vers la console, définissons donc une fonction de flux qui pourra obtenir la sortie générée parmorgan dans les fichiers journaux dewinston. Nous utiliserons le niveauinfo pour que la sortie soit récupérée par les deux transports (fichier et console):

~/myApp/config/winston.js

...
logger.stream = {
  write: function(message, encoding) {
    logger.info(message);
  },
};

Enfin, exportez l’enregistreur afin qu’il puisse être utilisé dans d’autres parties de l’application:

~/myApp/config/winston.js

...
module.exports = logger;

Le fichier de configuration dewinston terminé doit ressembler à ceci:

~/myApp/config/winston.js

var appRoot = require('app-root-path');
var winston = require('winston');

// define the custom settings for each transport (file, console)
var options = {
  file: {
    level: 'info',
    filename: `${appRoot}/logs/app.log`,
    handleExceptions: true,
    json: true,
    maxsize: 5242880, // 5MB
    maxFiles: 5,
    colorize: false,
  },
  console: {
    level: 'debug',
    handleExceptions: true,
    json: false,
    colorize: true,
  },
};

// instantiate a new Winston Logger with the settings defined above
var logger = new winston.Logger({
  transports: [
    new winston.transports.File(options.file),
    new winston.transports.Console(options.console)
  ],
  exitOnError: false, // do not exit on handled exceptions
});

// create a stream object with a 'write' function that will be used by `morgan`
logger.stream = {
  write: function(message, encoding) {
    // use the 'info' log level so the output will be picked up by both transports (file and console)
    logger.info(message);
  },
};

module.exports = logger;

Quittez et enregistrez le fichier.

Notre enregistreur est maintenant configuré, mais notre application ne sait toujours pas comment l’utiliser. Nous allons maintenant intégrer l’enregistreur à l’application.

[[step-4 -—- integrating-winston-with-our-application]] == Étape 4 - Intégration de Winston à notre application

Pour que notre enregistreur fonctionne avec l'application, nous devons en informerexpress. Nous avons déjà vu à l'étape 2 que notre configuration deexpress se trouve dansapp.js, alors importons notre logger dans ce fichier. Ouvrez le fichier pour le modifier en lançant:

nano ~/myApp/app.js

Importezwinston vers le haut du fichier avec les autres instructions require:

~/myApp/app.js

...
var winston = require('./config/winston');
...

Le premier endroit où nous utiliserons réellementwinston est avecmorgan. Nous utiliserons l'optionstream et la définirons sur l'interface de flux que nous avons créée dans le cadre de la configuration dewinston. Pour ce faire, recherchez la ligne suivante:

~/myApp/app.js

...
app.use(morgan('combined'));
...

Changez-le en ceci:

~/myApp/app.js

...
app.use(morgan('combined', { stream: winston.stream }));
...

Quittez et enregistrez le fichier.

Nous sommes prêts à voir des données de journal! Si vous rechargez la page dans le navigateur Web, vous devriez voir quelque chose de similaire au suivant dans la console de SSH Session A:

Output[nodemon] restarting due to changes...
[nodemon] starting `node bin/www`
info: ::ffff:72.80.124.207 - - [07/Mar/2018:17:29:36 +0000] "GET / HTTP/1.1" 304 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36"

info: ::ffff:72.80.124.207 - - [07/Mar/2018:17:29:37 +0000] "GET /stylesheets/style.css HTTP/1.1" 304 - "http://167.99.4.120:3000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36"

Il y a deux entrées de journal ici - la première pour la demande à la page HTML, la seconde pour la feuille de style accompagnée. Étant donné que chaque transport est configuré pour gérer les données du journal de niveauinfo, nous devrions également voir des informations similaires dans le transport de fichier situé à~/myApp/logs/app.log. La sortie dans le transport de fichiers, cependant, doit être écrite en tant qu'objet JSON puisque nous avons spécifiéjson: true dans la configuration de transport de fichiers. Vous pouvez en savoir plus sur JSON dans nosintroduction to JSON tutorial. Pour afficher le contenu du fichier journal, exécutez la commande suivante:

tail ~/myApp/logs/app.log

Vous devriez voir quelque chose de similaire au suivant:

{"level":"info","message":"::ffff:72.80.124.207 - - [07/Mar/2018:17:29:36 +0000] \"GET / HTTP/1.1\" 304 - \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36\"\n","timestamp":"2018-03-07T17:29:36.962Z"}
{"level":"info","message":"::ffff:72.80.124.207 - - [07/Mar/2018:17:29:37 +0000] \"GET /stylesheets/style.css HTTP/1.1\" 304 - \"http://167.99.4.120:3000/\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36\"\n","timestamp":"2018-03-07T17:29:37.067Z"}

Jusqu'à présent, notre enregistreur n'enregistre que les requêtes HTTP et les données associées. Ces informations sont très importantes dans nos journaux, mais comment enregistrer des messages de journal personnalisés? Il y aura certainement des moments où nous voudrons cette capacité pour des choses telles que les erreurs d'enregistrement ou le profilage des performances des requêtes de base de données, par exemple. Pour illustrer cette opération, appelons l’enregistreur à partir de la route du gestionnaire d’erreurs.

Le packageexpress-generator inclut une route de gestionnaire d'erreurs 404 et 500 par défaut, nous allons donc travailler avec cela. Ouvrez le fichier~/myApp/app.js:

nano ~/myApp/app.js

Trouvez le bloc de code au bas du fichier qui ressemble à ceci:

~/myApp/app.js

...
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
...

Il s’agit de la dernière route de traitement des erreurs qui renverra une réponse au client. Étant donné que toutes les erreurs côté serveur seront exécutées via cette route, c'est un bon endroit pour inclure le journal dewinston.

Puisque nous traitons maintenant des erreurs, nous voulons utiliser le niveau de journalisationerror. Encore une fois, les deux transports sont configurés pour consigner les messages de niveauerror donc nous devrions voir la sortie dans la console et les journaux de fichiers. Nous pouvons inclure tout ce que nous voulons dans le journal, alors assurez-vous d'inclure des informations utiles telles que:

  • err.status - Le code d'état d'erreur HTTP. S'il n'y en a pas déjà un, 500 par défaut.

  • err.message - Détails de l'erreur.

  • req.originalUrl - L'URL qui a été demandée.

  • req.path - La partie chemin de l'URL de la demande.

  • req.method - Méthode HTTP de la requête (GET, POST, PUT, etc.).

  • req.ip - Adresse IP distante de la demande.

Mettez à jour l'itinéraire du gestionnaire d'erreurs pour qu'il corresponde à ce qui suit:

~/myApp/app.js

...
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // add this line to include winston logging
  winston.error(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
...

Quittez et enregistrez le fichier.

Pour tester cela, essayons d’accéder à une page de notre projet inexistante, ce qui générera une erreur 404. De retour dans votre navigateur Web, essayez de charger l'URL suivante:http://your_server_ip:3000/foo. L'application est déjà configurée pour répondre à une telle erreur, grâce au passe-partout créé parexpress-generator. Votre navigateur devrait afficher un message d'erreur ressemblant à ceci (votre message d'erreur peut être plus détaillé que ce qui est affiché):

Browser error message

Maintenant, jetez un autre regard sur la console dans la session SSH A. Il devrait y avoir une entrée de journal pour l'erreur, et grâce au paramètre colorize, il devrait être facile à trouver.

Output[nodemon] starting `node bin/www`
error: 404 - Not Found - /foo - GET - ::ffff:72.80.124.207
info: ::ffff:72.80.124.207 - - [07/Mar/2018:17:40:11 +0000] "GET /foo HTTP/1.1" 404 985 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36"

info: ::ffff:72.80.124.207 - - [07/Mar/2018:17:40:11 +0000] "GET /stylesheets/style.css HTTP/1.1" 304 - "http://167.99.4.120:3000/foo" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36"

En ce qui concerne l'enregistreur de fichiers, exécuter à nouveau la commandetail devrait nous montrer les nouveaux enregistrements de journal:

tail ~/myApp/logs/app.log

Vous verrez un message comme celui-ci:

{"level":"error","message":"404 - Not Found - /foo - GET - ::ffff:72.80.124.207","timestamp":"2018-03-07T17:40:10.622Z"}

Le message d'erreur comprend toutes les données que nous avons spécifiquement demandé àwinston de consigner dans le cadre du gestionnaire d'erreurs, y compris l'état de l'erreur (404 - Not Found), l'URL demandée (localhost / foo), la méthode de demande (GET) , l'adresse IP qui fait la demande et l'horodatage de la demande.

Conclusion

Dans ce didacticiel, vous avez créé une application Web simple Node.js et intégré une solution de journalisation Winston qui fonctionnera comme un outil efficace pour fournir des informations sur les performances de l'application. Vous pouvez faire beaucoup plus pour créer des solutions de journalisation robustes pour vos applications, en particulier à mesure que vos besoins deviennent plus complexes. Nous vous recommandons de prendre le temps d'examiner certains de ces autres documents: