Side note: Get a Node.js logs dashboard
Save hours of sifting through Node.js logs. Centralize with Better Stack and start visualizing your log data in minutes.
See the Node.js demo dashboard live.
Winston is the most popular logging library for Node.js. It aims to make logging more flexible and extensible by decoupling different aspects such as log levels, formatting, and storage so that each API is independent and many combinations are supported. It also uses Node.js streams to minimize the performance impact of implementing logging in your application.
In this tutorial, we will explain how to install, set up, and use the Winston logger in a Node.js application. We'll go through all the options it provides and show how to customize them in various ways. Finally, we'll describe how to use it in conjunction with Morgan middleware for logging incoming requests in Express server.
Before proceeding with the rest of this article, ensure that you have a recent
version of Node.js and npm
installed
locally on your machine. This article also assumes that you are familiar with
the basic concepts of logging in Node.js.
The final step in this tutorial discusses how to configure Winston to send logs in a centralized platform. This step is optional but recommended so that you can collect logs from multiple servers and consolidate the data in one place for easy access. You'll need to sign up for a free Logtail account if that's something you're interested in learning about.
Also, note that all the provided examples in this article are accurate for Winston 3.x. We will also endeavor to keep this article up to date for major releases of the framework in the future.
Save hours of sifting through Node.js logs. Centralize with Better Stack and start visualizing your log data in minutes.
See the Node.js demo dashboard live.
Winston is available as an npm package, so you can install it through the command below.
npm install winston
Next, import it into your Node.js application:
const winston = require('winston');
Although Winston provides a default logger that you can use by calling a level
method on the winston
module, it the recommended way to use it is to create a
custom logger through the createLogger()
method:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
Afterwards, you can start logging through methods on the logger
object:
logger.info('Info message');
logger.error('Error message');
logger.warn('Warning message');
This yields the following output:
{"level":"info","message":"Info message"}
{"level":"error","message":"Error message"}
{"level":"warn","message":"Warning message"}
In subsequent sections, we'll examine all the properties that you can use to
customize your logger
so that you will end up with an optimal configuration
for your Node.js application.
Winston supports six log levels by default. It follows the order specified by the RFC5424 document. Each level is given an integer priority with the most severe being the lowest number and the least one being the highest.
{
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
silly: 6
}
The six log levels above each correspond to a method on the logger:
logger.error('error');
logger.warn('warn');
logger.info('info');
logger.verbose('verbose');
logger.debug('debug');
logger.silly('silly');
You can also pass a string representing the logging level to the log()
method:
logger.log('error', 'error message');
logger.log('info', 'info message');
The level
property on the logger
determines which log messages will be
emitted to the configured transports (discussed later). For example, since the
level
property was set to info
in the previous section, only log entries
with a minimum severity of info
(or maximum integer priority of 2) will be
written while all others are suppressed. This means that only the info
,
warn
, and error
messages will produce output with the current configuration.
To cause the other levels to produce output, you'll need to change the value of
level
property to the desired minimum. The reason for this configuration is so
that you'll be able to run your application in production at one level (say
info
) and your development/testing/staging environments can be set to a less
severe level like debug
or silly
, causing more information to be emitted.
The accepted best practice for setting a log level is to use an environmental variable. This is done to avoid modifying the application code when the log level needs to be changed.
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
With this change in place, the application will log at the info
level if the
LOG_LEVEL
variable is not set in the environment.
Winston allows you to readily customize the log levels to your liking. The
default log levels are defined in winston.config.npm.levels
, but you can also
use the Syslog levels
through winston.config.syslog.levels
:
const logger = winston.createLogger({
levels: winston.config.syslog.levels,
level: process.env.LOG_LEVEL || 'info',
format: winston.format.cli(),
transports: [new winston.transports.Console()],
});
The syslog
levels are shown below:
{
emerg: 0,
alert: 1,
crit: 2,
error: 3,
warning: 4,
notice: 5,
info: 6,
debug: 7
}
Afterwards, you can log using the methods that correspond to each defined level:
winston.emerg("Emergency");
winston.crit("Critical");
winston.warning("Warning");
If you prefer to change the levels to a completely custom system, you'll need to
create an object and assign a number priority to each one starting from the most
severe to the least. Afterwards, assign that object to the levels
property in
the configuration object passed to the createLogger()
method.
const logLevels = {
fatal: 0,
error: 1,
warn: 2,
info: 3,
debug: 4,
trace: 5,
};
const logger = winston.createLogger({
levels: logLevels,
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
You can now use methods on the logger
that correspond to your custom log
levels as shown below:
logger.fatal('fatal!');
logger.trace('trace!');
Winston outputs its logs in JSON format by default, but it also supports other
formats which are accessible on the winston.format
object. For example, if
you're creating a CLI application, you may want to switch this to the cli
format which will print a color coded output:
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.cli(),
transports: [new winston.transports.Console()],
});
The formats available in winston.format
are defined in the
logform module. This module provides the
ability to customize the format used by Winston to your heart's content. You can
create a completely custom format or modify an existing one to add new
properties.
Here's an example that adds a timestamp
field to the each log entry:
const winston = require('winston');
const { combine, timestamp, json } = winston.format;
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(timestamp(), json()),
transports: [new winston.transports.Console()],
});
When you use a level method on the logger
, you'll see a datetime value
formatted as new Date().toISOString()
.
logger.info('Info message')
{"level":"info","message":"Info message","timestamp":"2022-01-25T15:50:09.641Z"}
You can change the format of this datetime value by passing an object to
timestamp()
as shown below. The string value of the format
property below
must be one acceptable by the
fecha module.
timestamp({
format: 'YYYY-MM-DD hh:mm:ss.SSS A', // 2022-01-25 03:23:10.350 PM
})
You can also create a entirely different format as shown below:
const winston = require('winston');
const { combine, timestamp, printf, colorize, align } = winston.format;
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(
colorize({ all: true }),
timestamp({
format: 'YYYY-MM-DD hh:mm:ss.SSS A',
}),
align(),
printf((info) => `[${info.timestamp}] ${info.level}: ${info.message}`)
),
transports: [new winston.transports.Console()],
});
logger.info('Info message');
logger.error('Error message');
logger.warn('Warning message');
The combine()
method merges multiple formats into one, while colorize()
assigns colors to the different log levels so that each level is easily
identifiable. The timestamp()
method outputs a datatime value that corresponds
to the time that the message was emitted. The align()
method aligns the log
messages, while printf()
defines a custom structure for the message. In this
case, it outputs the timestamp and log level followed by the message.
[2022-01-27 06:37:27.653 AM] info: Info message
[2022-01-27 06:37:27.656 AM] error: Error message
[2022-01-27 06:37:27.656 AM] warn: Warning message
While you can format your log entries in any way you wish, the recommended practice for server applications is to stick with a structured logging format (like JSON) so that your logs can be easily machine readable for filtering and gathering insights.
Transports in Winston refer to the storage location for your log entries. Winston provides great flexibility in choosing where you want your log entries to be outputted to. The following transport options are available in Winston by default:
Thus far, we've demonstrated the default Console transport to output log messages to the Node.js console. Let's look at how we can store logs in a file next.
const winston = require('winston');
const { combine, timestamp, json } = winston.format;
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(timestamp(), json()),
transports: [
new winston.transports.File({
filename: 'combined.log',
}),
],
});
logger.info('Info message');
logger.error('Error message');
logger.warn('Warning message');
The snippet above configures the logger
to output all emitted log messages to
a file named combined.log
. When you run the snippet above, this file will be
created with the following contents:
{"level":"info","message":"Info message","timestamp":"2022-01-26T09:38:17.747Z"}
{"level":"error","message":"Error message","timestamp":"2022-01-26T09:38:17.748Z"}
{"level":"warn","message":"Warning message","timestamp":"2022-01-26T09:38:17.749Z"}
In a production application, it may not be ideal to log every single message
into a single file as that will make filtering critical issues harder since it
will be mixed together with inconsequential messages. A potential solution would
be to use two File
transports, one that logs all messages to a combined.log
file, and another that logs messages with a minimum severity of error
to a
separate file.
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(timestamp(), json()),
transports: [
new winston.transports.File({
filename: 'combined.log',
}),
new winston.transports.File({
filename: 'app-error.log',
level: 'error',
}),
],
});
With this change in place, all your log entries will still be outputted to the
combined.log
file, but a separate app-error.log
will also be created and it
will contain only error
messages. Note that the level
property on the
File()
transport signifies the minimum severity that should be logged to the
file. If you change it from error
to warn
, it means that any message with a
minimum severity of warn
will be logged to the app-error.log
file (warn
and error
levels in this case).
A common need that Winston does not enable by default is the ability to log each
level into different files so that only info
messages go to an app-info.log
file, debug
messages into an app-debug.log
file, and so on (see
this GitHub issue). To get
around this, use a custom format on the transport to filter the messages by
level. This is possible on any transport (not just File
), since they all
inherit from
winston-transport.
const errorFilter = winston.format((info, opts) => {
return info.level === 'error' ? info : false;
});
const infoFilter = winston.format((info, opts) => {
return info.level === 'info' ? info : false;
});
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(timestamp(), json()),
transports: [
new winston.transports.File({
filename: 'combined.log',
}),
new winston.transports.File({
filename: 'app-error.log',
level: 'error',
format: combine(errorFilter(), timestamp(), json()),
}),
new winston.transports.File({
filename: 'app-info.log',
level: 'info',
format: combine(infoFilter(), timestamp(), json()),
}),
],
});
The above code logs only error
messages in the app-error.log
file and info
messages to the app-info.log
file. What happens is that the custom functions
(infoFilter()
and errorFilter()
) checks the level of a log entry and returns
false
if it doesn't match the specified level which causes the entry to be
omitted from the transport. You can customize this further or create other
filters as you see fit.
Logging into files can quickly get out of hand if you keep logging to the same file as it can get extremely large and become cumbersome to manage. This is where the concept of log rotation can come in handy. The main purpose of log rotation is to restrict the size of your log files and create new ones based on some predefined criteria. For example, you can create a new log file every day and automatically delete those older than a time period (say 30 days).
Winston provides the
winston-daily-rotate-file module
for this purpose. It is a transport that logs to a rotating file that is
configurable based on date or file size, while older logs can be auto deleted
based on count or elapsed days. Go ahead and install it through npm
as shown
below:
npm install winston-daily-rotate-file
Once installed, it may be used to replace the default File
transport as shown
below:
const winston = require('winston');
require('winston-daily-rotate-file');
const { combine, timestamp, json } = winston.format;
const fileRotateTransport = new winston.transports.DailyRotateFile({
filename: 'combined-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxFiles: '14d',
});
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(timestamp(), json()),
transports: [fileRotateTransport],
});
The datePattern
property controls how often the file should be rotated (every
day), and the maxFiles
property ensures that log files that are older than 14
days are automatically deleted. You can also listen for the following events on
a rotating file transport if you want to perform some action on cue:
// fired when a log file is created
fileRotateTransport.on('new', (filename) => {});
// fired when a log file is rotated
fileRotateTransport.on('rotate', (oldFilename, newFilename) => {});
// fired when a log file is archived
fileRotateTransport.on('archive', (zipFilename) => {});
// fired when a log file is deleted
fileRotateTransport.on('logRemoved', (removedFilename) => {});
Winston supports the ability to create your own transports or utilize one made by the community. A custom transport may be used to store your logs in a database, log management tool, or some other location. Here are some custom transports that you might want to check out:
Winston supports the addition of metadata to log messages. You can add default
metadata to all log entries, or specific metadata to individual logs. Let's
start with the former which can be added to a logger
instance through the
defaultMeta
property:
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
defaultMeta: {
service: 'admin-service',
},
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
logger.info('Info message');
logger.error('Error message');
When you log any message using the logger
above, the contents of the
defaultMeta
object will be injected into each entry:
{"level":"info","message":"Info message","service":"admin-service"}
{"level":"error","message":"Error message","service":"admin-service"}
This default metadata can be used to differentiate log entries by service or other criteria when logging to the same location from different services.
Another way to add metadata to your logs is by creating a child logger through
the child
method. This is useful if you want to add certain metadata that
should be added to all log entries in a certain scope. For example, if you add a
requestId
to your logs entries, you can search your logs and find the all the
entries that pertain to a specific request.
const childLogger = logger.child({ requestId: 'f9ed4675f1c53513c61a3b3b4e25b4c0' });
childLogger.info('Info message');
childLogger.info('Error message');
{"level":"info","message":"Info message","requestId":"f9ed4675f1c53513c61a3b3b4e25b4c0","service":"admin-service"}
{"level":"error","message":"Error message","requestId":"f9ed4675f1c53513c61a3b3b4e25b4c0","service":"admin-service"}
A third way to add metadata to your logs is to pass an object to the level method at its call site:
const childLogger = logger.child({ requestId: 'f9ed4675f1c53513c61a3b3b4e25b4c0' });
childLogger.info('File uploaded successfully', {
file: 'something.png',
type: 'image/png',
userId: 'jdn33d8h2',
});
{"file":"something.png","level":"info","message":"File uploaded successfully","requestId":"f9ed4675f1c53513c61a3b3b4e25b4c0","service":"admin-service","type":"image/png","userId":"jdn33d8h2"}
One of the most surprising behaviors of Winston for newcomers to the library is
in its way of handling errors. Logging an instance of the Error
object results
in an empty message:
const winston = require("winston");
const { combine, timestamp, json } = winston.format;
const logger = winston.createLogger({
level: "info",
format: combine(timestamp(), json()),
transports: [new winston.transports.Console()],
});
logger.error(new Error("an error"));
{"level":"error","timestamp":"2022-07-03T19:58:26.516Z"}
Notice how the message
property is omitted, and other properties of the
Error
(like its name
and stack
) are also not included in the output. This
can result in a nightmare situation where errors in production are not recorded
leading to lost time when troubleshooting.
Fortunately, you can fix this issue by importing and specifying the errors
format as shown below:
const winston = require("winston");
const { combine, timestamp, json, errors } = winston.format;
const logger = winston.createLogger({
level: "info",
format: combine(errors({ stack: true }), timestamp(), json()),
transports: [new winston.transports.Console()],
});
logger.error(new Error("an error"));
You will now get the proper output:
{"level":"error","message":"an error","stack":"Error: an error\n at Object.<anonymous> (/home/ayo/dev/betterstack/betterstack-community/demo/snippets/main.js:9:14)\n at Module._compile (node:internal/modules/cjs/loader:1105:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1159:10)\n at Module.load (node:internal/modules/cjs/loader:981:32)\n at Module._load (node:internal/modules/cjs/loader:827:12)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)\n at node:internal/main/run_main_module:17:47","timestamp":"2022-07-03T20:11:23.303Z"}
Winston provides the ability to automatically catch and log uncaught exceptions
and uncaught promise rejections on a logger. You'll need to specify the
transport where these events should be emitted to through the
exceptionHandlers
and rejectionHandlers
properties respectively:
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
exceptionHandlers: [
new winston.transports.File({ filename: 'exception.log' }),
],
rejectionHandlers: [
new winston.transports.File({ filename: 'rejections.log' }),
],
});
The logger
above is configured to log uncaught exceptions to an
exception.log
file, while uncaught promise rejections are placed in a
rejections.log
file. You can try this out by throwing an error somewhere in
your code without catching it.
throw new Error('An uncaught error');
You'll notice that the entire stack trace is included in the log entry for the exception, along with the date and message.
cat exception.log
{"date":"Mon Jun 06 2022 14:00:03 GMT+0100 (West Africa Standard Time)","error":{},"exception":true,"level":"error","message":"uncaughtException: An uncaught error\nError: An uncaught error\n at Object.<anonymous> (/home/ayo/dev/betterstack/betterstack-community/demo/snippets/main.js:15:7)\n at Module._compile (node:internal/modules/cjs/loader:1105:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1159:10)\n at Module.load (node:internal/modules/cjs/loader:981:32)\n at Module._load (node:internal/modules/cjs/loader:827:12)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)\n at node:internal/main/run_main_module:17:47","os":{"loadavg":[1.75,0.82,0.95],"uptime":271655.58},"process":{"argv":[". . ."],"cwd":"/home/ayo/dev/betterstack/betterstack-community/demo/snippets","execPath":"/home/ayo/.volta/tools/image/node/18.1.0/bin/node","gid":1000,"memoryUsage":{"arrayBuffers":110487,"external":465350,"heapTotal":11141120,"heapUsed":7620128,"rss":47464448},"pid":421995,"uid":1000,"version":"v18.1.0"},"stack":". . .","trace":[. . .]}
Winston will also cause the process to exit with a non-zero status code once it
logs an uncaught exception. You can change this by setting the exitOnError
property on the logger to false
as shown below:
const winston = require('winston');
const logger = winston.createLogger({ exitOnError: false });
// or
logger.exitOnError = false;
Note that the accepted best practice is to exit immediately after an uncaught error is detected as the program will be in an undefined state, so the above configuration is not recommended. Instead, you should let your program crash and set up a Node.js process manager (such as PM2) to restart it immediately while setting up some alerting mechanism to notify you of the problem (see section on Centralizing Logs below).
Winston also provides basic profiling capabilities on any logger through the
profile()
method. You can use it to collect some basic performance data in
your application hotspots in the absence of specialized tools.
// start a timer
logger.profile('test');
setTimeout(() => {
// End the timer and log the duration
logger.profile('test');
}, 1000);
The code above produces the following output:
{"durationMs":1001,"level":"info","message":"test"}
You can also use the startTimer()
method on a logger
instance to create a
new timer and store a reference to it in a variable. Then use the done()
method on the timer to halt it and log the duration:
// start a timer
const profiler = logger.startTimer();
setTimeout(() => {
// End the timer and log the duration
profiler.done({ message: 'Logging message' });
}, 1000);
{"durationMs":1001,"level":"info","message":"Logging message"}
The durationMs
property contains the timers' duration in milliseconds. Also,
log entries produced by the Winston profiler are set to the info
level by
default, but you can change this by setting the level
property in the argument
to profiler.done()
or logger.profile()
:
profiler.done({ message: 'Logging message', level: 'debug' });
// or
logger.profile('test', { level: 'debug' });
A large application will often have multiple loggers with different settings for
logging in different areas of the application. This is exposed in Winston
through winston.loggers
:
const winston = require('winston');
winston.loggers.add('serviceALogger', {
level: process.env.LOG_LEVEL || 'info',
defaultMeta: {
service: 'service-a',
},
format: winston.format.logstash(),
transports: [
new winston.transports.File({
filename: 'service-a.log',
}),
],
});
winston.loggers.add('serviceBLogger', {
level: process.env.LOG_LEVEL || 'info',
defaultMeta: {
service: 'service-b',
},
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
The serviceALogger
shown above logs to a service-a.log
file using the
built-in Logstash format, while serviceBLogger
logs to the console using the
JSON format. Once you've configured the loggers for each service, you can access
them in any file using the winston.loggers.get()
provided that you import the
configuration preferably in the entry file of the application:
require('./loggers.js');
const winston = require('winston');
const serviceALogger = winston.loggers.get('serviceALogger');
const serviceBLogger = winston.loggers.get('serviceBLogger');
serviceALogger.error('logging to a file');
serviceBLogger.warn('logging to the console');
node main.js
{"level":"warn","message":"logging to the console","service":"service-b"}
cat service-a.log
{"@fields":{"level":"error","service":"service-a"},"@message":"logging to a file"}
Morgan is an HTTP request logger middleware for Express that automatically logs the details of incoming requests to the server (such as the remote IP Address, request method, HTTP version, response status, user agent, etc.), and generate the logs in the specified format. The main advantage of using Morgan is that it saves you the trouble of writing a custom middleware for this purpose.
Here's a simple program demonstrating Winston and Morgan being used together in an Express application. When you connect Morgan with a Winston logger, all your logging is formatted the same way and goes to the same place.
const winston = require('winston');
const express = require('express');
const morgan = require('morgan');
const axios = require('axios');
const app = express();
const { combine, timestamp, json } = winston.format;
const logger = winston.createLogger({
level: 'http',
format: combine(
timestamp({
format: 'YYYY-MM-DD hh:mm:ss.SSS A',
}),
json()
),
transports: [new winston.transports.Console()],
});
const morganMiddleware = morgan(
':method :url :status :res[content-length] - :response-time ms',
{
stream: {
// Configure Morgan to use our custom logger with the http severity
write: (message) => logger.http(message.trim()),
},
}
);
app.use(morganMiddleware);
app.get('/crypto', async (req, res) => {
try {
const response = await axios.get(
'https://api2.binance.com/api/v3/ticker/24hr'
);
const tickerPrice = response.data;
res.json(tickerPrice);
} catch (err) {
logger.error(err);
res.status(500).send('Internal server error');
}
});
app.listen('5000', () => {
console.log('Server is running on port 5000');
});
The morgan()
middleware function takes two arguments: a string describing the
format of the log message and the configuration options for the logger. The
format is composed up of
individual tokens, which can be
combined in any order. You can also use a
predefined format
instead. In the second argument, the stream
property is set to log the
provided message using our custom logger at the http
severity level. This
reflects the severity that all Morgan events will be logged at.
Before you execute this code, install all the required dependencies first:
npm install winston express morgan axios
Afterward, start the server and send requests to the /crypto
route through the
commands below:
node server.js
curl http://localhost:5000/crypto
You should observe the following output:
{"level":"http","message":"GET /crypto 200 1054176 - 3553.998 ms","timestamp":"2022-06-06 03:51:56.336 PM"}
{"level":"http","message":"GET /crypto 200 1054224 - 2745.956 ms","timestamp":"2022-06-06 03:52:00.530 PM"}
{"level":"http","message":"GET /crypto 200 1054214 - 3605.577 ms","timestamp":"2022-06-06 03:52:10.774 PM"}
Notice that the all the request metadata outputted by Morgan is placed as a
string in the message
property which makes it harder to search and filter.
Let's configure Morgan such that the message string will be a stringified JSON
object.
. . .
const morganMiddleware = morgan(
function (tokens, req, res) {
return JSON.stringify({
method: tokens.method(req, res),
url: tokens.url(req, res),
status: Number.parseFloat(tokens.status(req, res)),
content_length: tokens.res(req, res, 'content-length'),
response_time: Number.parseFloat(tokens['response-time'](req, res)),
});
},
{
stream: {
// Configure Morgan to use our custom logger with the http severity
write: (message) => {
const data = JSON.parse(message);
logger.http(`incoming-request`, data);
},
},
}
);
. . .
The string argument to the morgan()
function has been changed to a function
that returns a stringified JSON object. In the write()
function, the JSON
string is parsed and the resulting object is passed as metadata to the
logger.http()
method. This ensures that each metric is produced as a separate
property in the log entry.
Restart the server and send requests to the /crypto
route once again. You'll
observe the following output:
{"content_length":"1054555","level":"http","message":"incoming-request","method":"GET","response_time":2974.763,"status":200,"timestamp":"2022-06-06 08:46:41.267 PM","url":"/crypto"}
We already discussed logging to files in a previous section of this tutorial. It's a great way to persist your log entries for later examination, but may be insufficient for distributed applications running on multiple servers since looking at the logs of a single server may not longer enough to locate and diagnose problems.
A widely employed solution in such cases is to collect the logs from individual servers and centralize them one place so that its easy to get a complete picture on any issue. There are several solutions for aggregating logs such as the open source Elasticsearch, Logstash, and Kibana (ELK) stack but a reliable setup can be convoluted especially if you're opting for a self-hosted solution.
The simplest, and often more cost-effective way to centralized logs is to use a
cloud-based service. Most services offer log filtering, alerting, and an option
for unlimited log storage which could help with spotting long-term trends. In
this section, we will briefly discuss how to send Node.js application logs to
one such service (Logtail) when using the
Winston logger. We'll demonstrate this process using the server.js
example
from the previous section.
The first step involves creating or logging in to your Logtail account, then find the Sources option in the left-hand menu and click the Connect source button on the right.
Give your source a name and select the Node.js platform, then click the Create source button.
Once your Logtail source is created, copy the Source token to your clipboard.
Return to your terminal and install the @logtail/node and @logtail/winston packages in your Node.js project:
npm install @logtail/node @logtail/winston
Afterward, make the following changes to your server.js
file:
. . .
const axios = require('axios');
const { Logtail } = require('@logtail/node');
const { LogtailTransport } = require('@logtail/winston');
const logtail = new Logtail('<your_source_token>');
const { combine, timestamp, json } = winston.format;
const app = express();
const logger = winston.createLogger({
level: 'http',
format: combine(
timestamp({
format: 'YYYY-MM-DD hh:mm:ss.SSS A',
}),
json()
),
transports: [new winston.transports.Console(), new LogtailTransport(logtail)],
});
. . .
The Logtail
class is imported and initialized with your source token (replace
the <your_source_token>
placeholder). In a real application, you should use an
environmental variable to control the source token value. The LogtailTransport
class is a Winston compatible transport that transmits Winston logs to Logtail
when initialized in the transports
array as above.
After saving the server.js
file above, restart your server and send a few
requests to the /crypto
route. Your logs will continue to appear in the
console as before, but it will also sync to Logtail.com in realtime. Head over
to your browser and click the Live tail link under your source name, or you
can click the Live tail menu entry on the left and filter the logs using the
dropdown on the top left:
You should observe that your Winston-generated application logs are coming through as expected. You can click a log entry to expand its properties:
From this point onwards, all your application logs from any server or environment will be centralized in Logtail. You can easily apply filters to find the information you need or set up alerts to notify you whenever your logs match certain conditions. For more information on integrating Logtail in your application, explore the full documentation.
In this article, we've examined the Winston logging package for Node.js applications. It provides everything necessary in a logging framework, such as structured (JSON) logging, colored output, log levels, and the ability to log to several locations. It also has a simple API and is easy to customize which makes it a suitable solution for any type of project.
We hope this article has helped you learn about everything that you can do with Winston, and how it may be used to develop a good logging system in your application. Don't forget to check out it's official documentation pages to learn more.
Thanks for reading, and happy coding!
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for usWrite a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github