This text provides a guide on how to use Winston as a logging system for NestJS applications, replacing the default NestJS logger, and customizing it for production-grade log management systems.
Abstract
The text begins by explaining the limitations of the default NestJS logger and the advantages of using Winston as an alternative. It then proceeds to demonstrate how to install and configure Winston for NestJS applications, including creating a new logger module, attaching it to the app's module, and logging events. The guide also covers how to make the logs consistent by providing a new NestJS-Logger-based service that can be attached as an application logger for the core NestJS modules.
The text then moves on to configuring the output format of the logs, including adding formatters for pretty console output and error stack formatting. It also covers how to automatically log HTTP requests using a new NestJS middleware. The guide concludes with a summary of the workflow and best practices for creating a logging system for NestJS applications.
Bullet points
The default NestJS logger is limited and does not offer production-grade solutions.
Winston can be used as an alternative to the default NestJS logger, providing a large set of configuration options and customizable logging system.
The guide demonstrates how to install and configure Winston for NestJS applications, including creating a new logger module and attaching it to the app's module.
The guide covers how to make the logs consistent by providing a new NestJS-Logger-based service that can be attached as an application logger for the core NestJS modules.
The text covers how to configure the output format of the logs, including adding formatters for pretty console output and error stack formatting.
The guide demonstrates how to automatically log HTTP requests using a new NestJS middleware.
The guide concludes with a summary of the workflow and best practices for creating a logging system for NestJS applications.
NestJS - Monitoring - Logging (Winston)
In this section, we’ll introduce a new Logger Module (based on Winston) that can help our NestJS logs work well with production-grade log management systems (e.g. Grafana).
The first question that comes to mind is why use Winston to replace the default NestJS Logger that plays out of the box?
The reasoning is that the default NestJS Logger interface is a bit constrained and primitive and mainly focuses on giving an example of such a logging flow instead of offering a production-grade solution. We can’t configure its output format, we can’t provide metadata to our logs, and so on. Winston on the other hand doesn’t have these issues and comes with a large set of configuration options that can help us create a fully customizable logging system that works well with our desired log management system.
This section is part of a larger guide. You can follow it from the beginning or just complete the only prerequisite step (getting started).
At this point, you should have a fully operational NestJS project to follow the upcoming steps.
Introducing the new logger module
Now, let’s start by installing the needed packages:
npm install winston @nestjs/config
We’ll start with a new NestJS config file that will help us configure the Winston logger instance at its instantiation.
This defines two operation modes (based on whether NODE_ENV equals 'production'), the production mode that displays the logs as JSON objects with timestamps, and the development mode that displays the logs as simple strings. Note that Winston can help us log metadata (in addition to the message and context fields that the default NestJS logger uses), so we also define some fields as default metadata (expecting the code to override some of them when logging stuff).
Now we can proceed with the new Logger module implementation:
The new Winston logger is created at logger.factory.ts, which simply takes the predefined configuration, alongside a new context default value and passes it to the instance. The context value is the callee’s constructor name (check inquirer-injection-provider for more details).
Now that the module is ready to be used by our application, let’s attach it to our app’s module and log some stuff (we’ll logOnApplicationBootstrap event, check lifecycle-events).
So now that everything is set up, here is the log output when the system runs:
Notice that the app outputs logs in two different formats, one format comes from the core NestJS modules (default logger), and one format comes from our new onApplicationBootstrap log.
Let’s work towards providing a new NestJS-Logger-based service that can also be attached as an application logger for the core NestJS modules. This can help us make the logs consistent.
The new service implements LoggerService interface and can be attached to the application in the same way as the default logger. You can do so like this:
Note: this new logger will only be used by the NestJS core modules. The app modules should instead use the logger that provides the Winston interface (@Inject(LOGGER) private logger: Logger).
Current output:
developmentproduction
Configuring output format
Now that the entire system uses Winston for logs, we can upgrade the format with some extra formatters:
pretty-console-formatter.util.ts is used in the development environmentand makes the log output similar to the core NestJS Logger.
error-stack-formatter-inline.util.ts is used in the development environment (only for error logs) and extracts error stack from the metadata (stack, error.stack or err.stack) and appends it to the message.
error-stack-formatter.util.ts is used in the production environment(only for error logs) and extracts error stack from the metadata error fields (error or err) and stores at stack field (only if it doesn’t previously exist). This is very useful for issues that occur in the production environment.
Development output without pretty formatter:
Development output with pretty formatter:
Without error stack formatter:
development logs without error stack formatterproduction logs without error stack formatter
With error stack formatter:
development logs with error stack formatterproduction logs with error stack formatter
Logging HTTP Requests
Now that our application logic can log stuff, we can focus on something a bit more practical, to provide a way to automatically log HTTP requests. We’ll provide a new NestJS middleware and install it to all our HTTP routes.
If we send a request now we see the corresponding logs
Although the current middleware is functional and can log HTTP calls, we occasionally need to also see the body, query, and cookie data of the request. We’ll upgrade the module to provide new configuration options and the middleware to utilize these settings to log the aforementioned data.
New output:
HTTP Logs
Summary
So, let’s do a recap and revisit what we have created until now! This module introduces 2 different loggers that play on top of Winston, one for the application logic (@Inject(LOGGER) private logger: Logger), and one for core NestJS modules (NestJSLoggerService) and a middleware that logs incoming HTTP requests.
You can find the current Winston settings under config/logger.config.ts and change the logger behavior with environment variables.
NODE_ENV: ‘production’ => JSON output for easier searches using your logging system (e.g. Azure Log Analytic Workspace). other => NestJS-like console output.
LOGGER_MIN_LEVEL: The desired minimum log level.
LOGGER_DISABLE: By setting this to ‘true’, you can disable logs entirely.
SERVICE_NAME: The ‘service’ metadata field default value.
LOGGER_HTTP_BODY_ENABLED: By setting it to true, you can enable HTTP logs to include the ‘body’ field of the req.
LOGGER_HTTP_BODY_MAX_SIZE: The maximum allowed ‘body’ size that will be logged (this is used to prevent the system from logging a very large set of data).
LOGGER_HTTP_QUERY_ENABLED: By setting this to true, you can enable HTTP logs to include the ‘query’ field of the req.
LOGGER_HTTP_QUERY_MAX_SIZE: The maximum allowed ‘query’ size that will be logged.
LOGGER_HTTP_COOKIES_ENABLED: By setting this to true, you can enable HTTP logs to include the ‘cookies’ field of the req.
LOGGER_HTTP_COOKIES_MAX_SIZE: The maximum allowed ‘cookies’ size that will be logged.
For better logs, this logger module expects some provided meta key/values:
layer: Indicates whether the log comes from a specific logic layer (e.g. ‘Nest’ => internal NestJS system). By default, is set to 'App' for the application logger and to 'Nest' for the NestJS logger.
service: The name of the service that outputs these logs (useful when using this module on multiple microservices). Can be set via an environment variable (SERVICE_NAME).
context: The context of the log. By default, the context is the constructor name of the parent class.
type: This is a unique identifier that the callee can pass (as best practice), in order to be able to query the collection of logs and find the desired location in the code.
(e.g. logger.info('Random log', { type: 'RANDOM_LOG' })).
Final Thoughts
The goal of this guide is not to provide a jack-of-all-trades logger module, but instead, to showcase the workflow on how to create such a system, resolve various issues that you might face down the path, and of course showcase, some of the proposed best practices that you can follow when your code is logging stuff up (e.g. always provide a type meta field).
Feel free to use it as it is, change/upgrade it based on your needs, or even follow the workflow to create your own logging module based on other technologies (e.g. Pino, Bunyan).
Finally, you can find here a full-fledged example, alongside various other modules that you might need to create a production application.