Appearance
WinstonModule
The WinstonModule for NestJS provides a convenient way to integrate the popular logging library Winston into your NestJS application.
How to install WinstonModule to my app
To install the WinstonModule
to your app, follow these steps:
First, install the required dependencies by running the following command in your terminal:
bashnpm install --save winston
Logs Writer
You need to setup
Logs Writer
role for your service account in Google Cloud IAM.Set up the
WinstonConfig
using the providedcreateWinstonConfig
method:ts// config/index.ts export const WinstonConfig = createWinstonConfig({ appName: 'Jetstream', // Display in log messages useConsole: process.env.APP_ENV === AppEnv.LOCAL, useGoogleCloudLogging: process.env.APP_ENV !== AppEnv.LOCAL, googleCloudLoggingOptions: { logName: 'jetstream_core', // Google Logging source name }, });
Next, import the
WinstonModule
into your mainAppModule
:ts// src/app.module.ts import { WinstonModule } from '@deeepvision/nest-kit'; import { ConfigModule, ConfigType } from '@nestjs/config'; import * as configs from '@/config'; @Module({ imports:[ // Other modules // Configure the WinstonModule with Winston config WinstonModule.forRootAsync({ inject: [configs.WinstonConfig.KEY], useFactory: async (opts: ConfigType<typeof configs.WinstonConfig>) => opts, }), ], ... }) export class AppModule {}
How to use WinstonLogger
NestKit offers the WinstonLogger class for enhanced logging capabilities. You can create a logger instance using a factory within your service or resolver to streamline logging throughout your application.
ts
@Injectable()
export class BooksService {
logger = this.loggerFactory.create({
scope: BooksService.name,
});
constructor(
@InjectWinstonLoggerFactory() private readonly loggerFactory: WinstonLoggerFactory,
) {}
async getOne(id: string): Promise<MaybeNull<Book>> {
return this.bookRepository.findOneBy({
id,
});
}
async getOneOrFail(id: string, ctx: ServiceMethodContext): Promise<Book> {
const logger = this.logger.forMethod(this.getOneOrFail.name, ctx, {
id,
});
const book = await this.getOne(id);
if (!book) {
throw new NotFoundException({
message: `Book with id ${id} not found`,
key: ErrorKeys.LIB_BOOKS_NOT_FOUND,
context: logger.getContext(),
});
}
return book;
}
How to provide logging context
When providing logging context for each action, you can create a child logger with static context data. For example, WinstonLogger.forMethod serves as a convenient shorthand for the WinstonLogger.child method, allowing you to streamline the process of creating context-aware loggers for specific actions in your application.
ts
const logger = this.logger.forMethod(this.getOneOrFail.name, ctx, {
id,
});
// is the same as...
const logger = this.logger.child({
action: 'getOneOrFail',
requestId: ctx.requestId,
userId: ctx.user.id,
id,
});
When logging a message like this:
ts
logger.info(`Try to find book with id ${id}`);
You will see the following output:
plaintext
[DeepVision Library] - 23.11.2022, 18:23:00 INFO [BooksService -> getOneOrFail] Try to find book with id libb:j8gk36hvn3h -- {
id: 'libb:j8gk36hvn3h',
requestId: 'libreq:xxkcowkdf1012gjhg023g330vlhope',
userId: 'libu:jhkgrl2vnmy'
}
Adding Context Data to Logger Instance
To add context data to a logger instance, you can use the setContext method as shown in the example below:
ts
const logger = this.logger.forMethod(this.transfer.name, ctx, {
id,
toLibraryId: opts.libraryId
});
const book = await this.getOneOrFail(id, ctx);
// Adding context data to logger instance
logger.setContext({
fromLibraryId: book.libraryId,
});
logger.info(`Transferring book ${id}...`);
You will see the following output:
plaintext
[DeepVision Library] - 23.11.2022, 18:26:00 INFO [BooksService -> transfer] Transfer book libb:j8gk36hvn3h... -- {
id: 'libb:j8gk36hvn3h',
requestId: 'libreq:xnbcowkdf1012gjh2cbn3pjlpe',
userId: 'libu:jhkgrl2vnmy'
fromLibraryId: libl:kg829576gkp2,
toLibraryId: 'libl:mbirot785vd'
}
In this example, we first create a logger instance using the forMethod
method provided by the logger service. Then, we obtain a book object by calling the getOneOrFail
method.
After that, we use the setContext
method to add context data to the logger instance. The setContext
method takes an object as its input parameter, which should contain the context data to be added.
Finally, we log an info message using the logger instance, which includes the book ID
that is being transferred.
Alternatively, you can pass context data as the second argument to the WinstonLogger.info method. This data will be printed in the log message, but it won't be saved in the static context.
For example:
ts
this.logger.info(`User ${userId} successfully logged in`, { ip: userIp });
In this example, we log a message using the info method provided by the WinstonLogger
instance. The first argument is the log message, and the second argument is an object literal that contains the context data we want to add to the log message. Here, we include the IP address of the user who logged in.
Errors logging
To catch errors and log them using the WinstonLogger.error, you can use the following approach:
ts
try {
await this.bookRepository.save(book);
} catch (error) {
throw new InternalServerErrorException({
message: 'Failed to create book',
key: ErrorKeys.LIB_BOOKS_CREATE_FAILED,
context: logger.getContext(),
error,
});
}
In this example, we catch an error that occurred while saving a book to the database. We then throw an InternalServerErrorException
with an object literal as its input parameter, which contains the error details along with other contextual information. This exception will be caught and handled by the HttpExceptionFilter
.
To set the HttpExceptionFilter
as a global filter in the main.ts
file, you can use the following code:
ts
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
NESTKIT_WINSTON_LOGGER_FACTORY_PROVIDER,
NESTKIT_WINSTON_SYSTEM_LOGGER_PROVIDER,
HttpExceptionFilter,
} from '@deeepvision/nest-kit';
import { config as useDotEnv } from '@deeepvision/dotenv-yaml';
if (process.env.APP_ENV === AppEnv.LOCAL) {
useDotEnv();
}
const bootstrap = async () => {
const app = await NestFactory.create(AppModule, serverOptions);
app.useLogger(app.get(NESTKIT_WINSTON_SYSTEM_LOGGER_PROVIDER));
app.enableCors({
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
});
const { httpAdapter } = app.get(HttpAdapterHost);
const loggerFactory = app.get(NESTKIT_WINSTON_LOGGER_FACTORY_PROVIDER);
// Set HttpExceptionFilter from NestKit for errors interception
app.useGlobalFilters(new HttpExceptionFilter(httpAdapter, loggerFactory));
...
}
In this example, we obtain an instance of the httpAdapter
and loggerFactory
, and then we set the HttpExceptionFilter
as a global filter using the app.useGlobalFilters
method. This will allow the filter to catch and handle exceptions that are thrown from any part of the application, including the GraphQL or REST interfaces.
How to catch errors that occur outside of requests
To catch errors that occur outside of requests, such as those in eventemitter
handlers or internal worker loops, you can use the logger.error
method. Here's an example:
ts
@OnEvent(UserUpdatedEvent.id)
async handleUserUpdatedEvent(event: UserUpdatedEvent) {
const { user, ctx } = event;
const logger = this.logger.forMethod(this.handleUserUpdatedEvent.name, ctx, {
updatedUserId: user.id
});
...
try {
...
} catch (error) {
if (error instanceof Error) {
logger.error(error);
// or
logger.error('Failed to handle user update', error);
}
}
}
In this example, we have an OnEvent
decorator that listens to a UserUpdatedEvent
. We obtain the user and context data from the event object, and then we create a logger instance using the forMethod method.
Inside the handleUserUpdatedEvent
method, we perform some operations that might throw errors. If an error occurs, we catch it using a try...catch
block, and then we log it using the logger.error
method. This method takes an error object or a string message as its input parameter. Here, we use the error object as the input parameter, but you can also pass a custom error message along with the error object if needed.
By logging errors using the logger.error
method, you can easily track down issues that occur outside of requests and ensure that they are properly handled.