Logging is undoubtedly one of the most important parts of our application. Now, console is a very powerful tool, yes, but what if we wanted to log not only to console, but also to a file?

We could try to write a function logToFile and call it right after console.log . This, however, is not the most DRY (don’t repeat yourself) way to go about it.

What we actually want is a single logger with which we can call, for example, logger.info that automatically logs the message into our console, saves it into two files, and whatever else we might need at the time.

Libraries like Winston, which provide logging in our applications, are very good at what they do, so we don’t really need to reinvent the wheel. Still, I believe that implementing such a library ourselves often provides a lot of insight into how they work. Also, we may want to do a few things differently and add a feature or two.

Go ahead and clone the repository and let’s get started by having a look at what we want to achieve:

const logger = createLogger({ level: 'info', transports: [ new transports.console({ level: 'debug', colorize: true, template: createTemplate( format.level(), format.text(' :gift:'), format.newLine(), format.message(), format.newLine(), format.text('Logged from '), format.location(), format.text(' :tada:'), ), }), new transports.file({ level: 'info', path: path.join(__dirname, '../important.log'), template: createTemplate( format.level(), format.text(' :gift:'), format.newLine(), format.message(), format.newLine(), format.text('Logged from '), format.location(), format.text(' :tada:'), ), }), new transports.file({ level: 'info', path: path.join(__dirname, '../not-so-important.log'), template: createTemplate( format.level(), format.text(' :tada:'), format.newLine(), format.message(), format.newLine(), format.text('Logged from '), format.location(), format.text(' :tada:'), ), }), ], });

createLogger

Here is the interface for the createLogger config:

interface Config { transports?: Transport[]; level?: Level; }

Let’s break it down.

Level

A level is a string with a numeric value assigned to it.

Here are our levels and their numeric values:

const levels = { emerg: 'emerg', alert: 'alert', crit: 'crit', error: 'error', warning: 'warning', notice: 'notice', info: 'info', debug: 'debug', }; const levelsNumbers = { [levels.emerg]: 0, [levels.alert]: 1, [levels.crit]: 2, [levels.error]: 3, [levels.warning]: 4, [levels.notice]: 5, [levels.info]: 6, [levels.debug]: 7, };

As you can see, the lower the level, the more important the message.

No logs with a level lower than the one provided to the config will be accepted.

Transports

We provide an array of transports, which are different ways of displaying the message. transports.console is going to log the message into our console, and transports.file into a file. We could even create our own transport and use it to save each message inside a database.

Transport

A transport has to be an instance of a class called Transport so that it can inherit all the necessary methods.

Let’s take a look at a config passed to each transport:

interface TransportConfig { format?: (value: any) => string; level?: Level; template?: (info: Info) => string; }

Changing an expression to a string with the built-in function .toString() may sometimes return results such as [object Object] . It tells us literally nothing and we would like to avoid that, thus we are using our custom-build function to handle changing the expression into a string-based representation.

The message is going to be passed like this:

logger.info `This is a collection ${collection} and it is very nice. This is a number ${numb} and it is also very nice.` ;

Here, our function would be called twice: first with collection passed in as the value, and the second time with numb .

Note that I did not include parentheses () after calling the info method. This is an example of what we call a tag function; you can read more about this here. I chose to use tag functions to try something different, and also, it is actually the easiest way to pass variables inside our message.

Here is what the call would look like if we did not use a tag function:

console .info( 'This is a collection' , collection, ' and it is very nice. This is a number ' , numb, ' and it is also very nice' );

Our format function can be, for example, JSON.stringify .

Level

This level, aside from it being transport-specific, works exactly the same as the one inside the logger config.

Template

A template is a function that takes, as arguments, functions called Formatter s. Each Formatter returns a function that creates a chunk of our message by taking the Info object as an argument and returning a string.

Inside the Info object, we can find a lot of useful information. For example, for logger.info'This is message' that would be:

Level: info

The message: This is a message

Date of calling log : new Date()

: Place in the code where logger.info was called: log (/Users/primq/Repositories/loqqer/build/index.js:115:17) .

interface Info { date: Date; level: string; message: string; location: string; } type Formatter = (info: Info) => string; const format = { date: (format: string): Formatter => ({ date }) => moment(date).format(format), location: (): Formatter => ({ location }) => location, message: (): Formatter => ({ message }) => message, text: (message: string): Formatter => () => emoji.emojify(message), level: (): Formatter => ({ level }) => level.toUpperCase(), newLine: (): Formatter => () => '

', }; const createTemplate = (...fns: Formatter[]) => { return (info) => { return fns.reduce((prev, curr) => { return `${prev}${curr(info)}`; }, ''); }; };

Inside format.text , we use the node-emoji library, which lets us get the Unicode of emojis. They then can be rendered correctly in our terminal, our file, or anywhere else.

So, Here is a message :heart: , becomes Here is a message ❤️ .

It adds a little flavor to our logs and, for me, simply looks good.

Place in the code where logger.info was called…

Whenever we log something we may forget where the log was located — I know it is not a problem to find it, but still, it is interesting how one would go about finding it without searching manually.

If you think about it, we have this one way of revealing all the called functions just before the one we are in right now; it’s what we call a stack. We can gain access to the stack by throwing an error.

plug

200’s only Monitor failed and slow network requests in production Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, https://logrocket.com/signup/ Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause. LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free