/**
* module log avec ses nos loggers maison
* @todo utiliser https://www.npmjs.org/package/winston
* @todo utiliser https://nodejs.org/api/util.html#util_util_debuglog_section
*/
const fs = require('fs')
const { formatDateTime } = require('sesajs-date')
const sjt = require('sesajstools')
const config = require('../config')
/**
* Retourne une writeStream sur le fichier passé en arguments (qui sera ouvert dans le dossier de log défini dans la conf)
* @private
* @param {string} log Nom du log (sans dossier parent)
* @param {boolean} [verbose] Annonce l'ouverture et la fermeture du fichier dans le fichier
* @returns {stream.Writable}
*/
function getLogStream (log, verbose) {
const file = `${logDir}/${log}`
const options = { flags: 'a', mode: '0644' }
let stream
try {
stream = fs.createWriteStream(file, options)
if (stream) {
if (verbose) {
streamsVerbose.push(stream)
stream.write(getPrefix() + 'log opened by pid ' + process.pid + '\n')
} else {
streamsQuiet.push(stream)
}
} else {
console.error(Error(`impossible d’ouvrir le log ${file}`))
}
} catch (error) {
console.error(`impossible d’ouvrir le log ${file}`, error)
}
return stream
}
/**
* Formate le message et l'envoie dans un log ou en console (si stream est null)
* @private
* @param {string|Error} message
* @param {Object} [objectToDump] Un objet éventuel (qui sera rendu en json avec indentation de n si options.indent=n)
* @param {string} filter Un nom de filtre pour exclusion éventuelle
* @param {writeStream} stream stream vers le fichier de log
* @param {Object} [options] Passer une propriété
* indent pour indenter objectToDump du nombre d'espaces demandés,
* max pour modifier la limite de la sortie (200 par défaut)
*/
function out (message, objectToDump, filter, stream, options) {
// si on nous demande de laisse tomber on le fait
if (message && message.noLog) return
if (!options) options = {}
if (!filter || !exclusions[filter]) {
let msg // le message qu'on enverra en console
// si erreur on veut toute la pile, qui contient aussi message.toString() en 1er
if (message instanceof Error) {
if (message.logged) return // déjà traité précédemment
message.logged = true
msg = message.stack + '\n'
} else if (typeof message === 'string') {
msg = message
} else {
// y'a eu un pb avant l'appel de cette fct, on génère une erreur pour récupérer la pile d'appel
try {
throw new Error('erreur inconnue passé à log')
} catch (error) {
msg = error.stack + '\n'
}
}
if (objectToDump) {
if (objectToDump instanceof Error) {
msg += '\n' + objectToDump.stack + '\n'
} else if (typeof objectToDump === 'function') {
msg += '\n' + objectToDump.toString() + '\n'
} else {
let dump = sjt.stringify(objectToDump, options.indent)
if (dump) {
const max = (options && options.max) || 200
if (dump.length > max) dump = dump.substr(0, max) + '…'
msg += '\n' + dump + '\n'
} else {
console.error('pb dans log, objectToDump existe mais donne undefined', objectToDump)
}
}
}
msg = getPrefix() + msg
if (stream) stream.write(msg + '\n')
else console.log(msg)
}
}
/**
* Retourne le préfixe avec la date+heure courante entre crochet
* @private
* @returns {string}
*/
const getPrefix = () => `[${formatDateTime()}] `
let disabled = false
// si le dossier de log n'existe pas on le crée
const logDir = config.logs.dir
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, 0o775)
/**
* une pile pour les streams que l'on créé (pour les fermer au shutdown)
* @private
* @type {stream.Writable[]}
*/
const streamsQuiet = []
const streamsVerbose = []
// les streams vers nos logs, celui de dev est ouvert plus loin si on est en dev
let debugOutputStream
// ces logs dans tous les cas
/** un log d'erreur actif en prod */
const errorOutputStream = getLogStream(config.logs.error, true)
/** un log spécifique pour les erreurs liées à des datas incohérentes */
const errorDataOutputStream = getLogStream(config.logs.dataError, true)
/** un log pour mesure de performances */
let perfOutputStream
/**
* Les messages à exclure
* (une valeur à true excluera les debug de ce type dans le log de debug)
* @private
*/
const exclusions = {}
/**
* Méthode qui écrit en console si l'on est pas en prod (ne fait rien en prod)
* @service log
* @type {function}
* @param {string|object} message
* @param {Object} [objectToDump] Un objet éventuel qui sera rendu en json avec indentation
* @param {string} [filter] Un nom de filtre pour exclusion éventuelle
* @param {Object} [options] Passer les propriétés facultatives
* indent pour indenter objectToDump du nombre d'espaces demandés,
* max pour modifier la limite de la sortie (200 par défaut)
*/
function log (message, objectToDump, filter, options) {
if (disabled) return
// (log étant défini en global dans la conf jshint il râle si on le redéfini)
if (arguments.length === 3 && typeof filter === 'object') {
options = filter
filter = 'info'
}
out(message, objectToDump, filter, null, options)
}
/**
* Ajoute un message (avec éventuellement le dump d'un objet) dans le log d'erreur de données (config.logs.dataError)
* @memberOf log
* @param message
* @param objectToDump
* @param filter
*/
log.dataError = function dataError (message, objectToDump, filter) {
// on peut être utilisé comme callback
if (arguments.length === 0) return
// pour les dataError, on met un max élevé s'il est pas précisé
if (!filter) filter = {}
if (!filter.max) filter.max = 50000
out(message, objectToDump, filter, errorDataOutputStream, { max: 2000 })
}
// log.debug
if (config.logs.debug) {
// notre stream vers debug.log
debugOutputStream = getLogStream(config.logs.debug, true)
/**
* Écrit dans le fichier config.logs.debug s'il est précisé (ne fait rien sinon)
* @memberOf log
* @param message
* @param objectToDump
* @param [filter]
* @param options
*/
log.debug = function debug (message, objectToDump, filter, options) {
if (arguments.length === 3 && typeof filter === 'object') {
options = filter
filter = 'info'
}
out(message, objectToDump, filter, debugOutputStream, options)
}
if (config.logs.debugExclusions) {
config.logs.debugExclusions.forEach(function (filter) {
exclusions[filter] = true
})
}
} else {
log.debug = function () {}
}
/**
* Rend log() muet
*/
log.disable = () => { disabled = true }
/**
* Rend log() bavard
*/
log.enable = () => { disabled = false }
/**
* Active un filtre (le créé si besoin)
* @memberOf log
* @param {string} filter Le filtre à appliquer (pour exclure les messages qui le contiennent)
*/
log.exclude = (filter) => { exclusions[filter] = true }
/**
* Ajoute un message (avec éventuellement le dump d'un objet) dans le log d'erreur (config.logs.error)
* @name error
* @memberOf log
* @param message
* @param objectToDump
* @param filter
*/
log.error = function logError (message, objectToDump, filter) {
// on peut être utilisé comme callback
if (arguments.length === 0) return
if (typeof message === 'string' || message instanceof Error) {
out(message, objectToDump, filter, errorOutputStream, { max: 2000 })
} else {
// bizarre, on génère une vraie erreur avec sa trace
out(new Error(`log.error appelé sans message ni erreur, avec un type ${typeof message} :`), message, filter, errorOutputStream, { max: 2000 })
if (objectToDump) out('l’objet passé initialement', objectToDump, filter, errorOutputStream, { max: 2000 })
}
}
/**
* log.error si error, rien sinon
* @memberOf log
* @param {Error|string} [error]
*/
log.ifError = (error) => error && log.error(error)
/**
* Désactive un filtre
* @memberOf log
* @param {string} filter Le filtre à enlever (les messages qui le contiennent redeviennent actifs)
*/
log.include = (filter) => { exclusions[filter] = false }
// log.perf
if (config.logs.perf) {
const getElapsed = (start = 0) => Date.now() - start
perfOutputStream = getLogStream(config.logs.perf)
out('start log (démarrage appli)', null, null, perfOutputStream)
/**
* Ajoute une chaine avec un timer (depuis la réception de la requete) au message de perf courant
* S'active dans la conf (config.logs.perf), sinon ne fait rien
* @memberOf log
* @param response
* @param strToAdd
* @param {boolean} [noTimer=false] passer true pour ne pas ajouter la mesure de temps
*/
log.perf = function (response, strToAdd, noTimer) {
const timer = !noTimer
if (response.perf && response.perf.msg) {
response.perf.msg += '\t' + strToAdd
if (timer) response.perf.msg += ' ' + getElapsed(response.perf.start) + 'ms'
}
}
/**
* Clos la mesure de perf pour cette requête et écrit dans le log
* @param {Object} response L'objet response d'express, on traitera response.perf.msg si response.perf existe
*/
log.perf.out = function (response) {
if (response.perf) {
log.perf(response, 'end')
out((response.statusCode || '000') + '\t' + response.perf.msg, null, null, perfOutputStream)
}
}
} else {
log.perf = function () {}
}
/*
if (config.logs.sql) {
// pour que ça sorte qqchose, ajouter à node_modules/lassi/classes/entities/EntityQuery.js la ligne
// if (typeof log !== 'undefined' && log.sql) log.sql(query.toString(), query.args);
// juste avant l'appel de database.query
const sqlOutputStream = getLogStream(config.logs.sql)
log.sql = function (queryString, args) {
for (let i = 0; i < args.length; i++) {
queryString = queryString.replace('?', "'' +args[i] +''")
}
out(queryString, null, null, sqlOutputStream)
}
}
*/
// Et on fermera nos streams au shutdown
if (global.lassi) {
global.lassi.on('shutdown', function () {
streamsQuiet.forEach(stream => stream.end())
if (streamsVerbose.length) {
const msg = `${getPrefix()} log closed by pid ${process.pid} on shutdown\n`
streamsVerbose.forEach(stream => stream.end(msg))
}
})
}
module.exports = log