TL;DR Executer « kill -usr1 pid », « node inspect -p pid » et le debug mode (en ligne de commande) est à vous.
Récemment, j’ai eu besoin de débugger une app Node.js en production (un setTimeout qui ne fonctionne plus après 25 jours… Original). Seul problème: Comment faire des breakpoints et donc entrer en « mode debug » sur un process qui tourne déjà… Facile!
Commençons avec une app très simple qui affiche « Hello {name} » toutes les 2 secondes.
1 2 3 4 5 6 7 8 |
// sayHello.js function sayHello(name, howManyTimes) { for (let i = 0; i < howManyTimes; i++) { console.log('Hello ' + name, i); } } module.exports = {sayHello}; |
1 2 3 4 5 6 |
// index.js const hello = require('./sayHello'); setInterval(() => { hello.sayHello('Nico', 5); }, 2000); |
Pour le lancer en production, nous utilisons la commande « node index.js« .
Time to debug!
Première chose à faire, trouver le process
1 2 3 |
ps aux | grep node nicolasdeboose 6200 ... ... ... node index. |
Ensuite, il faut le passer en mode debug. Pour ce faire, il faut le « killer » avec l’option -usr1 et le pid.
1 |
kill -usr1 6200 |
Si tout va bien, vous devriez voir ce message dans vos logs. Comme si vous aviez fait un console.log, donc au même endroit que vos « Hello {name} ».
1 2 |
Debugger listening on ws://127.0.0.1:9229/9f72d0dc-5192-42ff-b81d-bb42e6592b1b For help, see: https://nodejs.org/en/docs/inspector |
Vu que mon port 9229 était fermé dans docker, j’ai préféré tout faire en ligne de commande. C’est un peu plus fastidieux, mais ça fonctionne bien!
La prochaine étape est d’écouter/inspecter le process en indiquant le même pid.
1 |
node inspect -p 6200 |
Vous devriez avoir un nouveau message dans vos logs:
1 |
Debugger attached. |
Nous pouvons, dès à présent, commencer à rentrer dans le vif du sujet. On peut par exemple connaitre la liste des fichiers utilisés:
1 2 3 |
debug> scripts 62: /Users/nicolasdeboose/workspace/node-debug-on-prod/index.js 63: /Users/nicolasdeboose/workspace/node-debug-on-prod/sayHello.js |
Ajouter des break points. Pour cela, il suffit d’indiquer le fichier et la ligne.
1 |
debug> setBreakpoint('index.js', 4) |
Attention, car vos fichiers seront légèrement différents que le fichier source que vous connaissez. Voici par exemple le fichier index.js tel que le node l’interprète:
1 2 3 4 5 6 7 |
(function (exports, require, module, __filename, __dirname) { const hello = require('./sayHello'); setInterval(() => { hello.sayHello('Nico', 5); }, 2000); }); |
Celui-ci est d’autant plus différent si vous utilisez « ts-node » par exemple. Au début, il faut donc un peu tâtonner pour arriver à la bonne ligne.
Après avoir mis le break point , votre app devrait rapidement arriver à la ligne demandée. Celle-ci se bloque, affiche un petit « > » sur la ligne « en cours », qui attend vos instructions. Les étoiles (*) correspondent à vos autres break points:
1 2 3 4 5 6 7 8 |
1 (function (exports, require, module, __filename, __dirname) { const hello = require('./sayHello'); 2 3 setInterval(() => { > 4 hello.sayHello('Nico', 5); 5 }, 2000); 6 7 }); debug> |
A présent, vous avez le choix:
« bt »: Afficher la backtrace:
1 2 3 4 5 6 |
debug> bt #0 setInterval /Users/nicolasdeboose/workspace/node-debug-on-prod/index.js:4:4 #1 ontimeout timers.js:424:10 #2 tryOnTimeout timers.js:288:4 #3 listOnTimeout timers.js:251:4 #4 processTimers timers.js:211:9 |
« c »: Continuer l’exécution (jusqu’au prochain break point si vous en avez d’autres):
1 |
debug> c |
« n »: Aller à la prochaine instructions (La prochaine ligne):
1 2 3 4 5 6 7 |
debug> n break in /Users/nicolasdeboose/workspace/node-debug-on-prod/index.js:5 3 setInterval(() => { * 4 hello.sayHello('Nico', 5); > 5 }, 2000); 6 7 }); |
« list(n) »: Afficher plus de lignes avant et après la ligne en cours:
1 2 3 4 5 6 7 8 |
debug> list(10) 1 (function (exports, require, module, __filename, __dirname) { const hello = require('./sayHello'); 2 3 setInterval(() => { * 4 hello.sayHello('Nico', 5); > 5 }, 2000); 6 7 }); |
« s »: « Entrer » dans la fonction où l’on se trouve (« o » pour en sortir). Vous allez même pouvoir entrer dans du code de node lui-même!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
break in /Users/nicolasdeboose/workspace/node-debug-on-prod/index.js:4 2 3 setInterval(() => { > 4 hello.sayHello('Nico', 5); 5 }, 2000); 6 debug> s break in /Users/nicolasdeboose/workspace/node-debug-on-prod/sayHello.js:2 1 (function (exports, require, module, __filename, __dirname) { function sayHello(name, howManyTimes) { > 2 for (let i = 0; i < howManyTimes; i++) { 3 console.log('Hello ' + name, i); 4 } |
« exec »: Executer/Interpréter du code node « on the fly », dans le context actuel. Pratique pour connaître l’état des variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
debug> exec name 'Nico' // Afficher quelque chose en console debug> exec console.log(name) undefined debug> exec i 0 // Après quelques "n", on peut réafficher "i" debug> exec i 5 // On peut même modifier une variable debug> exec i = i + 10 // Ou executer n'importe quel code... debug> Promise.resolve("cool").then(console.log) cool |
Il existe cependant quelques limitations. Par exemple « require » qui ne peut pas être appelé:
1 2 3 4 5 6 7 8 |
debug> exec require ReferenceError: require is not defined at eval (eval at setInterval (/Users/nicolasdeboose/workspace/node-debug-on-prod/index.js:4:5), <anonymous>:1:1) at Timeout.setInterval [as _onTimeout] (/Users/nicolasdeboose/workspace/node-debug-on-prod/index.js:4:5) at ontimeout (timers.js:424:11) at tryOnTimeout (timers.js:288:5) at listOnTimeout (timers.js:251:5) at Timer.processTimers (timers.js:211:10) |
Et voilà, plus d’excuse maintenant! Si vous avez un bug en production, vous avez les outils pour trouver votre bug si vos logs sont incomplets ou si, comme moi, vous avez un problème avec une version de node en particulier 🙂