@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
215 lines (187 loc) • 8.02 kB
JavaScript
const log = module.exports = exports = cds_log
const cds = require('../index'), defaults = { format: 'plain', levels: {} }
let conf = Object.hasOwn(cds,'env') ? cds.env.log : defaults
/* eslint-disable no-console */
/**
* Cache used for all constructed loggers.
*/
exports.loggers = {}
/**
* Returns a trace logger for the given module if trace is switched on for it,
* otherwise returns null. All cds runtime packages use this method for their
* trace and debug output. It can also be used in applications like that:
*
* const LOG = cds.log('sql')
* LOG._info && LOG.info ('whatever', you, 'like...')
*
* You can also specify alternate module names:
*
* const LOG = cds.log('sql|db')
*
* By default this logger would prefix all output with '[sql] - '.
* You can change this by specifying another prefix in the options:
*
* const LOG = cds.log('sql|db',{ prefix:'cds.ql' })
*
* Call cds.log() for a given module again to dynamically change the log level
* of all formerly created loggers, for example:
*
* const LOG = cds.log('sql')
* LOG.info ('this will show, as default level is info')
* cds.log('sql','warn')
* LOG.info ('this will be suppressed now')
*
* Tracing can be switched on/off through env variable DEBUG:
* Set it to a comma-separated list of modules to switch on tracing.
* Set it to 'all' or 'y' to switch on tracing for all modules.
*
* @param {string} [module] the module for which a logger is requested
* @param {string|number|{ level, prefix }} [options] the log level to enable -> 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
*/
function cds_log (module, options) { // NOSONAR
const id = module?.match(/^[^|]+/)[0] || 'cds', cache = log.loggers
const cached = cache[id]
if (cached && options == undefined) return cached // Note: SILENT = 0, so falsy check on option would be wrong
let label = options?.label || options?.prefix || cached?.label || id
let level = typeof options === 'object' ? options.level : options
if (level == undefined) level = DEBUG_matches(module) ? DEBUG : conf.levels[id] || INFO
if (typeof level === 'string') level = log.levels [level.toUpperCase()]
if (cached && cached.level === level) return cached
const logger = new Logger (label, level)
return cache[id] = Object.assign (cached || logger.log, {
id, label, level, setFormat(fn){ logger.format = fn; return this },
_trace: level >= TRACE,
_debug: level >= DEBUG,
_info: level >= INFO,
_warn: level >= WARN,
_error: level >= ERROR,
}, logger)
}
/**
* Shortcut to `cds.log(...).debug`, returning undefined if `cds.log(...)._debug` is false.
* @param {string} [module] the module for which a logger is requested
* @param {object} [options] logger options
*/
exports.debug = function cds_debug (id, options) {
const L = cds_log (id, options)
return Object.assign((..._) => L._debug && L.debug (..._), {
time: label => L._debug && console.time (`[${id}] - ${label}`),
timeEnd: label => L._debug && console.timeEnd (`[${id}] - ${label}`),
})
}
/**
* Constructs a new Logger with the method signature of `{ debug, log, info, warn, error }`
* from console. The default implementation actually maps it to `global.console`.
* You can assign different implementations, e.g. to integrate with advanced
* logging frameworks, for example like that:
*
* cds.log.Logger = () => winston.createLogger (...)
*
* @param {string} [label] the module for which a logger is requested
* @param {number} [level] the log level to enable -> 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
*/
exports.Logger = (label, level) => {
const fmt = (level,args) => logger.format (label,level,...args)
const logger = {
format: exports.format, // use logger.format as this could be changed dynamically
trace: level < DEBUG ? ()=>{} : (...args) => console.trace (...fmt(TRACE,args)),
debug: level < DEBUG ? ()=>{} : (...args) => console.debug (...fmt(DEBUG,args)),
log: level < INFO ? ()=>{} : (...args) => console.log (...fmt(INFO,args)),
info: level < INFO ? ()=>{} : (...args) => console.info (...fmt(INFO,args)),
warn: level < WARN ? ()=>{} : (...args) => console.warn (...fmt(WARN,args)),
error: level < ERROR ? ()=>{} : (...args) => console.error (...fmt(ERROR,args)),
}
return logger
}
function Logger (label, level) { return exports.Logger (label, level) }
/**
* Convenience method to construct winston loggers, very similar to `winston.createLogger()`.
* @param {object} options - as in `winston.createLogger()`
* @returns The winston logger, decorated with the standard cds.log methods
* .debug(), .info(), .warn(), .error(), etc.
*/
exports.winstonLogger = (options) => (label, level) => {
const winston = require("winston")
const logger = winston.createLogger({
levels: log.levels, level: Object.keys(log.levels)[level],
transports: [new winston.transports.Console()],
...options
})
const { formatWithOptions } = require('util')
const _fmt = ([...args]) => formatWithOptions(
{ colors: false }, `[${label}] -`, ...args
)
return Object.assign (logger, {
trace: (...args) => logger.TRACE (_fmt(args)),
debug: (...args) => logger.DEBUG (_fmt(args)),
log: (...args) => logger.INFO (_fmt(args)),
info: (...args) => logger.INFO (_fmt(args)),
warn: (...args) => logger.WARN (_fmt(args)),
error: (...args) => logger.ERROR (_fmt(args)),
})
}
/**
* Built-in formatters
*/
exports.formatters = new class {
plain (label, level, ...args) { return [ `[${label}] -`, ...args ] }
get json() { return super.json = require('./format/json') }
}
/**
* Formats log outputs by returning an array of arguments which are passed to
* console.log() et al.
* You can assign custom formatters like that:
*
* cds.log.format = (label, level, ...args) => [ '[', label, ']', ...args ]
*
* @param {string} label the label to prefix to log output
* @param {number} level the log level to enable -> 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
* @param {any[]} args the arguments passed to Logger.debug|log|info|warn|error()
*/
exports.format = log.formatters[conf.format || 'plain']
const DEBUG_matches = (m) => process.env.DEBUG?.match(RegExp(`\\b(y|all|${m||'any'})\\b`))
const { ERROR, WARN, INFO, DEBUG, TRACE } = exports.levels = {
SILENT:0, ERROR:1, WARN:2, INFO:3, DEBUG:4, TRACE:5, SILLY:5, VERBOSE:5
}
// If cds.env is not yet loaded, update the loggers when it is
if (conf === defaults) cds.once ('env', ()=>{
let { loggers, format } = exports, fmt = format
let { plain } = log.formatters
conf = cds.env.log
// Update log levels for already existing loggers
if (conf.levels) {
for (let each in conf.levels) cds_log (each, conf.levels[each])
}
// User configured formatter, if any
if (conf.format) {
fmt = exports.format = log.formatters [conf.format]
}
// Include tenant in plain log output if multitenancy is enabled
if (fmt === plain && cds.env.requires.multitenancy) {
fmt = exports.format = (label, level, ...args) => {
const t = cds.context?.tenant; if (t) label += '|'+t
return plain (label, level, ...args)
}
}
// If formatter got changed above, propagate to all loggers
if (fmt !== format) {
for (let each in loggers) loggers[each] .setFormat (fmt)
}
_init()
}); else _init()
function _init() {
if (conf.Logger) {
const path = require('path')
let resolvedPath
try { resolvedPath = require.resolve(conf.Logger) } catch {
try { resolvedPath = require.resolve(path.join(cds.root, conf.Logger)) } catch {
throw new Error(`Cannot find logger at "${conf.Logger}"`)
}
}
exports.Logger = require (resolvedPath) // Use configured logger in case of cds serve
}
if (conf.service) {
const {app} = cds, serveIn = app => require('./service').serveIn(app)
app ? setImmediate(() => serveIn(app)) : cds.on('bootstrap', app => serveIn(app))
}
}