Jest / NodeJS, how to build a test for "unhandledRejection" - node.js

I'm trying to have 100% coverage.
I'm stuck at creating a test for this part of my code:
// Manually throwing the exception will let winston handle the logging
process.on("unhandledRejection", (ex) => {
throw ex;
});
I tried to use the following:
const logger = require("../../../startup/logging");
describe("startup / logging", () => {
it("should log the error in case of unhandledRejection", () => {
process.emit("unhandledRejection");
const loggerError = jest.spyOn(logger, "error");
expect(loggerError).toHaveBeenCalled();
});
});
But the test fails with: thrown: undefined
This is the full code of logging.js:
const { createLogger, format, transports } = require("winston");
require("winston-mongodb");
// This will forward the error in the pipeline to our error handler
require("express-async-errors");
// Manually throwing the exception will let winston handle the logging
process.on("unhandledRejection", (ex) => {
throw ex;
});
// Log to files
const logger = createLogger({
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
format.errors({ stack: true }),
format.splat(),
format.json()
),
transports: [
new transports.File({filename: "./logs/combined.log", level: "verbose"}),
],
transports: [
new transports.File({filename: "./logs/error.log", level: "error"}),
new transports.File({filename: "./logs/combined.log"}),
],
exceptionHandlers: [
new transports.File({ filename: "./logs/exceptions.log" }),
new transports.File({ filename: "./logs/combined.log" }),
],
handleExceptions: true,
});
// Log to database
logger.add(new transports.MongoDB({
level: "error",
db: "mongodb://localhost:27017/rest-api-mongodb",
options: {
useUnifiedTopology: true,
useNewUrlParser: true,
},
metaKey: "stack",
handleExceptions: true,
}));
module.exports = logger;
This is the error middleware that is triggered in case of unhandledRejection:
// Winston is a logger, it allows to store errors in a log and mongoDB
const logger = require("../startup/logging");
// This function will handle all errors in the router
// It works thanks to require("express-async-errors"); that forwards the error in the pipeline
// It does not work outside of the context of express
module.exports = function (err, req, res, next) {
logger.error(err.message, err);
// error, warn, info, berbose, debug, silly
res.status(500).send("Something on the server failed.");
}
Any help is appreciated!

Try triggering unhandledRejection by using a real unhandled rejection. For example, call Promise.reject() without attaching a handler.

After playing around with the code, I made it work as follows:
require("../../../startup/logging");
describe("startup / logging", () => {
it("should throw an error if there is an unhandledRejection", () => {
const emitUnhandledRejection = () => process.emit("unhandledRejection");
expect(emitUnhandledRejection).toThrow();
});
});

Related

Why error logs appears in warning logs file - Node.Js & Winston

In my Node.js REST API I am using Winston Logging to print the errors and info logs in a separated files.
This is my Winston setup:
const { createLogger, format, transports } = require("winston");
const { timestamp, printf, errors } = format;
const logFormat = printf(({ level, message, timestamp, stack }) => {
return `${timestamp} ${level}: ${stack || message}`;
});
const logger = createLogger({
transports: [
//new transports.Console(),
new transports.File({
level: "info",
filename: "logsWarnings.log",
}),
new transports.File({
level: "error",
filename: "logsErrors.log",
}),
],
format: format.combine(
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.json(),
format.prettyPrint(),
errors({ stack: true })
//logFormat
),
statusLevels: true,
});
module.exports = logger;
This is an example of one of my requests (info log is for req details):
deleteUser: (req, res) => {
logger.info("This is an info log");
mySqlConnection.query(
"DELETE FROM Users WHERE user_id=?",
req.params.userid,
(err, rows) => {
try {
if (rows.affectedRows >= 1) {
res.send("user deleted successfully");
} else {
res.status(500).send("Can't delete row");
}
} catch (err) {
logger.error("this is error", { err });
}
}
);
},
I have a problem that error logs appears both in the error logs file and in the warning logs file. How can I fix it?
What is going on?
As mentioned in the documentation
Logging levels in winston conform to the severity ordering specified by RFC5424: severity of all levels is assumed to be numerically ascending from most important to least important.
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
};
and
winston allows you to define a level property on each transport which specifies the maximum level of messages that a transport should log.
What that means? For example:
You used "info" level for logsWarnings.log file. it means that you are going to log itself and more important levels into the file (info, warn and error).
For logsErrors.log file you used "error" level. That means it will only log the error level logs because "error" has the highest importance.
That is why you are getting both in your logsWarnings.log file.
Solution:
You can separate logger like:
...
const formatConf = format.combine(
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.json(),
format.prettyPrint(),
errors({ stack: true })
//logFormat
);
const infoLogger = createLogger({
transports: [
//new transports.Console(),
new transports.File({
level: "info",
filename: "./logs/logsWarnings.log",
}),
],
format: formatConf,
statusLevels: true,
});
const errLogger = createLogger({
transports: [
new transports.File({
level: "error",
filename: "./logs/logsErrors.log",
}),
],
format: formatConf,
statusLevels: true,
});
...
And use like:
errLogger.error("this is error", { err });
// or
infoLogger.info("This is an info log");
For more customization, please see the documentation.

Meta is null in Winston-mongodb

I am using winston-mongodb modlue to log the request response.
Logger.service.ts
const options = {
console: {
db: DB_URL,
level: 'info',
format: format.combine(format.timestamp(), format.json()),
collection: 'logs',
},
error: {
db: DB_URL,
level: 'error',
format: format.combine(format.timestamp(), format.json()),
collection: 'logs',
}
};
const logger = createLogger({
transports: [
new MongoDB(options.console),
new MongoDB(options.error)
],
});
Server.ts
server.listen(port, () => {
logger.info({message:`Server is up and running on port ${port}`, meta: {
myProp: 'foo'
}});
);
Result:
{
"_id" : ObjectId("5ed8bd2c726d273004b082ca"),
"timestamp" : ISODate("2020-06-04T09:21:48.835Z"),
"level" : "info",
"message" : "Server is up and running on port 4000 HI",
**"meta" : null**
}
I am trying to add meta data to logger. But it is adding null to the result.
To get err object stored in the meta (with all its javascript properties), you need to specify :
format.metadata()
In the createLogger call.
Here is a working sample code:
// Declare winston
const winston = require("winston");
// Import all needed using Object Destructuring
const { createLogger, format, transports } = require("winston");
const { combine, timestamp, printf } = format;
// Export the module
module.exports = function (err, req, res, next) {
const logger = createLogger({
level: "error",
format: combine(
format.errors({ stack: true }), // log the full stack
timestamp(), // get the time stamp part of the full log message
printf(({ level, message, timestamp, stack }) => {
// formating the log outcome to show/store
return `${timestamp} ${level}: ${message} - ${stack}`;
}),
format.metadata() // >>>> ADD THIS LINE TO STORE the ERR OBJECT IN META field
),
transports: [
new transports.Console(), // show the full stack error on the console
new winston.transports.File({
// log full stack error on the file
filename: "logfile.log",
format: format.combine(
format.colorize({
all: false,
})
),
}),
new winston.transports.MongoDB({
db: "mongodb://localhost/logdb",
// collection: "log",
level: "error",
storeHost: true,
capped: true,
// metaKey: 'meta'
}),
],
});
logger.log({
level: "error",
message: err,
});
// Response sent to client but nothing related to winston
res.status(500).json(err.message);
};
try this
// create new meta object to store in database
let logMeta={
stack: err.stack, // error stack
otherInfo: otherInfoData // any other info you want to store
}
// use metadata propery for meta
logger.error(err.message, {metadata: logMeta });
OR another way, if you are exporting the function as error middleware (using nodejs, express):
module.exports = function (err, req, res, next) {
logger.log({
level: "error",
message: err.message,
stack: err.stack,
metadata: err.stack, // Put what you like as meta
});
res.status(500).send("Uncaught error caught here.");
};
Specify metadata object explicitly:
logger.error(err.message, {metadata: err.stack});
apparently i had the same problem where the meta property in mongoDB transport was null: This is how i fixed it....
const winston = require("winston")
const { format } = require("winstone")
winstone.add(new
winstone.transports.MongoDB({
format:format.metadata()})

How to force nodejs winston log to file before process exit

I am using winston 3 to log my data. But sometimes it didn't log the error before process exit.
The following process will exit, without log to the logfile.log:
const winston = require('winston');
winston.add(new winston.transports.File({
filename: 'logfile.log'
}));
winston.info('please log me in');
process.exit(1);
One of attempted solution is used callback:
const winston = require('winston');
const logger = new winston.createLogger({
new winston.transports.File({
filename: 'logfile.log'
}));
winston.info('please log me in', () => {
process.exit(1);
});
setTimeout(() => {}, 100000000000); // pause the process until process.exit is call
But Winston 3.x have callback issue, so the above code won't work, the process will not exit.
I am looking for a workable solution? Any help will be greatly appreciated. My OS is Ubuntu 16.04, Node 10.17.
Edit 1:
I also have try Prabhjot Singh Kainth's suggestion use finish event to trigger process exit:
const winston = require('winston');
const logger = winston.createLogger({
transports: [
new winston.transports.File({
filename: 'logfile.log'
})
]
});
logger.info('please log me in');
logger.on('finish', () => {
process.exit();
});
logger.end();
setTimeout(() => {}, 100000000000); // pause the process until process.exit is call
In the above case, the process will exit, but no log file will be created.
How about this one?
logger.info('First message...')
logger.info('Last message', () => process.exit(1))
Finally, I find a workable solution. Instead of listening to logger finish event, it should listen to file._dest finish event. But file._dest is only created after file open event. So it need to wait for file open event in initialization process.
const winston = require('winston');
const file = new winston.transports.File({
filename: 'logfile.log'
});
const logger = winston.createLogger({
transports: [file]
});
file.on('open', () => { // wait until file._dest is ready
logger.info('please log me in');
logger.error('logging error message');
logger.warn('logging warning message');
file._dest.on('finish', () => {
process.exit();
});
logger.end();
});
setTimeout(() => {}, 100000000000); // pause the process until process.exit is call
Each instance of winston.Logger is also a Node.js stream.
A finish event will be raised when all logs have flushed to all transports after the stream has been ended.
Just try using this code :
const transport = new winston.transports.Console();
const logger = winston.createLogger({
transports: [transport]
});
logger.on('finish', function (info) {
// All `info` log messages has now been logged
});
logger.info('CHILL WINSTON!', { seriously: true });
logger.end();
The answer from #WanChap was good, but not when there is multiple file transports. I will post my entire logging solution below, which handles exiting the process correctly, including support for Development and Production.
The only caveat is that it does not work with the exceptions log at the moment, but it will work with every other transport based log. If anyone figures out how to make this work with the exception log as well, please post in the comments.
First, you want to create a folder in the same directory that your file that is doing the logs is in, and call it logger. In this folder, create three .js files, one called dev-logger.js, the other prod-logger.js, and finally index.js
In node, when you require a directory, it will assume you want to load the index.js file.
TheFileYouWantToLogFrom.js
const logger = require('./logger');
logger.info("it works!");
index.js
This file will look at your node process environment state and use the correct logger.
const buildDevLogger = require('./dev-logger');
const buildProdLogger = require('./prod-logger');
let logger = null;
if(process.env.NODE_ENV === 'development'){
logger = buildDevLogger();
}else{
logger = buildProdLogger();
}
module.exports = logger;
You can test this later, by simply calling your node process (in my case cron.js) in your terminal, by saying something like NODE_ENV=production node cron.js or NODE_ENV=development node cron.js
dev-logger.js
const { format, createLogger, transports} = require('winston');
const { timestamp, combine, printf, errors } = format;
var numOpenTransports = 0;
function buildDevLogger(){
const logFormat = printf(({ level, message, timestamp, stack, durationMs }) => {
return `${timestamp} ${level}: ${stack || message}${durationMs ? " | "+durationMs+'ms' : ''}`;
});
//For info on the options go here.
//https://github.com/winstonjs/winston/blob/master/docs/transports.md
var options = {
console: {
handleExceptions: true,
level: 'debug',
format: combine(
format.colorize(),
timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
errors({stack: true}),
logFormat
),
},
combined: {
filename: 'logs/dev/combined.log',
maxsize: 10000000,
maxFiles: 10,
tailable:true,
format: combine(
timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
errors({stack: true}),
logFormat
),
},
error: {
filename: 'logs/dev/error.log',
level: 'error',
maxsize: 10000000,
maxFiles: 10,
tailable:true ,
format: combine(
timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
errors({stack: true}),
logFormat
),
},
exception : {
filename: 'logs/dev/exceptions.log',
maxsize: 10000000,
maxFiles: 10,
tailable:true,
format: combine(
timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
errors({stack: true}),
logFormat
),
}
};
function fileFinished(){
numOpenTransports--;
if(numOpenTransports==0){
process.exit(0);
}
}
var combinedFile = new transports.File(options.combined);
var errorFile = new transports.File(options.error);
var exceptionFile = new transports.File(options.exception);
combinedFile.on('open', () => { // wait until file._dest is ready
numOpenTransports++;
combinedFile._dest.on('finish', () => {
fileFinished();
});
});
errorFile.on('open', () => { // wait until file._dest is ready
numOpenTransports++;
errorFile._dest.on('finish', () => {
fileFinished();
});
});
return createLogger({
defaultMeta: {service:'cron-dev'},
transports: [
new transports.Console(options.console),
combinedFile,
errorFile,
],
exceptionHandlers: [
exceptionFile
]
});
}
module.exports = buildDevLogger;
prod-logger.js
const { format, createLogger, transports} = require('winston');
const { timestamp, combine, errors, json } = format;
var numOpenTransports = 0;
function buildProdLogger(){
var options = {
console: {
handleExceptions: true,
level: 'debug',
format: combine(
timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
errors({stack: true}),
json()
),
},
combined: {
filename: 'logs/prod/combined.log',
maxsize: 10000000,
maxFiles: 10,
tailable:true,
format: combine(
timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
errors({stack: true}),
json()
),
},
error: {
filename: 'logs/prod/error.log',
level: 'error',
maxsize: 10000000,
maxFiles: 10,
tailable:true ,
format: combine(
timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
errors({stack: true}),
json()
),
},
exception : {
filename: 'logs/prod/exceptions.log',
maxsize: 10000000,
maxFiles: 10,
tailable:true,
format: combine(
timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
errors({stack: true}),
json()
),
}
};
function fileFinished(){
numOpenTransports--;
if(numOpenTransports==0){
process.exit(0);
}
}
var combinedFile = new transports.File(options.combined);
var errorFile = new transports.File(options.error);
var exceptionFile = new transports.File(options.exception);
combinedFile.on('open', () => { // wait until file._dest is ready
numOpenTransports++;
combinedFile._dest.on('finish', () => {
fileFinished();
});
});
errorFile.on('open', () => { // wait until file._dest is ready
numOpenTransports++;
errorFile._dest.on('finish', () => {
fileFinished();
});
});
return createLogger({
defaultMeta: {service:'cron-prod'},
transports: [
new transports.Console(options.console),
combinedFile,
errorFile,
],
exceptionHandlers: [
exceptionFile
]
});
}
module.exports = buildProdLogger;
The way the dev and prod logger files work is by using the numOpenTransports variable to keep track of which 'files transports' are currently open, and once they are all closed, then, and only then, exit the process.

nodejs auto maintain winston log

I'm trying to integrate winston to a nodeJs Express app. It works but I don't know how could it be auto maintained. I wanted something like when it reach, for example 50000 rows, it delete the first ones before adding the new ones for terms of space. The idea is doing that without something like cron. Is it possible?
Here is my testing code:
//Winston logger
const { createLogger, format, transports } = require('winston');
const { combine, timestamp, label, printf } = format;
const winstonFormat = printf(info => {
return `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`;
});
const logger = createLogger({
level: 'info',
//maxsize:'10000', //It doesn't works for me
format: combine(
label({ label: config.environment }),
timestamp(),
winstonFormat
),
transports: [
new transports.File({ filename: 'error.log', level: 'error' }),
new transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new transports.Console({
format: format.simple()
}));
}
You're looking for log rotation, so googling "winston log rotation" points to https://github.com/winstonjs/winston-daily-rotate-file, which is probably just the ticket for you.

How to use Winston in several modules?

I have several modules - let's say server.js, module1.js,...,moduleN.js.
I would like define the log file in my server.js:
winston.add(winston.transports.File, { filename: 'mylogfile.log' });
and then use it in all my modules.
What is the best way to do that? I could exports.winston=winston; in each module and then set it in the server.js, but is there any better solution?
Thank you in advance!
The default logger concept handles this nicely.
Winston defines a default logger that any straight require (and subsequent require) to winston will retrieve. Thus you simply configure this default logger once, and it's available for subsequent module use via vanilla require('winston') in its glorious tweaked multi-transport mode.
e.g. here is my complete logging setup that defines 3 transports. I swap Loggly for MongoDB sometimes.
server.js
var logger=require('./log.js');
// requires winston and configures transports for winstons default logger- see code below.
all other .js files
var logger=require('winston'); // this retrieves default logger which was configured in log.js
logger.info("the default logger with my tricked out transports is rockin this module");
log.js - this is a one time configuration of the DEFAULT logger
var logger = require('winston');
var Loggly = require('winston-loggly').Loggly;
var loggly_options={ subdomain: "mysubdomain", inputToken: "efake000-000d-000e-a000-xfakee000a00" }
logger.add(Loggly, loggly_options);
logger.add(winston.transports.File, { filename: "../logs/production.log" });
logger.info('Chill Winston, the logs are being captured 3 ways- console, file, and Loggly');
module.exports=logger;
Alternatively for more complex scenarios you can use winston containers and retrieve the logger from a named container in other modules. I haven't used this.
My only issue with this was a missing logs directories on my deployment host which was easily fixed.
What I do ( which may not be the best way ) is use a 'global' module where I export all the stuff that I use through my applications.
For instance:
//Define your winston instance
winston.add(winston.transports.File, { filename: 'mylogfile.log' });
exports.logger = winston;
exports.otherGlobals = ....
Now just require this globally used module from your other modules
var Global = require(/path/to/global.js);
Because the file is cached after the first time it is loaded (which you can verify by including a log statement in your global; it will only log once), there's very little overhead in including it again. Putting it all into one file is also easier than requiring ALL your globally used modules on every page.
I wanted to use custom colours and levels.
So I removed the default console-transport and set a colorized one
here is my logger.js
var logger = require('winston');
logger.setLevels({
debug:0,
info: 1,
silly:2,
warn: 3,
error:4,
});
logger.addColors({
debug: 'green',
info: 'cyan',
silly: 'magenta',
warn: 'yellow',
error: 'red'
});
logger.remove(logger.transports.Console);
logger.add(logger.transports.Console, { level: 'debug', colorize:true });
module.exports = logger;
Loading from app.js:
var logger = require('./lib/log.js');
Loading from other modules:
var logger = require('winston');
Slightly off topic (as the OP asks about Winston), but I like the 'child-logger' approach by Bunyan:
var bunyan = require('bunyan');
var log = bunyan.createLogger({name: 'myapp'});
app.use(function(req, res, next) {
req.log = log.child({reqId: uuid()});
next();
});
app.get('/', function(req, res) {
req.log.info({user: ...});
});
It solves the OP's problem as the logger is available through the req object (hence no need for 'require(log)' in each module). Additionally, all log entries belonging to a particular request will have a unique ID that connects them together.
{"name":"myapp","hostname":"pwony-2","pid":14837,"level":30,"reqId":"XXXX-XX-XXXX","user":"...#gmail.com","time":"2014-05-26T18:27:43.530Z","v":0}
I'm not sure if Winston supports this as well.
I am working on Winston 3.0.0 right now.
And it seems the way to configure the default logger has changed a little bit.
The way that works for me is folloing:
log.js// the setting for global logger
const winston= require('winston');
winston.configure({
level:"debug",
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
transports: [
new winston.transports.Console()
]
});
The other part is the same.
In the beginning of you application, require('log.js'), and also require ('winston'),
While in all other files, simply require('winston')
.
I'm creating a new Winston logger.
log.js
'use strict';
const winston = require('winston');
module.exports = new(winston.Logger)({
transports: [
new(winston.transports.Console)({
level: 'info'
})
]
});
a.js
const log = require('./log');
log.info("from a.js");
b.js
const log = require('./log');
log.info("from b.js");
Here is my logger configuration with winston version is 3.2.1.
It storing logs in application.log file and for error stack trace I am using errors({ stack: true }) and small trick in printf function to print stack trace in error case.
Configuration
const {format, transports} = require('winston');
const { timestamp, colorize, printf, errors } = format;
const { Console, File } = transports;
LoggerConfig = {
level: process.env.LOGGER_LEVEL || 'debug',
transports: [
new Console(),
new File({filename: 'application.log'})
],
format: format.combine(
errors({ stack: true }),
timestamp(),
colorize(),
printf(({ level, message, timestamp, stack }) => {
if (stack) {
// print log trace
return `${timestamp} ${level}: ${message} - ${stack}`;
}
return `${timestamp} ${level}: ${message}`;
}),
),
expressFormat: true, // Use the default Express/morgan request formatting
colorize: false, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red).
ignoreRoute: function (req, res) {
return false;
} // optional: allows to skip some log messages based on request and/or response
}
Declare
I am using this same configuration in express-winston and for general log also. I declared __logger object globally so that you don't need to import every time in every file. Generally in node js all the global variable prefix with 2 time underscore(__) so it will be good to follow this.
Server.js
const winston = require('winston');
const expressWinston = require('express-winston');
/**
* winston.Logger
* logger for specified log message like console.log
*/
global.__logger = winston.createLogger(LoggerConfig);
/**
* logger for every HTTP request comes to app
*/
app.use(expressWinston.logger(LoggerConfig));
Use
__logger is global so you can use it any place, for example:
blog.controller.js
function save(req, res) {
try {
__logger.debug('Blog add operation');
.
.
return res.send(blog);
} catch (error) {
__logger.error(error);
return res.status(500).send(error);
}
}
Hope this will help !
I use a factory function and pass in the module name so it can be added to the meta data:
logger-factory.js
const path = require('path');
const { createLogger, format, transports } = require('winston');
const { combine, errors, timestamp } = format;
const baseFormat = combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
errors({ stack: true }),
format((info) => {
info.level = info.level.toUpperCase();
return info;
})(),
);
const splunkFormat = combine(
baseFormat,
format.json(),
);
const prettyFormat = combine(
baseFormat,
format.prettyPrint(),
);
const createCustomLogger = (moduleName) => createLogger({
level: process.env.LOG_LEVEL,
format: process.env.PRETTY_LOGS ? prettyFormat : splunkFormat,
defaultMeta: { module: path.basename(moduleName) },
transports: [
new transports.Console(),
],
});
module.exports = createCustomLogger;
app-harness.js (so I can run the exported index module)
const index = require('./index');
// https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html
const sampleEvent = {
"Records": [
{
"eventVersion": "2.1",
"eventSource": "aws:s3",
"awsRegion": "us-east-2",
"eventTime": "2019-09-03T19:37:27.192Z",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "AWS:AIDAINPONIXQXHT3IKHL2"
},
"requestParameters": {
"sourceIPAddress": "205.255.255.255"
},
"responseElements": {
"x-amz-request-id": "D82B88E5F771F645",
"x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo="
},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1",
"bucket": {
"name": "lambda-artifacts-deafc19498e3f2df",
"ownerIdentity": {
"principalId": "A3I5XTEXAMAI3E"
},
"arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df"
},
"object": {
"key": "b21b84d653bb07b05b1e6b33684dc11b",
"size": 1305107,
"eTag": "b21b84d653bb07b05b1e6b33684dc11b",
"sequencer": "0C0F6F405D6ED209E1"
}
}
}
]
};
index.handler(sampleEvent)
.then(() => console.log('SUCCESS'))
.catch((_) => console.log('FAILURE'));
index.js
const logger = require('./logger-factory')(__filename);
const app = require('./app');
exports.handler = async function (event) {
try {
logger.debug('lambda triggered with event', { event });
await app.run(event);
logger.debug(`lambda finished`);
} catch(error) {
logger.error('lambda failed: ', error);
// rethrow the error up to AWS
throw error;
}
}
app.js
const logger = require('./logger-factory')(__filename);
const run = async (event) => {
logger.info('processing S3 event', event);
try {
logger.info('reading s3 file')
// throws because I used "Record" instead of "Records"
const s3 = event.Record[0].s3;
// use s3 to read the file
} catch (error) {
logger.error('failed to read from S3: ', error);
throw error;
}
};
module.exports = { run };
when I run the application locally at WARN level:
~/repos/ghe/lambda-logging (master * u=)> LOG_LEVEL=warn node -r dotenv/config ./src/app-harness.js
{
module: 'app.js',
level: 'ERROR',
message: "failed to read from S3: Cannot read property '0' of undefined",
stack: "TypeError: Cannot read property '0' of undefined\n" +
' at Object.run (/Users/jason.berk/repos/ghe/lambda-logging/src/app.js:8:28)\n' +
' at Object.exports.handler (/Users/jason.berk/repos/ghe/lambda-logging/src/index.js:7:15)\n' +
' at Object.<anonymous> (/Users/jason.berk/repos/ghe/lambda-logging/src/test-harness.js:44:7)\n' +
' at Module._compile (internal/modules/cjs/loader.js:1158:30)\n' +
' at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)\n' +
' at Module.load (internal/modules/cjs/loader.js:1002:32)\n' +
' at Function.Module._load (internal/modules/cjs/loader.js:901:14)\n' +
' at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)\n' +
' at internal/main/run_main_module.js:18:47',
timestamp: '2020-05-11 17:34:06'
}
{
module: 'index.js',
level: 'ERROR',
message: "lambda failed: Cannot read property '0' of undefined",
stack: "TypeError: Cannot read property '0' of undefined\n" +
' at Object.run (/Users/jason.berk/repos/ghe/lambda-logging/src/app.js:8:28)\n' +
' at Object.exports.handler (/Users/jason.berk/repos/ghe/lambda-logging/src/index.js:7:15)\n' +
' at Object.<anonymous> (/Users/jason.berk/repos/ghe/lambda-logging/src/test-harness.js:44:7)\n' +
' at Module._compile (internal/modules/cjs/loader.js:1158:30)\n' +
' at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)\n' +
' at Module.load (internal/modules/cjs/loader.js:1002:32)\n' +
' at Function.Module._load (internal/modules/cjs/loader.js:901:14)\n' +
' at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)\n' +
' at internal/main/run_main_module.js:18:47',
timestamp: '2020-05-11 17:34:06'
}
when I run at DEBUG level:
~/repos/ghe/lambda-logging (master * u=)> LOG_LEVEL=debug node -r dotenv/config ./src/test-harness.js
{
module: 'index.js',
event: {
Records: [
{
eventVersion: '2.1',
eventSource: 'aws:s3',
awsRegion: 'us-east-2',
eventTime: '2019-09-03T19:37:27.192Z',
eventName: 'ObjectCreated:Put',
userIdentity: { principalId: 'AWS:AIDAINPONIXQXHT3IKHL2' },
requestParameters: { sourceIPAddress: '205.255.255.255' },
responseElements: {
'x-amz-request-id': 'D82B88E5F771F645',
'x-amz-id-2': 'vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo='
},
s3: {
s3SchemaVersion: '1.0',
configurationId: '828aa6fc-f7b5-4305-8584-487c791949c1',
bucket: {
name: 'lambda-artifacts-deafc19498e3f2df',
ownerIdentity: { principalId: 'A3I5XTEXAMAI3E' },
arn: 'arn:aws:s3:::lambda-artifacts-deafc19498e3f2df'
},
object: {
key: 'b21b84d653bb07b05b1e6b33684dc11b',
size: 1305107,
eTag: 'b21b84d653bb07b05b1e6b33684dc11b',
sequencer: '0C0F6F405D6ED209E1'
}
}
}
]
},
level: 'DEBUG',
message: 'lambda triggered with event',
timestamp: '2020-05-11 17:38:21'
}
{
module: 'app.js',
Records: [
{
eventVersion: '2.1',
eventSource: 'aws:s3',
awsRegion: 'us-east-2',
eventTime: '2019-09-03T19:37:27.192Z',
eventName: 'ObjectCreated:Put',
userIdentity: { principalId: 'AWS:AIDAINPONIXQXHT3IKHL2' },
requestParameters: { sourceIPAddress: '205.255.255.255' },
responseElements: {
'x-amz-request-id': 'D82B88E5F771F645',
'x-amz-id-2': 'vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo='
},
s3: {
s3SchemaVersion: '1.0',
configurationId: '828aa6fc-f7b5-4305-8584-487c791949c1',
bucket: {
name: 'lambda-artifacts-deafc19498e3f2df',
ownerIdentity: { principalId: 'A3I5XTEXAMAI3E' },
arn: 'arn:aws:s3:::lambda-artifacts-deafc19498e3f2df'
},
object: {
key: 'b21b84d653bb07b05b1e6b33684dc11b',
size: 1305107,
eTag: 'b21b84d653bb07b05b1e6b33684dc11b',
sequencer: '0C0F6F405D6ED209E1'
}
}
}
],
level: 'INFO',
message: 'processing S3 event',
timestamp: '2020-05-11 17:38:21'
}
{
message: 'reading s3 file',
level: 'INFO',
module: 'app.js',
timestamp: '2020-05-11 17:38:21'
}
{
module: 'app.js',
level: 'ERROR',
message: "failed to read from S3: Cannot read property '0' of undefined",
stack: "TypeError: Cannot read property '0' of undefined\n" +
' at Object.run (/Users/jason.berk/repos/ghe/lambda-logging/src/app.js:8:28)\n' +
' at Object.exports.handler (/Users/jason.berk/repos/ghe/lambda-logging/src/index.js:7:15)\n' +
' at Object.<anonymous> (/Users/jason.berk/repos/ghe/lambda-logging/src/test-harness.js:44:7)\n' +
' at Module._compile (internal/modules/cjs/loader.js:1158:30)\n' +
' at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)\n' +
' at Module.load (internal/modules/cjs/loader.js:1002:32)\n' +
' at Function.Module._load (internal/modules/cjs/loader.js:901:14)\n' +
' at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)\n' +
' at internal/main/run_main_module.js:18:47',
timestamp: '2020-05-11 17:38:21'
}
{
module: 'index.js',
level: 'ERROR',
message: "lambda failed: Cannot read property '0' of undefined",
stack: "TypeError: Cannot read property '0' of undefined\n" +
' at Object.run (/Users/jason.berk/repos/ghe/lambda-logging/src/app.js:8:28)\n' +
' at Object.exports.handler (/Users/jason.berk/repos/ghe/lambda-logging/src/index.js:7:15)\n' +
' at Object.<anonymous> (/Users/jason.berk/repos/ghe/lambda-logging/src/test-harness.js:44:7)\n' +
' at Module._compile (internal/modules/cjs/loader.js:1158:30)\n' +
' at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)\n' +
' at Module.load (internal/modules/cjs/loader.js:1002:32)\n' +
' at Function.Module._load (internal/modules/cjs/loader.js:901:14)\n' +
' at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)\n' +
' at internal/main/run_main_module.js:18:47',
timestamp: '2020-05-11 17:38:21'
}
if you want to make the logger a global variable- you have to do specifically by assign it to the global variable like so
logger.js
var winston = require('winston')
var winston = winston.createLogger({
transports: [
new (winston.transports.Console)(),
new (winston.transports.File)({
filename: './logs/logger.log'
})
]
});
module.exports=winston;
app.js
let logger = require('./logger')
global.__logger = logger
someController.js
__logger.info('created log successfully')
Note: it's good practice to assign a prefix for every global variable so you will know that is a global one. i'm using __ as prefix (double low dash)
Just create logger.js and put
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
transports: [
new winston.transports.Console()
]
});
module.exports = logger
Then you can require and use it anywhere, since logger is now singleton.
const logger = require('./utils/logger');
logger.info('Hello!');
This even gives you an option to swap logging library if needed. The accepted answer is totally wrong and one step closer to spaghetti code.
In my team we have created a private npm package with all default configs (as you've shown in previous answers)
I've just one question: would it be a good practice to declare the logger object as a global in order to avoid import in each and every module?

Resources