log4js-node/lib/layouts.js
Patrick Hogan 6b029e98fc Allow for blank tokens due to dynamic data
Metadata for users such as name, email, etc are not always present for users.  For example, I am running express and I want to log the %x{company}%x{username} so that when I look at my logs I can immediately understand which user this affects.  Or for example, I could log %x{payingOrTial} the type of user.  

This works well when the user is logged in.  However, my logger encompasses everything.  I log when the server boots up.  I log during the login screen where a user is not yet logged in.  In these circumstances there is no way to retrieve this metadata.

So for example

```
"username": function () {
            var session = require('continuation-local-storage').getNamespace('api.callinize');
            if(!session) session = require('continuation-local-storage').getNamespace('dashboard.callinize');
            var username = session && session.get('user') && session.get('user').username;
            if(!username) return "";
            return " " + username + " ";
        }
```

I try to get the metadata.  If I get no metdata I return a blank string.  Unfortunately, in the current implementation, due to the OR operator, even if I have a replacement of "" || matchedString, 

```
  replaceToken(conversionCharacter, loggingEvent, specifier) || 
          matchedString;
```

the blank string equals false and puts the token in the log instead of the blank string.  This makes the log lines get long with information that is not relevant.  The better thing to do is simply allow for blank strings.  This lets the user have control over their logs and also allows for more metadata to go in the logs, without having to pick only metadata that is always present.
2014-12-21 14:49:25 -08:00

323 lines
8.5 KiB
JavaScript

"use strict";
var dateFormat = require('./date_format')
, os = require('os')
, eol = os.EOL || '\n'
, util = require('util')
, replacementRegExp = /%[sdj]/g
, layoutMakers = {
"messagePassThrough": function() { return messagePassThroughLayout; },
"basic": function() { return basicLayout; },
"colored": function() { return colouredLayout; },
"coloured": function() { return colouredLayout; },
"pattern": function (config) {
return patternLayout(config && config.pattern, config && config.tokens);
}
}
, colours = {
ALL: "grey",
TRACE: "blue",
DEBUG: "cyan",
INFO: "green",
WARN: "yellow",
ERROR: "red",
FATAL: "magenta",
OFF: "grey"
};
function wrapErrorsWithInspect(items) {
return items.map(function(item) {
if ((item instanceof Error) && item.stack) {
return { inspect: function() { return util.format(item) + '\n' + item.stack; } };
} else {
return item;
}
});
}
function formatLogData(logData) {
var data = Array.isArray(logData) ? logData : Array.prototype.slice.call(arguments);
return util.format.apply(util, wrapErrorsWithInspect(data));
}
var 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' : [31, 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) {
var output = colorize(
formatLogData(
'[%s] [%s] %s - '
, dateFormat.asString(loggingEvent.startTime)
, loggingEvent.level
, loggingEvent.categoryName
)
, colour
);
return output;
}
/**
* BasicLayout is a simple layout for storing the logs. The logs are stored
* in following format:
* <pre>
* [startTime] [logLevel] categoryName - message\n
* </pre>
*
* @author Stephan Strittmatter
*/
function basicLayout (loggingEvent) {
return timestampLevelAndCategory(loggingEvent) + formatLogData(loggingEvent.data);
}
/**
* colouredLayout - taken from masylum's fork.
* same as basicLayout, but with colours.
*/
function colouredLayout (loggingEvent) {
return timestampLevelAndCategory(
loggingEvent,
colours[loggingEvent.level.toString()]
) + formatLogData(loggingEvent.data);
}
function messagePassThroughLayout (loggingEvent) {
return formatLogData(loggingEvent.data);
}
/**
* 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
* Fields can be any of:
* - %r time in toLocaleTimeString format
* - %p log level
* - %c log category
* - %h hostname
* - %m log data
* - %d date in various formats
* - %% %
* - %n newline
* - %z pid
* - %x{<tokenname>} add dynamic tokens to your log. Tokens are specified in the tokens parameter
* 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.
* @param {String} Log format pattern String
* @param {object} map object of different tokens
* @return {Function}
* @author Stephan Strittmatter
* @author Jan Schmidle
*/
function patternLayout (pattern, tokens) {
var TTCC_CONVERSION_PATTERN = "%r %p %c - %m%n";
var regex = /%(-?[0-9]+)?(\.?[0-9]+)?([\[\]cdhmnprzx%])(\{([^\}]+)\})?|([^%]+)/;
pattern = pattern || TTCC_CONVERSION_PATTERN;
function categoryName(loggingEvent, specifier) {
var loggerName = loggingEvent.categoryName;
if (specifier) {
var precision = parseInt(specifier, 10);
var loggerNameBits = loggerName.split(".");
if (precision < loggerNameBits.length) {
loggerName = loggerNameBits.slice(loggerNameBits.length - precision).join(".");
}
}
return loggerName;
}
function formatAsDate(loggingEvent, specifier) {
var format = dateFormat.ISO8601_FORMAT;
if (specifier) {
format = specifier;
// Pick up special cases
if (format == "ISO8601") {
format = dateFormat.ISO8601_FORMAT;
} else if (format == "ISO8601_WITH_TZ_OFFSET") {
format = dateFormat.ISO8601_WITH_TZ_OFFSET_FORMAT;
} else if (format == "ABSOLUTE") {
format = dateFormat.ABSOLUTETIME_FORMAT;
} else if (format == "DATE") {
format = dateFormat.DATETIME_FORMAT;
}
}
// Format the date
return dateFormat.asString(format, loggingEvent.startTime);
}
function hostname() {
return os.hostname().toString();
}
function formatMessage(loggingEvent) {
return formatLogData(loggingEvent.data);
}
function endOfLine() {
return eol;
}
function logLevel(loggingEvent) {
return loggingEvent.level.toString();
}
function startTime(loggingEvent) {
return "" + loggingEvent.startTime.toLocaleTimeString();
}
function startColour(loggingEvent) {
return colorizeStart(colours[loggingEvent.level.toString()]);
}
function endColour(loggingEvent) {
return colorizeEnd(colours[loggingEvent.level.toString()]);
}
function percent() {
return '%';
}
function pid() {
return process.pid;
}
function userDefined(loggingEvent, specifier) {
if (typeof(tokens[specifier]) !== 'undefined') {
if (typeof(tokens[specifier]) === 'function') {
return tokens[specifier](loggingEvent);
} else {
return tokens[specifier];
}
}
return null;
}
var replacers = {
'c': categoryName,
'd': formatAsDate,
'h': hostname,
'm': formatMessage,
'n': endOfLine,
'p': logLevel,
'r': startTime,
'[': startColour,
']': endColour,
'z': pid,
'%': percent,
'x': userDefined
};
function replaceToken(conversionCharacter, loggingEvent, specifier) {
return replacers[conversionCharacter](loggingEvent, specifier);
}
function truncate(truncation, toTruncate) {
var len;
if (truncation) {
len = parseInt(truncation.substr(1), 10);
return toTruncate.substring(0, len);
}
return toTruncate;
}
function pad(padding, toPad) {
var len;
if (padding) {
if (padding.charAt(0) == "-") {
len = parseInt(padding.substr(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;
}
return function(loggingEvent) {
var formattedString = "";
var result;
var searchString = pattern;
while ((result = regex.exec(searchString))) {
var matchedString = result[0];
var padding = result[1];
var truncation = result[2];
var conversionCharacter = result[3];
var specifier = result[5];
var text = result[6];
// Check if the pattern matched was just normal text
if (text) {
formattedString += "" + text;
} else {
// Create a raw replacement string based on the conversion
// character and specifier
var replacement = replaceToken(conversionCharacter, loggingEvent, specifier);
// Format the replacement according to any padding or
// truncation specified
replacement = truncate(truncation, replacement);
replacement = pad(padding, replacement);
formattedString += replacement;
}
searchString = searchString.substr(result.index + result[0].length);
}
return formattedString;
};
}
module.exports = {
basicLayout: basicLayout,
messagePassThroughLayout: messagePassThroughLayout,
patternLayout: patternLayout,
colouredLayout: colouredLayout,
coloredLayout: colouredLayout,
layout: function(name, config) {
return layoutMakers[name] && layoutMakers[name](config);
}
};