get line number and filename for a log output

Viewed 20338

Is it possible to get the line number and file for each log output ?

For example:

  var winston = require('winston');

  winston.log('info', 'some message!'); // this is at line 4 of myfile.js

should specify in log file that 'some message' came from myFile.js line 4.

6 Answers

I found this code somewhere, yeah but It is working. Use it in a new winston.js and then requires that in any file.

var winston = require('winston')
var path = require('path')
var PROJECT_ROOT = path.join(__dirname, '..')
var appRoot = require('app-root-path');


const options = {
  file: {
    level: 'info',
    filename: `${appRoot}/logs/app.log`,
    handleExceptions: true,
    json: true,
    maxsize: 5242880, // 5MB
    maxFiles: 5,
    colorize: false,
    timestamp: true
  },
  console: {
    level: 'debug',
    handleExceptions: true,
    json: true,
    colorize: true,
    timestamp: true
  }
};

var logger = new winston.Logger({
  transports: [
    new winston.transports.File(options.file),
    new winston.transports.Console(options.console)
  ],
  exitOnError: false // do not exit on handled exceptions
});

logger.stream = {
  write: function (message) {
    logger.info(message)
  }
}

// A custom logger interface that wraps winston, making it easy to instrument
// code and still possible to replace winston in the future.

module.exports.debug = module.exports.log = function () {
  logger.debug.apply(logger, formatLogArguments(arguments))
}

module.exports.info = function () {
  logger.info.apply(logger, formatLogArguments(arguments))
}

module.exports.warn = function () {
  logger.warn.apply(logger, formatLogArguments(arguments))
}

module.exports.error = function () {
  logger.error.apply(logger, formatLogArguments(arguments))
}

module.exports.stream = logger.stream

/**
 * Attempts to add file and line number info to the given log arguments.
 */
function formatLogArguments (args) {
  args = Array.prototype.slice.call(args)

  var stackInfo = getStackInfo(1)

  if (stackInfo) {
    // get file path relative to project root
    var calleeStr = '(' + stackInfo.relativePath + ':' + stackInfo.line + ')'

    if (typeof (args[0]) === 'string') {
      args[0] = calleeStr + ' ' + args[0]
    } else {
      args.unshift(calleeStr)
    }
  }

  return args
}

/**
 * Parses and returns info about the call stack at the given index.
 */
function getStackInfo (stackIndex) {
  // get call stack, and analyze it
  // get all file, method, and line numbers
  var stacklist = (new Error()).stack.split('\n').slice(3)

  // stack trace format:
  // http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
  // do not remove the regex expresses to outside of this method (due to a BUG in node.js)
  var stackReg = /at\s+(.*)\s+\((.*):(\d*):(\d*)\)/gi
  var stackReg2 = /at\s+()(.*):(\d*):(\d*)/gi

  var s = stacklist[stackIndex] || stacklist[0]
  var sp = stackReg.exec(s) || stackReg2.exec(s)

  if (sp && sp.length === 5) {
    return {
      method: sp[1],
      relativePath: path.relative(PROJECT_ROOT, sp[2]),
      line: sp[3],
      pos: sp[4],
      file: path.basename(sp[2]),
      stack: stacklist.join('\n')
    }
  }
}

Source: https://gist.github.com/transitive-bullshit/39a7edc77c422cbf8a18

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', 'bizAlert'],
            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

You can do string operations on

console.log(new Error().stack.split('\n')[1].slice(7));

to get line Number and file path too along with function name.

The output would look like

AutomationFramework._this.parsingLogic (/Users/user_name/Desktop/something/some-file.js:49:25)

here is how I go around it

import winston from 'winston'
const {format } = winston
const { combine, timestamp, printf   } = format;

const myFormat = printf(({ level, message,timestamp , at }) => {

    let on = at.stack.split('\n')[1].slice(7).split('/').pop() ;
    let file = on.split(':')[0]
    let line = on.split(':')[1]
    let data = Date(timestamp).toString().split(' GMT')[0]
    
    return `${level}: [${data}] Hello <yourName>, there is a ${level} message on file:${file}  line ${line} 
      ${message}`;
  });


const logger = winston.createLogger({
  level: 'info',
  format: combine(
    timestamp(),
    myFormat ,
  ),
  transports: [new winston.transports.Console()],
});

on loggin the output will be like

logger.info('wrong file type' , {at : new Error})
//logs
warn: [Thu Aug 18 2022 15:06:28] Hello <yourName>, there is a warn message on file:winston.js  line 7
      >>wrong file type
Related