18

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.

Lev
  • 13,856
  • 14
  • 52
  • 84
  • This question is very similar to what you are hoping to accomplish, unless you're hoping to log explicitly with winston. https://stackoverflow.com/questions/14172455/get-name-and-line-of-calling-function-in-node-js – daviscodesbugs Jul 06 '17 at 15:22

7 Answers7

18

You can pass the file name as label and you can get the file name from callingModule.

Create logger.js file and code like

var winston = require('winston');
var getLabel = function (callingModule) {
    var parts = callingModule.filename.split('/');
    return parts[parts.length - 2] + '/' + parts.pop();
};

module.exports = function (callingModule) {
    return new winston.Logger({
        transports: [
            new winston.transports.Console({
                label: getLabel(callingModule),
                json: false,
                timestamp: true,
                depth:true,
                colorize:true
            })
        ]
    });
};

Now Here your test file

var logger = require('./logger')(module);
function test() {
    logger.info('test logger');
}
test();

and if you run test file than the output looks like

2017-07-08T07:15:20.671Z - info: [utils/test.js] test logger
Nitin
  • 2,701
  • 2
  • 30
  • 60
  • i am passing the module but the problem is that i am not able to get the filename as undefined. – Learner Dec 11 '18 at 04:36
  • You can check here, https://stackoverflow.com/questions/53702679/getting-undefined-for-the-filename-even-though-file-name-is-passed – Learner Dec 11 '18 at 04:41
  • also how can we use the morgan , here. All the solutions shows without morgan , how can we use the morgan . I have raised a question for it. Do you have any idea on this https://stackoverflow.com/questions/53718192/stream-write-is-not-a-function-when-using-morgan-with-logger – Learner Dec 11 '18 at 06:15
  • use path.sep instead of using '/' to split the path. const path = require('path'); – Avi Jan 15 '20 at 09:04
  • I've used this in a large project with strapi...we end up getting a maxEventEmitters warning after a point. There's possibly a memory leak here. – amitavk May 07 '21 at 15:28
  • looks like we are create new Logger Instance for every file with `new winston.Logger` – vanduc1102 Jul 02 '21 at 06:04
  • Yes I also faced maxEventEmitters warning in large project because it is creating new logger instance for each file which is very bad. do you have any better solution? – Vimox Shah Jan 13 '22 at 10:34
4

winston didn't have the plan to do that due to performance concerns. Please check here for detailed information.

I tried https://github.com/baryon/tracer but it isn't good, e.g. line number is incorrect from time to time.

Qiulang
  • 10,295
  • 11
  • 80
  • 129
4

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

Nir Alfasi
  • 53,191
  • 11
  • 86
  • 129
  • I'm getting this is not available in typescript because it's a function – xenoterracide Mar 05 '20 at 17:19
  • 1
    @xenoterracide making it work with TS is a different issue. Feel free to post a separate question and provide more information so that it's reproducible. I'm less proficient with TS but if you'll tag me I'll take a look. – Nir Alfasi Mar 06 '20 at 10:24
  • Have you checked the functionality of your code? There are mistakes in it. `const boilerplateLines = line => line && line.getFileName() &&(line.getFileName().indexOf('') && (line.getFileName().indexOf('/node_modules/') < 0); ` – Alex May 13 '21 at 07:56
  • @Alex maybe it's confusing but this is an arrow-function. This code works in production for a few years now :) that said, if you think you've found a mistake please help by pinpointing what is it exactly! – Nir Alfasi May 13 '21 at 11:47
  • @NirAlfasi `.indexOf('/node_modules/') < 0));` Missing `)` – Alex May 13 '21 at 12:55
  • @Alex good catch, probably missed it in the copy&paste - thank you!!! – Nir Alfasi May 13 '21 at 16:49
  • Hm the solution looks really nice but i dont think it can ever be portable. After i got it working by solving the "did you create a format instance" issue, i was not able to find a way to determine the "file" of interest. For quick and dirty i now use this.stack[this.stack.length -4].getFileName() but it mostly returns bogus, e.g. the logger itself or similar. Looks like, depending on where i log from, the filename of interest will usually have a different index. Anyway, nice solution, thanks! – Harry Mar 19 '23 at 00:26
3

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

M--
  • 25,431
  • 8
  • 61
  • 93
Rupesh
  • 850
  • 2
  • 13
  • 30
1

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)
1

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
Seven
  • 21
  • 4
0

result like

2023-03-04 21:19:54.023 | info | D:\dev\cosmos\e2e\index.spec.js:7:8 | test message with date parameter 2023-03-09T21:19:54.021Z
2023-03-04 21:19:54.040 | info | D:\dev\cosmos\e2e\index.spec.js:68:12 | Authenticating with Salesforce...
2023-03-04 21:19:54.042 | info | D:\dev\cosmos\e2e\sfAuth.js:21:10 | Getting credentials from parameter store
2023-03-04 21:19:54.569 | info | D:\dev\cosmos\e2e\sfAuth.js:38:10 | Getting access token for Salesforce
2023-03-04 21:19:56.691 | info | D:\dev\cosmos\e2e\index.spec.js:78:12 | Creating test subscriptions in Salesforce...
2023-03-04 21:19:58.287 | info | D:\dev\cosmos\e2e\index.spec.js:91:12 | Retrieving created subs...
2023-03-04 21:19:58.733 | error | D:\dev\cosmos\e2e\index.spec.js:96:30 | Error: Request failed with status code 400
    at createError (D:\dev\cosmos\e2e\node_modules\axios\lib\core\createError.js:16:15)
    at settle (D:\dev\cosmos\e2e\node_modules\axios\lib\core\settle.js:17:12)
    at IncomingMessage.handleStreamEnd (D:\dev\cosmos\e2e\node_modules\axios\lib\adapters\http.js:293:11)
    at IncomingMessage.emit (node:events:525:35)
    at IncomingMessage.emit (node:domain:489:12)
    at endReadableNT (node:internal/streams/readable:1359:12)
    at processTicksAndRejections (node:internal/process/task_queues:82:21)

usage

const logger = require('./logger.js');

logger.info("test message with date parameter %s", new Date());
...
logger.error(e.stack)

logger.js

const winston = require('winston');
const { createLogger, format, transports } = winston;

function getCaller() {
    var err;
    try { throw Error('') } catch (e) { err = e; }
    var pattern = /\s*at (Object.)?(silly|debug|verbose|info|warn|error) /;
    var callerLine = err.stack.split("\n").filter(line => pattern.test(line))[0];
    return callerLine.replace(pattern, '').replace(/^\(|\)$/g, '');
}

const logConfig = {
    level: 'debug',
    format: winston.format.combine(
        winston.format.timestamp({ format: 'YYYY-MM-dd HH:mm:ss.SSS' }),
        winston.format.printf((log) => `${[log.timestamp]} | ${log.level} | ${getCaller()} | ${log.message}`)
    )
};

const logger = createLogger({
    format: format.combine(
        format.splat(),
        format.simple()
    ),
    transports: [
        new winston.transports.Console(logConfig)
    ],
    exitOnError: false
});

module.exports = logger;
Mike
  • 20,010
  • 25
  • 97
  • 140