Update (for Winston 3.x)
I also created a gist for the following code:
const { format } = require('winston');
const { combine, colorize, timestamp, printf } = format;
/**
* /**
* Use CallSite to extract filename and number, for more info read: https://v8.dev/docs/stack-trace-api#customizing-stack-traces
* @param numberOfLinesToFetch - optional, when we want more than one line back from the stacktrace
* @returns {string|null} filename and line number separated by a colon, if numberOfLinesToFetch > 1 we'll return a string
* that represents multiple CallSites (representing the latest calls in the stacktrace)
*
*/
const getFileNameAndLineNumber = function getFileNameAndLineNumber (numberOfLinesToFetch = 1) {
const oldStackTrace = Error.prepareStackTrace;
const boilerplateLines = line => line &&
line.getFileName() &&
(line.getFileName().indexOf('<My Module Name>') &&
(line.getFileName().indexOf('/node_modules/') < 0));
try {
// eslint-disable-next-line handle-callback-err
Error.prepareStackTrace = (err, structuredStackTrace) => structuredStackTrace;
Error.captureStackTrace(this);
// we need to "peel" the first CallSites (frames) in order to get to the caller we're looking for
// in our case we're removing frames that come from logger module or from winston
const callSites = this.stack.filter(boilerplateLines);
if (callSites.length === 0) {
// bail gracefully: even though we shouldn't get here, we don't want to crash for a log print!
return null;
}
const results = [];
for (let i = 0; i < numberOfLinesToFetch; i++) {
const callSite = callSites[i];
let fileName = callSite.getFileName();
fileName = fileName.includes(BASE_DIR_NAME) ? fileName.substring(BASE_DIR_NAME.length + 1) : fileName;
results.push(fileName + ':' + callSite.getLineNumber());
}
return results.join('\n');
} finally {
Error.prepareStackTrace = oldStackTrace;
}
};
function humanReadableFormatter ({ level, message, ...metadata }) {
const filename = getFileNameAndLineNumber();
return `[${level}] [${filename}] ${message} ${JSON.stringify(metadata)}`;
}
const logger = winston.createLogger({
transports: [
new winston.transports.Console({
level: 'info',
handleExceptions: true,
humanReadableUnhandledException: true,
json: false,
colorize: { all: true },
stderrLevels: ['error', 'alert', 'critical'],
format: combine(
colorize(),
timestamp(),
humanReadableFormatter,
),
})
]
});
Original Answer (for Winston 2.x)
I'm using winston 2.x (but the same solution will work for winston 3.x) and that's the way I'm logging the filename and linenumber of the caller:
IMPORTANT: pay attention to the embedded code comments!
/**
* Use CallSite to extract filename and number
* @returns {string} filename and line number separated by a colon
*/
const getFileNameAndLineNumber = () => {
const oldStackTrace = Error.prepareStackTrace;
try {
// eslint-disable-next-line handle-callback-err
Error.prepareStackTrace = (err, structuredStackTrace) => structuredStackTrace;
Error.captureStackTrace(this);
// in this example I needed to "peel" the first 10 CallSites in order to get to the caller we're looking for, hence the magic number 11
// in your code, the number of stacks depends on the levels of abstractions you're using, which mainly depends on winston version!
// so I advise you to put a breakpoint here and see if you need to adjust the number!
return this.stack[11].getFileName() + ':' + this.stack[11].getLineNumber();
} finally {
Error.prepareStackTrace = oldStackTrace;
}
};
And (a simplified version of) the formatter function:
function humanReadableFormatter ({level, message}) {
const filename = getFileNameAndLineNumber();
return `[${level}] ${filename} ${message}`;
}
Then declare the transport to use the formatter:
new winston.transports.Console({
level: 'info',
handleExceptions: true,
humanReadableUnhandledException: true,
json: false,
colorize: 'level',
stderrLevels: ['warn', 'error', 'alert'],
formatter: humanReadableFormatter,
})
To read more about prepareStackTrace read: https://v8.dev/docs/stack-trace-api#customizing-stack-traces