const dateFormat = require('date-format'); const os = require('os'); const util = require('util'); const path = require('path'); const url = require('url'); const debug = require('debug')('log4js:layouts'); const styles = { // styles bold: [1, 22], italic: [3, 23], underline: [4, 24], inverse: [7, 27], // grayscale white: [37, 39], grey: [90, 39], black: [90, 39], // colors blue: [34, 39], cyan: [36, 39], green: [32, 39], magenta: [35, 39], red: [91, 39], yellow: [33, 39], }; function colorizeStart(style) { return style ? `\x1B[${styles[style][0]}m` : ''; } function colorizeEnd(style) { return style ? `\x1B[${styles[style][1]}m` : ''; } /** * Taken from masylum's fork (https://github.com/masylum/log4js-node) */ function colorize(str, style) { return colorizeStart(style) + str + colorizeEnd(style); } function timestampLevelAndCategory(loggingEvent, colour) { return colorize( util.format( '[%s] [%s] %s - ', dateFormat.asString(loggingEvent.startTime), loggingEvent.level.toString(), loggingEvent.categoryName ), colour ); } /** * BasicLayout is a simple layout for storing the logs. The logs are stored * in following format: *
 * [startTime] [logLevel] categoryName - message\n
 * 
* * @author Stephan Strittmatter */ function basicLayout(loggingEvent) { return ( timestampLevelAndCategory(loggingEvent) + util.format(...loggingEvent.data) ); } /** * colouredLayout - taken from masylum's fork. * same as basicLayout, but with colours. */ function colouredLayout(loggingEvent) { return ( timestampLevelAndCategory(loggingEvent, loggingEvent.level.colour) + util.format(...loggingEvent.data) ); } function messagePassThroughLayout(loggingEvent) { return util.format(...loggingEvent.data); } function dummyLayout(loggingEvent) { return loggingEvent.data[0]; } /** * PatternLayout * Format for specifiers is %[padding].[truncation][field]{[format]} * e.g. %5.10p - left pad the log level by 5 characters, up to a max of 10 * both padding and truncation can be negative. * Negative truncation = trunc from end of string * Positive truncation = trunc from start of string * Negative padding = pad right * Positive padding = pad left * * Fields can be any of: * - %r time in toLocaleTimeString format * - %p log level * - %c log category * - %h hostname * - %m log data * - %m{l} where l is an integer, log data.slice(l) * - %m{l,u} where l and u are integers, log data.slice(l, u) * - %d date in constious formats * - %% % * - %n newline * - %z pid * - %f filename * - %l line number * - %o column postion * - %s call stack * - %C class name [#1316](https://github.com/log4js-node/log4js-node/pull/1316) * - %M method or function name [#1316](https://github.com/log4js-node/log4js-node/pull/1316) * - %A method or function alias [#1316](https://github.com/log4js-node/log4js-node/pull/1316) * - %F fully qualified caller name [#1316](https://github.com/log4js-node/log4js-node/pull/1316) * - %x{} add dynamic tokens to your log. Tokens are specified in the tokens parameter * - %X{} add dynamic tokens to your log. Tokens are specified in logger context * You can use %[ and %] to define a colored block. * * Tokens are specified as simple key:value objects. * The key represents the token name whereas the value can be a string or function * which is called to extract the value to put in the log message. If token is not * found, it doesn't replace the field. * * A sample token would be: { 'pid' : function() { return process.pid; } } * * Takes a pattern string, array of tokens and returns a layout function. * @return {Function} * @param pattern * @param tokens * @param timezoneOffset * * @authors ['Stephan Strittmatter', 'Jan Schmidle'] */ function patternLayout(pattern, tokens) { const TTCC_CONVERSION_PATTERN = '%r %p %c - %m%n'; const regex = /%(-?[0-9]+)?(\.?-?[0-9]+)?([[\]cdhmnprzxXyflosCMAF%])(\{([^}]+)\})?|([^%]+)/; pattern = pattern || TTCC_CONVERSION_PATTERN; function categoryName(loggingEvent, specifier) { let loggerName = loggingEvent.categoryName; if (specifier) { const precision = parseInt(specifier, 10); const loggerNameBits = loggerName.split('.'); if (precision < loggerNameBits.length) { loggerName = loggerNameBits .slice(loggerNameBits.length - precision) .join('.'); } } return loggerName; } function formatAsDate(loggingEvent, specifier) { let format = dateFormat.ISO8601_FORMAT; if (specifier) { format = specifier; // Pick up special cases switch (format) { case 'ISO8601': case 'ISO8601_FORMAT': format = dateFormat.ISO8601_FORMAT; break; case 'ISO8601_WITH_TZ_OFFSET': case 'ISO8601_WITH_TZ_OFFSET_FORMAT': format = dateFormat.ISO8601_WITH_TZ_OFFSET_FORMAT; break; case 'ABSOLUTE': process.emitWarning( 'Pattern %d{ABSOLUTE} is deprecated in favor of %d{ABSOLUTETIME}. ' + 'Please use %d{ABSOLUTETIME} instead.', 'DeprecationWarning', 'log4js-node-DEP0003' ); debug( '[log4js-node-DEP0003]', 'DEPRECATION: Pattern %d{ABSOLUTE} is deprecated and replaced by %d{ABSOLUTETIME}.' ); // falls through case 'ABSOLUTETIME': case 'ABSOLUTETIME_FORMAT': format = dateFormat.ABSOLUTETIME_FORMAT; break; case 'DATE': process.emitWarning( 'Pattern %d{DATE} is deprecated due to the confusion it causes when used. ' + 'Please use %d{DATETIME} instead.', 'DeprecationWarning', 'log4js-node-DEP0004' ); debug( '[log4js-node-DEP0004]', 'DEPRECATION: Pattern %d{DATE} is deprecated and replaced by %d{DATETIME}.' ); // falls through case 'DATETIME': case 'DATETIME_FORMAT': format = dateFormat.DATETIME_FORMAT; break; // no default } } // Format the date return dateFormat.asString(format, loggingEvent.startTime); } function hostname() { return os.hostname().toString(); } function formatMessage(loggingEvent, specifier) { let dataSlice = loggingEvent.data; if (specifier) { const [lowerBound, upperBound] = specifier.split(','); dataSlice = dataSlice.slice(lowerBound, upperBound); } return util.format(...dataSlice); } function endOfLine() { return os.EOL; } function logLevel(loggingEvent) { return loggingEvent.level.toString(); } function startTime(loggingEvent) { return dateFormat.asString('hh:mm:ss', loggingEvent.startTime); } function startColour(loggingEvent) { return colorizeStart(loggingEvent.level.colour); } function endColour(loggingEvent) { return colorizeEnd(loggingEvent.level.colour); } function percent() { return '%'; } function pid(loggingEvent) { return loggingEvent && loggingEvent.pid ? loggingEvent.pid.toString() : process.pid.toString(); } function clusterInfo() { // this used to try to return the master and worker pids, // but it would never have worked because master pid is not available to workers // leaving this here to maintain compatibility for patterns return pid(); } function userDefined(loggingEvent, specifier) { if (typeof tokens[specifier] !== 'undefined') { return typeof tokens[specifier] === 'function' ? tokens[specifier](loggingEvent) : tokens[specifier]; } return null; } function contextDefined(loggingEvent, specifier) { const resolver = loggingEvent.context[specifier]; if (typeof resolver !== 'undefined') { return typeof resolver === 'function' ? resolver(loggingEvent) : resolver; } return null; } function fileName(loggingEvent, specifier) { let filename = loggingEvent.fileName || ''; // support for ESM as it uses url instead of path for file /* istanbul ignore next: unsure how to simulate ESM for test coverage */ const convertFileURLToPath = function (filepath) { const urlPrefix = 'file://'; if (filepath.startsWith(urlPrefix)) { // https://nodejs.org/api/url.html#urlfileurltopathurl if (typeof url.fileURLToPath === 'function') { filepath = url.fileURLToPath(filepath); } // backward-compatible for nodejs pre-10.12.0 (without url.fileURLToPath method) else { // posix: file:///hello/world/foo.txt -> /hello/world/foo.txt -> /hello/world/foo.txt // win32: file:///C:/path/foo.txt -> /C:/path/foo.txt -> \C:\path\foo.txt -> C:\path\foo.txt // win32: file://nas/foo.txt -> //nas/foo.txt -> nas\foo.txt -> \\nas\foo.txt filepath = path.normalize( filepath.replace(new RegExp(`^${urlPrefix}`), '') ); if (process.platform === 'win32') { if (filepath.startsWith('\\')) { filepath = filepath.slice(1); } else { filepath = path.sep + path.sep + filepath; } } } } return filepath; }; filename = convertFileURLToPath(filename); if (specifier) { const fileDepth = parseInt(specifier, 10); const fileList = filename.split(path.sep); if (fileList.length > fileDepth) { filename = fileList.slice(-fileDepth).join(path.sep); } } return filename; } function lineNumber(loggingEvent) { return loggingEvent.lineNumber ? `${loggingEvent.lineNumber}` : ''; } function columnNumber(loggingEvent) { return loggingEvent.columnNumber ? `${loggingEvent.columnNumber}` : ''; } function callStack(loggingEvent) { return loggingEvent.callStack || ''; } function className(loggingEvent) { return loggingEvent.className || ''; } function functionName(loggingEvent) { return loggingEvent.functionName || ''; } function functionAlias(loggingEvent) { return loggingEvent.functionAlias || ''; } function callerName(loggingEvent) { return loggingEvent.callerName || ''; } const replacers = { c: categoryName, d: formatAsDate, h: hostname, m: formatMessage, n: endOfLine, p: logLevel, r: startTime, '[': startColour, ']': endColour, y: clusterInfo, z: pid, '%': percent, x: userDefined, X: contextDefined, f: fileName, l: lineNumber, o: columnNumber, s: callStack, C: className, M: functionName, A: functionAlias, F: callerName, }; function replaceToken(conversionCharacter, loggingEvent, specifier) { return replacers[conversionCharacter](loggingEvent, specifier); } function truncate(truncation, toTruncate) { let len; if (truncation) { len = parseInt(truncation.slice(1), 10); // negative truncate length means truncate from end of string return len > 0 ? toTruncate.slice(0, len) : toTruncate.slice(len); } return toTruncate; } function pad(padding, toPad) { let len; if (padding) { if (padding.charAt(0) === '-') { len = parseInt(padding.slice(1), 10); // Right pad with spaces while (toPad.length < len) { toPad += ' '; } } else { len = parseInt(padding, 10); // Left pad with spaces while (toPad.length < len) { toPad = ` ${toPad}`; } } } return toPad; } function truncateAndPad(toTruncAndPad, truncation, padding) { let replacement = toTruncAndPad; replacement = truncate(truncation, replacement); replacement = pad(padding, replacement); return replacement; } return function (loggingEvent) { let formattedString = ''; let result; let searchString = pattern; while ((result = regex.exec(searchString)) !== null) { // const matchedString = result[0]; const padding = result[1]; const truncation = result[2]; const conversionCharacter = result[3]; const specifier = result[5]; const text = result[6]; // Check if the pattern matched was just normal text if (text) { formattedString += text.toString(); } else { // Create a raw replacement string based on the conversion // character and specifier const replacement = replaceToken( conversionCharacter, loggingEvent, specifier ); formattedString += truncateAndPad(replacement, truncation, padding); } searchString = searchString.slice(result.index + result[0].length); } return formattedString; }; } const layoutMakers = { messagePassThrough() { return messagePassThroughLayout; }, basic() { return basicLayout; }, colored() { return colouredLayout; }, coloured() { return colouredLayout; }, pattern(config) { return patternLayout(config && config.pattern, config && config.tokens); }, dummy() { return dummyLayout; }, }; module.exports = { basicLayout, messagePassThroughLayout, patternLayout, colouredLayout, coloredLayout: colouredLayout, dummyLayout, addLayout(name, serializerGenerator) { layoutMakers[name] = serializerGenerator; }, layout(name, config) { return layoutMakers[name] && layoutMakers[name](config); }, };