로깅의 정의와 구현.

로깅이란?

로깅은 애플리케이션에서 발생한 이벤트나 요청을 기록하는 작업을 말한다.

path, 에러, 실행 시간 등 요청 및 응답 정보를 기록함으로써 시스템의 동작을 추적하고, 버그가 발생했을 때 빠르게 원인을 파악하는데에 주로 사용된다.

구현

이 프로젝트에서는 미들웨어 레이어에 로깅을 구현했다.

사용자가 요청을 보내면, 요청의 메서드, URL, 헤더, 본문, 파라미터, 쿼리 등을 DB에 저장하고, 응답이 완료되면 응답 본문과 상태 코드, 요청 처리 시간을 업데이트한다.

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  constructor(
    private readonly prisma: PrismaService,
    private readonly configService: ConfigService,
    private readonly jwtService: JwtService,
  ) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl: url, headers, body, params, query } = req;
    const requestedAt = new Date();
    const logId = randomUUID();
    const userId = this.getBearerTokenUserId(headers);

    // 로그 생성 (요청 시점)
    await this.prisma.log
      .create({
        data: {
          id: logId,
          user_id: userId ?? null,
          method,
          url,
          headers: JSON.stringify(headers),
          body: body ? JSON.stringify(body) : null,
          param: params ? JSON.stringify(params) : null,
          query: query ? JSON.stringify(query) : null,
          ttl: null,
          created_at: requestedAt,
        },
      })
      .catch(console.error);

    const originalSend = res.send;

    // send 메서드 오버라이드
    res.send = (responseBody: any): Response => {
      const statusCode = res.statusCode;
      const duration = Date.now() - requestedAt.getTime();

      try {
        // 응답 후 로그 업데이트
        this.prisma.log
          .update({
            data: {
              response: responseBody ?? null,
              ttl: duration,
              error: statusCode >= 400,
            },
            where: { id: logId },
          })
          .catch(console.error); // 에러가 발생해도 무시하고 진행
      } catch (error) {
        console.error('Error while updating log:', error);
      }

      // 응답 전송
      return originalSend.call(res, responseBody);
    };

    next();
  }

  /**
   * 헤더 x-user에서 id를 인증해 반환한다.
   * 토큰 정보가 없거나 verify에 실패한 경우에도 로직에 문제는 없기에 null 반환
   *
   * @param headers 요청 헤더
   */
  private getBearerTokenUserId(headers: IncomingHttpHeaders): string | null {
    const token = headers['x-user'] as string;
    if (token) {
      try {
        const secret = this.configService.get<string>('JWT_SECRET_USER');
        const decoded = this.jwtService.verify(token, { secret }) as Guard.UserResponse;

        const userId = decoded.id;
        return userId;
      } catch (error) {
        return null;
      }
    }
    return null;
  }
}

@Module({
  imports: [
    JwtModule.register({ global: true }),
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    PrismaModule,
    AuthModule,
 . . . 
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}