Unable to inject winston's logger instance with NestJS - node.js

I'm using NestJS 7.0.7 and Winston 3.2.1 (with nest-winston 1.3.3).
I'm trying to integrate Winston into NestJS, but so far, I'm unable to inject a logger instance (to actually log anything) into any controller/service.
Since I would like to use Winston across the application AND during bootstrapping, I'm using the approach as the main Nest logger:
// main.ts
import { NestFactory } from "#nestjs/core";
import { WinstonModule } from "nest-winston";
import { format, transports } from "winston";
import { AppModule } from "./app.module";
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
exitOnError: false,
format: format.combine(format.colorize(), format.timestamp(), format.printf(msg => {
return `${msg.timestamp} [${msg.level}] - ${msg.message}`;
})),
transports: [new transports.Console({ level: "debug" })], // alert > error > warning > notice > info > debug
}),
});
app.use(helmet());
await app.listen(process.env.PORT || 3_000);
}
bootstrap().then(() => {
// ...
});
I'm not doing anything in regard to the logging in app.module.ts:
// app.module.ts
import { SomeController } from "#controller/some.controller";
import { Module } from "#nestjs/common";
import { SomeService } from "#service/some.service";
#Module({
controllers: [SomeController],
imports: [],
providers: [SomeService],
})
export class AppModule {
// ...
}
// some.controller.ts
import { Controller, Get, Inject, Param, ParseUUIDPipe, Post } from "#nestjs/common";
import { SomeService } from "#service/some.service";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";
import { Logger } from "winston";
#Controller("/api/some-path")
export class SomeController {
constructor(#Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, private readonly service: SomeService) {
// ...
}
...
}
The application tries to start but fails at some point:
2020-04-06T18:51:08.779Z [info] - Starting Nest application...
2020-04-06T18:51:08.787Z [error] - Nest can't resolve dependencies of the SomeController (?, SomeService). Please make sure that the argument winston at index [0] is available in the AppModule context.
Potential solutions:
- If winston is a provider, is it part of the current AppModule?
- If winston is exported from a separate #Module, is that module imported within AppModule?
#Module({
imports: [ /* the Module containing winston */ ]
})

Try importing the WinstonModule in the root AppModule, as explained in the official docs: https://github.com/gremo/nest-winston:
import { Module } from '#nestjs/common';
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
const logger: LoggerConfig = new LoggerConfig();
#Module({
imports: [WinstonModule.forRoot(logger.console())],
})
export class AppModule {}
It's probably a good idea to create some kind of factory/Logging-Config in order not to have to duplicate the logger options.
import winston, { format, transports } from "winston";
export class LoggerConfig {
private readonly options: winston.LoggerOptions;
constructor() {
this.options = {
exitOnError: false,
format: format.combine(format.colorize(), format.timestamp(), format.printf(msg => {
return `${msg.timestamp} [${msg.level}] - ${msg.message}`;
})),
transports: [new transports.Console({ level: "debug" })], // alert > error > warning > notice > info > debug
};
}
public console(): object {
return this.options;
}
}

Implement Winston custom logger in NestJs project
Prerequisit:
npm install --save nest-winston winston winston-daily-rotate-file
import { NestFactory } from '#nestjs/core';
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import * as winstonDailyRotateFile from 'winston-daily-rotate-file';
import { AppModule } from './app.module';
const transports = {
console: new winston.transports.Console({
level: 'silly',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.colorize({
colors: {
info: 'blue',
debug: 'yellow',
error: 'red',
},
}),
winston.format.printf((info) => {
return `${info.timestamp} [${info.level}] [${
info.context ? info.context : info.stack
}] ${info.message}`;
}),
// winston.format.align(),
),
}),
combinedFile: new winstonDailyRotateFile({
dirname: 'logs',
filename: 'combined',
extension: '.log',
level: 'info',
}),
errorFile: new winstonDailyRotateFile({
dirname: 'logs',
filename: 'error',
extension: '.log',
level: 'error',
}),
};
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useLogger(
WinstonModule.createLogger({
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json(),
),
transports: [
transports.console,
transports.combinedFile,
transports.errorFile,
],
}),
);
await app.listen(4000);
}
bootstrap();
NestJs Custom Logger
NestJs Winston NPM Documentation
Note Log Levels, file names, dateformat you may edit as per your requirement. Follow officials documentations with more option.

Related

How to sent log to Jaeger in NestJs?

In my nestjs project, I am using Winston as a logger. here is the example of my log.info:
logger.log(
'Error: test',
inout_data,
);
here is the logger:
var winston = require('winston');
const logger = winston.createLogger({
level: 'verbose',
format: winston.format.json(),
transports: [
new winston.transports.Console({
format: winston.format.json(),
}),
],
});
export default logger;
this is how I setup the Jaeger:
import { NodeSDK, tracing } from '#opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '#opentelemetry/auto-instrumentations-node';
import { JaegerExporter, ExporterConfig } from '#opentelemetry/exporter-jaeger';
import { BatchSpanProcessor } from '#opentelemetry/sdk-trace-base';
import { Resource } from '#opentelemetry/resources';
import { SemanticResourceAttributes } from '#opentelemetry/semantic-conventions';
interface Tag {
key: string;
value: TagValue;
}
declare type TagValue = string | number | boolean;
const options = {
tags: [], // optional
host: 'localhost',
port: 6832, // optional
maxPacketSize: 65000,
};
const exporter = new JaegerExporter(options);
export const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'test,
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: '2',
}),
traceExporter: exporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
I want to send the log to Jaeger, but I can not find a way to connect them. Am I missing anything here?

How to get class name in logs in which winston logger is called

I am creating NestJS application in which I am using winston logger for logging purposes.I am getting logs inside log file but along with logs I also want the class name in which logger is called so that it can easily be identified which logs are producing by which class.
Below is my winston setup:
app.module.ts
import { Module } from '#nestjs/common';
import { WinstonModule,utilities as nestWinstonModuleUtilities } from 'nest-winston';
import * as winston from 'winston';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TestModule } from './test/test.module';
import * as path from 'path';
#Module({
imports: [ WinstonModule.forRoot({
transports: [
new winston.transports.Console({
level: 'info',
format:winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({
format: 'DD-MM-YYYY HH:mm:ss'
}),
winston.format.printf(
info => `${info.timestamp} ${info.level}: ${info.message}`
),
)
}),
new winston.transports.File({
dirname: path.join(__dirname, '../log/info/'), //path to where save logging result
filename: 'info.log', //name of file where will be saved logging result
level: 'info',
format:winston.format.combine(
winston.format.timestamp({
format: 'DD-MM-YYYY HH:mm:ss'
}),
winston.format.printf(
info => `${info.timestamp} ${info.level}: ${info.message}`
)
),
}),
],
}), TestModule,],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
app.service.ts
import { Inject, Injectable } from '#nestjs/common';
import { Logger } from 'winston';
#Injectable()
export class AppService {
constructor( #Inject('winston') private readonly logger: Logger){}
getHello(): string {
this.logger.info('Hello logs');
return 'Hello World!';
}
}
Here logger is called by AppService class so I want this classname also in logs.How can I achieve desired result
It's possible by not injecting the logger, but directly creating a new instance.
export class AppService {
private readonly logger = new Logger(AppService.name);
constructor(...)
}
Unfortunately you loose the advantages of dependency injection. For exmaple you need special way to mock it in tests. But it's possible.
It still works if you registered a custom logger. For example when you are using winston logger, the solution will also get you the Winton Logger.:
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
exitOnError: false,
format: format.json(),
transports: [
new transports.Console({
format: format.combine(
format.simple(),
format.timestamp(),
format.ms(),
format.errors({ stack: true }),
utilities.format.nestLike()
)
})
],
level: process.env.WINSTON_LOGGING ?? 'info'
})
});
You can mock it the same way, for example for tests:
const moduleFixture: TestingModule = await Test.createTestingModule({
// ...
}).compile();
const app = moduleFixture.createNestApplication();
app.useLogger(loggerMock);

process.env's are undefined - NestJS

I've decided to write here because I've ran out of ideas. I have a NestJS app in which I use env's - nothing unusual. But something strange happens when I want to use them. I also have my own parser of these values which returns them in a convenient object - that's the first file:
env.ts
const parseStringEnv = (name: string) => {
const value: string = process.env[name];
if (!value) {
throw new Error(`Invalid env ${name}`);
}
return value;
};
const parseIntEnv = (name: string) => {
const value: string = process.env[name];
const int: number = parseInt(value);
if (isNaN(int)) {
throw new Error(`Invalid env ${name}`);
}
return int;
};
const parseBoolEnv = (name: string) => {
const value: string = process.env[name];
if (value === "false") {
return false;
}
if (value === "true") {
return true;
}
throw new Error(`Invalid env ${name}`);
};
const parseMongoString = (): string => {
const host = parseStringEnv("DATABASE_HOST");
const port = parseStringEnv("DATABASE_PORT");
const user = parseStringEnv("DATABASE_USER");
const pwd = parseStringEnv("DATABASE_PWD");
const dbname = parseStringEnv("DATABASE_NAME");
return `mongodb://${user}:${pwd}#${host}:${port}/${dbname}?authSource=admin&ssl=false`;
};
export const env = {
JWT_SECRET: parseStringEnv("JWT_SECRET"),
PORT_BACKEND: parseIntEnv("PORT_BACKEND"),
CLIENT_HOST: parseStringEnv("CLIENT_HOST"),
ENABLE_CORS: parseBoolEnv("ENABLE_CORS"),
MONGO_URI: parseMongoString(),
};
export type Env = typeof env;
I want to use it for setting port on which the app runs on and also the connection parameters for Mongoose:
In main.ts:
<rest of the code>
await app.listen(env.PORT_BACKEND || 8080);
<rest of the code>
Now, the magic starts here - the app starts just fine when ONLY ConfigModule is being imported. It will also start without ConfigModule and with require('doting').config() added. When I add MongooseModule, the app crashes because it can't parse env - and the best thing is that exception thrown has nothing to do with env's that are used to create MONGO_URI!! I'm getting "Invalid env JWT_SECRET" from my parser.
In app.module.ts
import { Module } from "#nestjs/common";
import { ConfigModule } from "#nestjs/config";
import { MongooseModule } from "#nestjs/mongoose";
import { AppController } from "./app.controller";
import { env } from "./common/env";
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
MongooseModule.forRoot(env.MONGO_URI), //WTF?
],
controllers: [AppController],
})
export class AppModule {}
I've honestly just ran out of ideas what could be wrong. The parser worked just fine in my last project (but I haven't used Mongoose so maybe that's what causes issues). Below is my .env file template.
JWT_SECRET=
ENABLE_CORS=
PORT_BACKEND=
DATABASE_HOST=
DATABASE_PORT=
DATABASE_USER=
DATABASE_PWD
DATABASE_NAME=
CLIENT_HOST=
Thanks for everyone who has spent their time trying to help me ;)
What's happening is you're importing env.ts before the ConfigModule has imported and set the variables in your .env file.
This is why calling require('dotenv').config() works. Under the hood, that's what the ConfigModule is doing for you. However, your call to ConfigModule.forRoot is happening after you import env.ts, so the .env file hasn't been imported yet and those variables don't yet exist.
I would highly recommend you take a look at custom configuration files, which handles this for you the "Nest way":
From the Nest docs, but note that you could also use the env.ts file you already have:
// env.ts
export default () => ({
// Add your own properties here however you'd like
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432
}
});
And then modify your AppModule to the following. Note that we're using the forRootAsync so that we can get a handle to the ConfigService and grab the variable from that.
// app.module.ts
import configuration from './common/env';
#Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
}),
//
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('MONGO_URI'),
}),
inject: [ConfigService],
});
],
})
export class AppModule {}
As an alternative, you could also just call require('dotenv').config() inside your env.ts file at the top, but you'll miss out on all the ConfigModule helpers like dev/prod .env files.
By using registerAsync of JWT module and read process.env inside useFactory method worked for me
#Module({
imports: [
JwtModule.registerAsync({
useFactory: () => ({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: 3600 },
}),
})
],
controllers: [AppController],
})
In my case just need to replace the order import module.
import { Module } from "#nestjs/common";
import { ConfigModule } from "#nestjs/config";
import { MongooseModule } from "#nestjs/mongoose";
import { AppController } from "./app.controller";
import { env } from "./common/env"; // call process.env.xxx here > undefined
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}), // process.env.xxx must be called after this line
MongooseModule.forRoot(env.MONGO_URI),
],
controllers: [AppController],
})
export class AppModule {}
so fix
import { Module } from "#nestjs/common";
import { ConfigModule } from "#nestjs/config";
// should place this at very first line
const envModule = ConfigModule.forRoot({
isGlobal: true,
})
import { MongooseModule } from "#nestjs/mongoose";
import { AppController } from "./app.controller";
import { env } from "./common/env";
#Module({
imports: [
envModule,
MongooseModule.forRoot(env.MONGO_URI),
],
controllers: [AppController],
})
export class AppModule {}
In my case I downgraded #types/node to be the same version as my node version. Could be a hint.

how to capture nestjs bootstrap errors in a log file

I'm trying to capture all logs(bootstrap, app error messages, db connection error messages) into a single log file in nestjs.
As of now I'm using a custom logger to so. Below is my custom logger code
logger.ts
import * as winston from 'winston';
import * as chalk from 'chalk';
import PrettyError from 'pretty-error';
import { LoggerOptions } from 'winston';
export class LoggerService {
private readonly logger: winston.Logger;
private readonly prettyError = new PrettyError();
public static loggerOptions: LoggerOptions = {
transports: [
new winston.transports.File({
filename: 'logs/mgmtserver-main.log',
format: winston.format.json()
}),
],
};
constructor(private context: string, transport?) {
this.logger = (winston as any).createLogger(LoggerService.loggerOptions);
this.prettyError.skipNodeFiles();
this.prettyError.skipPackage('express', '#nestjs/common', '#nestjs/core');
}
get Logger(): winston.Logger {
return this.logger;
}
static configGlobal(options?: LoggerOptions) {
this.loggerOptions = options;
}
log(message: string): void {
const currentDate = new Date();
this.logger.info(message, {
timestamp: currentDate.toISOString(),
context: this.context,
});
this.formatedLog('info', message);
}
error(message: string, trace?: any): void {
const currentDate = new Date();
this.logger.error(`${message} -> (${trace || 'trace not provided !'})`, {
timestamp: currentDate.toISOString(),
context: this.context,
});
this.formatedLog('error', message, trace);
}
warn(message: string): void {
const currentDate = new Date();
this.logger.warn(message, {
timestamp: currentDate.toISOString(),
context: this.context,
});
this.formatedLog('warn', message);
}
overrideOptions(options: LoggerOptions) {
this.logger.configure(options);
}
// this method just for printing a cool log in your terminal , using chalk
private formatedLog(level: string, message: string, error?): void {
let result = '';
const color = chalk.default;
const currentDate = new Date();
const time = `${currentDate.getHours()}:${currentDate.getMinutes()}:${currentDate.getSeconds()}`;
switch (level) {
case 'info':
result = `[${color.blue('INFO')}] ${color.dim.yellow.bold.underline(time)} [${color.green(
this.context,
)}] ${message}`;
break;
case 'error':
result = `[${color.red('ERR')}] ${color.dim.yellow.bold.underline(time)} [${color.green(
this.context,
)}] ${message}`;
break;
case 'warn':
result = `[${color.yellow('WARN')}] ${color.dim.yellow.bold.underline(time)} [${color.green(
this.context,
)}] ${message}`;
break;
default:
break;
}
console.log(result);
}
}
I'm able to log application erro messages(err, warn, info) using the above logger in any file like below
import { LoggerService } from 'logger';
private readonly logger: LoggerService = new LoggerService(RegistrationService.name);
this.logger.warn('this is a warn message');
my main.ts looks like below
import { ValidationPipe, Logger } from "#nestjs/common";
import { NestFactory } from "#nestjs/core";
import { ConfigService } from '#nestjs/config';
import { AppModule } from "./app.module";
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import { LoggerService } from "logger";
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new LoggerService('Main'), abortOnError: false
});
app.enableCors();
await app.listen(3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
The issue is I'm not able to capture the Nestfactory.create. bootstrap errors in the log file. They are getting printed on the console but not to log file.
For example, the below bootstrap errors are getting printed on console but not into the log file.
[INFO] 15:12:50 [Main] Starting Nest application...
[ERR] 15:12:50 [Main] Nest cannot create the AuthorisationModule instance.
The module at index [3] of the AuthorisationModule "imports" array is undefined.
Potential causes:
- A circular dependency between modules. Use forwardRef() to avoid it. Read more: https://docs.nestjs.com/fundamentals/circular-dependency
- The module at index [3] is of type "undefined". Check your import statements and the type of the module.
Please help me. Your help is much appreciated.
That is because your logger module isn't initialized until Nest finishes initializing the App context. Your logger will capture all the errors after the your App is running but not before.
However you can make use of node's inbuilt events to log/file these exceptions
process.on('uncaughtException', err => {
console.error('There was an uncaught error', err)
process.exit(1) //mandatory (as per the Node.js docs)
})
or, for Promises,
process.on('unhandledRejection', err => {
console.error('There was an uncaught error', err)
process.exit(1) //mandatory (as per the Node.js docs)
})
https://nodejs.dev/learn/error-handling-in-nodejs#catching-uncaught-exceptions

Winston with AWS Cloudwatch on Nestjs

All the articles and documentation I have read so far talk about the integration of Cloudwatch and Winston on a vanilla Node app, but nothing on Nestjs
So far I have on my app.module.ts:
imports: [
ConfigModule.forRoot({ isGlobal: true }),
MongooseModule.forRoot(
`mongodb://${environment.MONGO_INITDB_ROOT_USERNAME}:${environment.MONGO_INITDB_ROOT_PASSWORD}#${environment.MONGODB_HOST}/${environment.MONGO_INITDB_DATABASE}`,
),
VoucherModule,
ApiKeyModule,
WinstonModule.forRoot(loggerConfig),
],
where loggerConfig are the basic Winston configs depending on the env.
Using winston-cloudwatch package I need to create a new Transporter and add it add it to winston, but can't seem to find a way to do this.
I recently implemented aws-cloudwatch in nestjs and faced similar issue but after some browsing and reading about winston and cloudwatch, came up with this solution.
//main.ts
import {
utilities as nestWinstonModuleUtilities,
WinstonModule,
} from 'nest-winston';
import * as winston from 'winston';
import CloudWatchTransport from 'winston-cloudwatch';
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
format: winston.format.uncolorize(), //Uncolorize logs as weird character encoding appears when logs are colorized in cloudwatch.
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.ms(),
nestWinstonModuleUtilities.format.nestLike()
),
}),
new CloudWatchTransport({
name: "Cloudwatch Logs",
logGroupName: process.env.CLOUDWATCH_GROUP_NAME,
logStreamName: process.env.CLOUDWATCH_STREAM_NAME,
awsAccessKeyId: process.env.AWS_ACCESS_KEY,
awsSecretKey: process.env.AWS_KEY_SECRET,
awsRegion: process.env.CLOUDWATCH_AWS_REGION,
messageFormatter: function (item) {
return (
item.level + ": " + item.message + " " + JSON.stringify(item.meta)
);
},
}),
],
}),
});
Here, we have defined two transports one is transports.Console() which is the default winston transport and another one is the cloudwatch transport.
What this basically does is, replaces the default nestjs Logger(imported from #nestjs/common). So, we don't have to import winston in any module not even in 'app.module.ts'. Instead just import Logger in whichever module you need, and whenever we log anything it will be shown in the terminal and will upload it to the cloudwatch.
EDIT:
With configService..
//main.js
import {
utilities as nestWinstonModuleUtilities,
WinstonModule,
} from 'nest-winston';
import * as winston from 'winston';
import CloudWatchTransport from 'winston-cloudwatch';
import { ConfigService } from '#nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
app.useLogger(
WinstonModule.createLogger({
format: winston.format.uncolorize(),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.ms(),
nestWinstonModuleUtilities.format.nestLike(),
),
}),
new CloudWatchTransport({
name: 'Cloudwatch Logs',
logGroupName: configService.get('CLOUDWATCH_GROUP_NAME'),
logStreamName: configService.get('CLOUDWATCH_STREAM_NAME'),
awsAccessKeyId: configService.get('AWS_ACCESS_KEY'),
awsSecretKey: configService.get('AWS_KEY_SECRET'),
awsRegion: configService.get('CLOUDWATCH_AWS_REGION'),
messageFormatter: function (item) {
return (
item.level + ': ' + item.message + ' ' + JSON.stringify(item.meta)
);
},
}),
],
}),
);
await app.listen(configService.get('PORT') || 3000);
}

Resources