Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overwriting sails logger does not work as expected #2695

Closed
felixmc opened this issue Feb 26, 2015 · 15 comments
Closed

Overwriting sails logger does not work as expected #2695

felixmc opened this issue Feb 26, 2015 · 15 comments

Comments

@felixmc
Copy link

felixmc commented Feb 26, 2015

This is for Sails v0.10.5

I wanted to overwrite the default Captain's Log logger in my Sails application to an instance of Winston from an internal library my company uses in our node.js apps.

I was unable to find anything specifically about overwriting the logger in the official Sails docs pertaining to logging (let me know if I missed something):
http://sailsjs.org/#!/documentation/concepts/Logging
http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.log.html
http://sailsjs.org/#!/documentation/anatomy/myApp/config/log.js.html

However, the following Stack Overflow answer claims that I can use the "log.custom" config property with an instance of Winston to overwrite the default logger:
http://stackoverflow.com/questions/25211183/sails-js-0-10-x-log-to-file

This correlates with the official Captain's Log documentation which states that a "custom" property with an instance of Winston or another logger can be passed to the Captain's Log constructor.
Reference: https://github.com/balderdashy/captains-log/blob/master/README.md#configuring-a-custom-logger

And so I tried the following in my app.js:

var logger = myLib.logger;

logger.log('test'); // works
logger.debug('test'); // works

sails.lift({
    log: {
        custom: logger
    }
}, function(err, sails) {
    sails.log('testing');
});

But got the following error:

error: uncaughtException: Unsupported logger override!
(has no `.log()` or `.debug()` method.) date=Thu Feb 26 2015 15:10:38 GMT-0700 (MST), pid=25243, uid=0, gid=0, cwd=/home/felix/Development/work/repiscore/repiscore-web-app, execPath=/usr/bin/nodejs, version=v0.10.29, argv=[node, /home/felix/Development/work/repiscore/repiscore-web-app/app], rss=27459584, heapTotal=18635008, heapUsed=9721784, loadavg=[1.13427734375, 0.8935546875, 0.97607421875], uptime=217524.947277618, trace=[column=10, file=/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/captains-log/index.js, function=new CaptainsLog, line=53, method=null, native=false, column=25, file=/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/lib/app/configuration/load.js, function=Array.async.auto.logger [as 1], line=94, method=async.auto.logger [as 1], native=false, column=38, file=/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/async/lib/async.js, function=null, line=459, method=null, native=false, column=null, file=null, function=Array.forEach, line=null, method=forEach, native=true, column=24, file=/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/async/lib/async.js, function=_each, line=32, method=null, native=false, column=9, file=/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/async/lib/async.js, function=Object.async.auto, line=430, method=async.auto, native=false, column=11, file=/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/lib/app/configuration/load.js, function=Configuration.loadConfig, line=35, method=loadConfig, native=false, column=21, file=/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/lodash/dist/lodash.js, function=Array.bound [as 0], line=729, method=bound [as 0], native=false, column=38, file=/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/async/lib/async.js, function=null, line=459, method=null, native=false, column=null, file=null, function=Array.forEach, line=null, method=forEach, native=true], stack=[Error: Unsupported logger override!, (has no `.log()` or `.debug()` method.),     at new CaptainsLog (/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/captains-log/index.js:53:10),     at Array.async.auto.logger [as 1] (/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/lib/app/configuration/load.js:94:25),     at /home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/async/lib/async.js:459:38,     at Array.forEach (native),     at _each (/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/async/lib/async.js:32:24),     at Object.async.auto (/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/async/lib/async.js:430:9),     at Configuration.loadConfig (/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/lib/app/configuration/load.js:35:11),     at Array.bound [as 0] (/home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/lodash/dist/lodash.js:729:21),     at /home/felix/Development/work/repiscore/repiscore-web-app/node_modules/sails/node_modules/async/lib/async.js:459:38,     at Array.forEach (native)]

The error seems to suggest that Sails tried to load a logger from the "custom" property, but the instance I passed did not have "info()" and "debug()" methods, so it's not a valid logger, even though I had called those methods on the logger right before passing it to the Sails config.

What's strange is that if I passed a custom logger with just those methods, Sails had no problem with it and worked as expected:

sails.lift({
    log: {
        custom: {
            info: function(text) {
                console.log('INFO >> ' + text); 
            },
            debug: function(text) {
                console.log('DEBUG >> ' + text);
            }
        }
    }
}, function(err, sails) {
    sails.log('testing');
});

This might mean the problem isn't with the "config.log.custom" property, but rather with my specific instance of Winston or the fact that I passed in an instance of Winston.

On a hunch, I decided set my Winston logger to "config.log", and this appears to be working great (respects the logging options and transports on my Winston logger).

var logger = myLib.logger;

logger.log('test');
logger.debug('test');

sails.lift({
    log: logger
}, function(err, sails) {
    sails.log('test');
});

However I am confused about whether this is a supported feature or just a hack I stumbled upon that might get removed in future versions of Sails.

Since overwriting the logger isn't really covered in Sail's documentation, though it is mentioned in the docs of Captain's Log, which is a Sails sub-project, I would have expected that setting the "custom" property of "config.log" would be the proper way of overwriting the logger.

If "config.log.custom" is supposed to be a proper way of setting a custom Winston logger, then there might be a bug in v0.10.5 that prevents that from working, at least in some instances (since it gave me the above error when I passed a valid instance of Winston to it).

Also if setting "config.log" is not a hack and is supposed to be supported, it should probably be mentioned in the docs along with "config.log.custom".

@felixmc
Copy link
Author

felixmc commented Feb 26, 2015

Messed around with this a little bit more and it seems that if I set the Winston logger to "config.log.custom" by doing it in the file "config/log.js" as opposed to passing it to "sails.lift" it works (almost) as expected. The only issue is that now the log message type gets printed twice (presumably once from Winston and once from Captain's Log):

image

It would then appear that the issue with the custom Winston logger not working might be with the config object passed to "sails.lift" and not with the whole logging component.

EDIT: Looked into Captain's Log and I think the duplicate log labels issue stems from there, so I created a separate issue on that project: https://github.com/balderdashy/captains-log/issues/14

@felixmc
Copy link
Author

felixmc commented Feb 27, 2015

So I dug into the code and narrowed it down to the line of code causing the issue.

When I call sail.lift and pass in my config object, it first gets passed to sails' load method as the configOverride parameter. On line 38 of lib/app/load.js a deep clone is made of the configOverride object (and therefore of my Winson logger) and stored into sails.config. configOverride is disregarded after that line and sails.config is used from there on.

Captain's Log looks for a log() method on the custom logger being passed in to determine if it's a valid logger (line 52: https://github.com/balderdashy/captains-log/blob/master/index.js). A Winston logger has a log method, but it's a prototype method (line 119: https://github.com/winstonjs/winston/blob/master/lib/winston/logger.js). When lodash does a deep clone of the configOverride object, it seems to omit prototype members (because they are not part of hasOwnProperty?), which means log() goes away and Captain's Log finds the Winston logger invalid. I tested this with some console.log's of the original configOverride object and the cloned sails.config to confirm that cloneDeep removes the log() method.

I don't understand the need to clone the configOverride object to store it in sails.config, but I'm sure there's a reason for it. Since Winston doesn't have a custom clone method, I'm not sure how this should be remedied.

@felixmc
Copy link
Author

felixmc commented Feb 27, 2015

Found a temporary solution by explicitly defining the log property on the logger and setting it to to the log method inherited from the prototype. It's possible to it might cause some unexpected behavior, but it allows the log() method to be cloned and seems to be a working hack for now.

var logger = myLib.logger;
logger.log = logger.log;

sails.lift({
    log: {
        custom: logger
    }
}, function(err, sails) {
    if (err) console.error(err);
    sails.log('testing');
});

@sgress454
Copy link
Member

@felixmc Thanks for all the work on this. Custom logging is a bit of a neglected feature. If you have a PR that would fix this, we'd love to see it.

@felixmc
Copy link
Author

felixmc commented Mar 2, 2015

@sgress454 do you know by any chance why configOverride is deep cloned when it is assigned to sails.config in lib/app/load.js on line 38? (ref: https://github.com/balderdashy/sails/blob/master/lib/app/load.js#L38)

Directly assigning configOverride to sails.config instead of a clone would fix this issue, but I don't know if it is being cloned for a specific purpose.

@gotmikhail
Copy link

@felixmc Have you tried any of this w/ the config/log.js file?

I've basically been wrestling almost the same issue, but trying to resolve it through the config. My goal is for each log entry to be a JSON object, with added meta-data, but it seems sails.log will only stuff it into the message attribute.

Here's the equivalent of what I have in config/log.js:

var winston = require('winston');
var wLogger = new (winston.Logger)({
        exitOnError: false,
        transports: [
            new winston.transports.File({
                filename: '/home/gotmikhail/sails.log',
                json: true,
                level: 'silly'
            })
        ],
        timestamp: true
    });
wLogger.log = wLogger.log;

module.exports.log = {
    level: 'silly',
    colors: false,
    custom: wLogger,
    wLogger: wLogger 
};

Note that I added the change you mentioned, wLogger.log = wLogger.log. Also, in modules.exports.log I didn't initially have wLogger: wLogger, but have added that for testing so I can directly call winston.log.

Here's a test:

sails.log.verbose('test1', { headers: req.headers });
sails.log('verbose', 'test2', { headers: req.headers });
sails.config.log.wLogger.log('verbose', 'test3', { headers: req.headers });

If everything was working as I expected, all 3 would be equivalent except for the message attribute. However, here's what we get:

{
  "level": "verbose",
  "message": "test1 { headers: \n   { host: '192.168.1.123:1337',\n     connection: 'keep-alive',\n     'content-length': '419',\n     csp: 'active',\n     'cache-control': 'no-cache',\n     origin: 'chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop',\n     'content-type': 'application/json',\n     'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2368.0 Safari/537.36',\n     'postman-token': '7ae1969a-f3fa-5ada-e8cb-a238441bd23b',\n     accept: '*/*',\n     dnt: '1',\n     'accept-encoding': 'gzip, deflate',\n     'accept-language': 'en-US,en;q=0.8' } }",
  "timestamp": "2015-04-14T21:23:09.537Z"
}
{
  "level": "debug",
  "message": "verbose test2 { headers: \n   { host: '192.168.1.123:1337',\n     connection: 'keep-alive',\n     'content-length': '419',\n     csp: 'active',\n     'cache-control': 'no-cache',\n     origin: 'chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop',\n     'content-type': 'application/json',\n     'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2368.0 Safari/537.36',\n     'postman-token': '7ae1969a-f3fa-5ada-e8cb-a238441bd23b',\n     accept: '*/*',\n     dnt: '1',\n     'accept-encoding': 'gzip, deflate',\n     'accept-language': 'en-US,en;q=0.8' } }",
  "timestamp": "2015-04-14T21:23:09.538Z"
}
{
  "headers": {
    "host": "192.168.1.123:1337",
    "connection": "keep-alive",
    "content-length": "419",
    "csp": "active",
    "cache-control": "no-cache",
    "origin": "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop",
    "content-type": "application/json",
    "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2368.0 Safari/537.36",
    "postman-token": "7ae1969a-f3fa-5ada-e8cb-a238441bd23b",
    "accept": "*/*",
    "dnt": "1",
    "accept-encoding": "gzip, deflate",
    "accept-language": "en-US,en;q=0.8"
  },
  "level": "verbose",
  "message": "test3",
  "timestamp": "2015-04-14T21:23:09.538Z"
}

So, looks like sails.log is still not using winston's log(). As noted above, sails.log is stuffing everything into the message attribute as a single string, not a JSON object.

Any suggestions?

@gotmikhail
Copy link

@bberry6
Copy link

bberry6 commented May 21, 2015

@gotmikhail @felixmc
fwiw: i got sails to use winston to my liking by doing this...

// sails/config/local.js
 var Winston = require('winston');

module.exports = {
   // ...
   log: {
      custom: new Winston.Logger({
         levels: {
            silly: 0,
            verbose: 1,
            debug: 2,
            info: 3,
            warn: 4,
            error: 5
         },
         transports: [
            new Winston.transports.Console({
               level: 'silly',
               showLevel: false
            })
         ]
      }),
      // the following line doesnt make sense, but if you dont 
      // have it, the "verbose" and "silly" levels will not appear
      level: ''
   },
   // ...
}

I haven't looked into why this works, but it does. The reason I defined my own custom log levels is because I disliked how sails goes against convention by having debug > info, and also because I disliked how Winston has verbose > debug. The latter was especially annoying because all the sails verbose messages would be displayed when I had the log level to debug....

Anyway, not a permanent fix, but it works for now.

@gotmikhail
Copy link

@bberry6 Does your fix take care of the issue w/ JSON / metadata?

@bberry6
Copy link

bberry6 commented May 21, 2015

@gotmikhail

Just tested it, and it looks like, no, they are not formatted the same...

Here's my local.js file, which has the Winston logger for custom.

The tests...

sails.log.info('test', {foo: 'bar'});
sails.config.log.custom.log('info', 'test4', {foo: 'bar'});

output...

{
  "level": "info",
  "message": "\u001b[32minfo: \u001b[39mtest { foo: 'bar' }"
}
{
  "foo": "bar",
  "level": "info",
  "message": "test4"
}

Honestly, this is probably because sails is doing some behind the scenes serialization when setting up the sails.log to use the Winston logger, but I haven't done the research so I honestly don't know....

Which could imply that the 2nd test is the expected output of the Winston logger with that setup?

@stensrud
Copy link

@gotmikhail @bberry6
Have you tried setting "inspect":false in the log config? It voids the argument to string-serialization in captains-log/lib/write.js.

@joshuamarquez
Copy link

you need to use formatter option inside transports in order to remove "level"

var logger = new (winston.Logger)({
  transports: [
    new (winston.transports.Console)({
      timestamp: function() {
        return Date.now();
      },
      formatter: function(options) {
        // Return string will be passed to logger.
        return options.timestamp() +' '+ options.level.toUpperCase() +' '+ (undefined !== options.message ? options.message : '') +
          (options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' );
      }
    })
  ]
});

just remove options.level.toUpperCase()

@rh0dium
Copy link

rh0dium commented Jul 9, 2015

I would add a slightly simpler way is to simply do this.

var winston = require('winston');

var customLogger = new winston.Logger({
  transports: [
      new (winston.transports.Console)({
        level: 'debug',
        colorize: true,
        timestamp: true,
        showLevel: false
      })
  ],
});

showLevel will hide the level.

@sailsbot
Copy link

Thanks for posting, @felixmc. I'm a repo bot-- nice to meet you!

It has been 60 days since there have been any updates or new comments on this page. If this issue has been resolved, feel free to disregard the rest of this message. On the other hand, if you are still waiting on a patch, please:

  • review our contribution guide to make sure this submission meets our criteria (only verified bugs with documented features, please; no questions, commentary, or bug reports about undocumented features or unofficial plugins)
  • create a new issue with the latest information, including updated version details with error messages, failing tests, and a link back to the original issue. This allows GitHub to automatically create a back-reference for future visitors arriving from search engines.

Thanks so much for your help!

@mikermcneil
Copy link
Member

@gotmikhail @stensrud @joshuamarquez @rh0dium thanks for the help guys!

@bberry6 @felixmc as @gotmikhail pointed out above, there's some additional points covered in https://github.com/balderdashy/captains-log/issues/16 that might be helpful. Specifically, https://github.com/balderdashy/captains-log/issues/16#issuecomment-169890710 is a pretty thorough example of another way to completely override the default logging with your own custom handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

10 participants