ํ‹ฐ์Šคํ† ๋ฆฌ ๋ทฐ

nestjs๋กœ ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ winston์„ ์ด์šฉํ•ด logging์„ ์ ์šฉํ•œ ๊ฒฝํ—˜์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ์ˆœ์„œ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

  • http-logger-middleware ์ ์šฉ (morgan X)
  • nest-winston + winston-daily-rotate-file

log๋ฅผ ๋‚จ๊ฒจ์•ผํ•˜๋Š” ์ด์œ 

log๋ฅผ ๋‚จ๊ฒจ์•ผํ•˜๋Š” ์ด์œ ๋Š” ๋‹ค์–‘ํ•˜๊ฒ ์ง€๋งŒ ๋‚ด๊ฐ€ ํ•ด๋‹น ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ์—์„œ log๋ฅผ ๋‚จ๊ฒจ์•ผ๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ•œ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. ๋ณดํ†ต ํ”ผ๋“œ๋ฐฑ๊ณผ ๊ด€๋ จ๋œ ๋‚ด์šฉ์ด๋‹ค.

  • ์•ˆ์ •๋œ ์„œ๋น„์Šค๋ฅผ ์œ„ํ•ด log๋ฅผ ํ†ตํ•œ ์„œ๋น„์Šค ์ƒํƒœ ๋ฐ ๋กœ์ง์„ ์ถ”์ 
  • error ๋ฐœ์ƒ ์‹œ์— error ์ƒํ™ฉ ์ถ”์ 
  • file ํ˜•ํƒœ๋กœ log๋ฅผ ๋‚จ๊ฒจ ์œ„ ๊ณผ์ •์„ ์ข€ ๋” ์œ ์—ฐํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ
  • ๋กœ๊ทธ๋ฅผ ํ™œ์šฉํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ํ™˜๊ฒฝ ๊ตฌ์ถ•

http-logger-middleware ์ ์šฉํ•˜๊ธฐ

๋จผ์ €, ๊ธฐ์กด nodejs ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋˜ morgan module์ฒ˜๋Ÿผ http ์š”์ฒญ ์ •๋ณด๋ฅผ logging ํ•ด์ฃผ๋Š” ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ์ด ํ•„์š”ํ–ˆ๋‹ค.
๊ทธ๋ž˜์„œ ๊ฐ€์žฅ ๋จผ์ € ๋– ์˜ค๋ฅธ morgan์„ nest-morgan์„ ์ด์šฉํ•ด์„œ ์ ์šฉํ•ด๋ณด์•˜์œผ๋‚˜ ํ•ด๋‹น package๋Š” deprecated ๋˜์–ด์žˆ์—ˆ๊ณ , ์ถœ๋ ฅ๋˜๋Š” ๋‚ด์šฉ๋„ ๋‚ด๊ฐ€ ์›ํ•˜๋˜ ํ˜•์‹์€ ์•„๋‹ˆ์˜€๋‹ค.

๊ฒฐ๊ตญ ๊ฐ„๋‹จํ•˜๊ฒŒ http-logger-middeleware๋Š” ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

๊ตฌํ˜„ํ•œ http-logger middleware๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. ๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋˜ morgan์˜ ํ˜•ํƒœ์™€ ๋น„์Šทํ•œ ํ˜•ํƒœ์ด๋‹ค.
Youtube http-logger๋ฅผ ๋งŒ๋“œ๋Š” ์˜์ƒ์ด ์žˆ์–ด ์ฐธ๊ณ ํ–ˆ๋‹ค.

// http-logger.middleware.ts

import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class HttpLoggerMiddleware implements NestMiddleware {
  private logger = new Logger('HTTP');

  use(request: Request, response: Response, next: NextFunction) {
    const { ip, method, originalUrl } = request;
    const userAgent = request.get('user-agent') || '';

    response.on('finish', () => { // (1)
      const { statusCode } = response;
      const contentLength = response.get('content-length');

      this.logger.log(
        `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
      );
    });

    next();
  }
}
  • (1) response์˜ finish eventListener : node์—์„œ http response ์ „์†ก ์‹œ์— emit ํ•˜๋Š” finish ์ด๋ฒคํŠธ๋ฅผ catch ํ•œ๋‹ค node.js event-finish

์œ„ middleware๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , ์•„๋ž˜์™€ ๊ฐ™์ด ์ตœ์ƒ์œ„ ๋ชจ๋“ˆ์ธ AppModule์— HttpLoggerMiddleware๋ฅผ ์ ์šฉํ–ˆ๋‹ค.

// app.module.ts 

@Module({
  imports: [
    TypeOrmModule.forRoot(typeORMConfig),
    AuthModule,
    WorkbookModule,
    UserModule,
    CommonModule,
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(HttpLoggerMiddleware).forRoutes('*');
  }
}

์ดํ›„ API๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ณธ ๊ฒฐ๊ณผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถœ๋ ฅ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค.

file ํ˜•ํƒœ๋กœ log๋ฅผ ๊ธฐ๋กํ•˜์ž

๋ณดํ†ต ์„œ๋ฒ„ ๋ฐฐํฌ ์‹œ์— nohub์„ ์ด์šฉํ•ด ์ถœ๋ ฅ ๋กœ๊ทธ๊ฐ€ ๊ธฐ๋ก๋˜๊ธดํ•˜์ง€๋งŒ, ์ด๋Š” ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ต๊ณ  ๊ฐœ์ธ์ ์œผ๋กœ ๋กœ๊ทธ๋ฅผ ๋”ฐ๋กœ ํŒŒ์ผ๋กœ ์ •๋ฆฌํ•ด์„œ ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ file ํ˜•ํƒœ๋กœ log๋ฅผ ๋”ฐ๋กœ ์ €์žฅํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

node์—์„œ ์ต์ˆ™ํ•œ winston module์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ, nest-winston ๋ชจ๋“ˆ์„ ๋ฐœ๊ฒฌํ•ด์„œ ๋น ๋ฅด๊ฒŒ ์ ์šฉํ•ด๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๋˜ํ•œ ๋‚ ์งœ๋ณ„๋กœ ํŒŒ์ผ์„ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด winston-daily-rotate-file module๋„ ์ถ”๊ฐ€๋กœ ์ด์šฉํ–ˆ๋‹ค

npm install nest-winston winston
npm install winston-daily-rotate-file
// logger-config.ts

import {
  utilities as nestWinstonModuleUtilities,
  WinstonModule,
} from 'nest-winston';
import { format, Logform, transports } from 'winston';
import 'winston-daily-rotate-file';

export class LoggerConfig {
  static createApplicationLogger() {
    return WinstonModule.createLogger({
      format: format.combine(
        format.timestamp(),
        nestWinstonModuleUtilities.format.nestLike('FLIP'), // (1)
      ),
      transports: [
        new transports.Console({}),
        new transports.DailyRotateFile({ // (2)
          format: this.logFileFormat(),
          filename: 'application-%DATE%.log', // (3)
          dirname: 'logs',
          datePattern: 'YYYY-MM-DD-HH',
          zippedArchive: true,
          maxSize: '20m', 
          maxFiles: '14d', 
        }),
        new transports.DailyRotateFile({
          format: this.logFileFormat(),
          level: 'error',
          filename: 'error-%DATE%.log',
          dirname: 'logs',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true,
          maxSize: '20m',
          maxFiles: '14d',
        }),
      ],
    });
  }

  private static logFileFormat(): Logform.Format {
    return format.combine(
      format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
      format.printf((info) => JSON.stringify(info)), // (4)
    );
  }
  • (1) nest-winston ๋ชจ๋“ˆ์— ๊ธฐ์กด nest log์™€ ๋น„์Šทํ•˜๊ฒŒ ์ถœ๋ ฅ ํฌ๋ฉง์„ ์ •ํ•ด์ฃผ๋Š” ์œ ํ‹ธ์ด ์žˆ์–ด ์‚ฌ์šฉํ–ˆ๋‹ค. FLIP์€ ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ ์ด๋ฆ„์ด๋‹ค. ์•„๋ฌด๊ฑฐ๋‚˜ ์ž…๋ ฅํ•ด๋„ ๋œ๋‹ค.
  • (2) winston-daily-rotate-file์˜ DailyRotateFile()์„ ์ด์šฉํ•ด ํŒŒ์ผ์„ ์‹œ๊ฐ„์— ๋”ฐ๋ผ ๋ฐ”๊พธ๊ฒŒ ํ•œ๋‹ค.
  • (3) %DATE%์—๋Š” ์‹œ๊ฐ„ ํฌ๋งท(datePattern)๊ฐ’์ด ์ž…๋ ฅ๋œ๋‹ค. ex) application-2022-02-21.log
  • (4) ํ•ด๋‹น ํฌ๋ฉง์€ logFile์— ์ €์žฅ๋  ํฌ๋ฉง์ด๋‹ค ์ž์‹ ์ด ์›ํ•˜๋Š” ํ˜•์‹์„ ์„ ํƒํ•˜์ž.

์ดํ›„ main.ts bootstrap()์—์„œ ๊ธฐ๋ณธ logger๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝํ•ด์ฃผ์—ˆ๋‹ค.

// main.ts
import { LoggerConfig } from './common/config/logger-config';

async function bootstrap() {
  const appOptions: NestApplicationOptions = {
    logger: LoggerConfig.createApplicationLogger(),
  };

  const app = await NestFactory.create(AppModule, appOptions);

  // ...
}

๋งˆ๋ฌด๋ฆฌ

๋‹ค์Œ์€ ์œ„ ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•œ ๊ฒฐ๊ณผ์ด๋‹ค. (http-logger, log files)
์›ํ•˜๋Š”๋Œ€๋กœ ์ž˜ ์ž‘๋™ํ–ˆ๊ณ , ์ดํ›„์— ๋ชจ๋‹ˆํ„ฐ๋ง, ๋กœ๊ทธ ๋ถ„์„ ๋“ฑ์˜ ํ•™์Šต์„ ์ด์–ด๊ฐˆ ์ƒ๊ฐ์ด๋‹ค

 

log files

 

๋ฐ˜์‘ํ˜•