kimyu0218
  • [etc] winston과 sentry로 서버가 터지는 이유 분석하기
    2024년 01월 13일 03시 56분 16초에 업로드 된 글입니다.
    작성자: @kimyu0218
    서버 개발자를 노트북에서 잠시 해방시켜 주는 법

    이번에 프로젝트를 진행하면서 가장 많이 했던 말이 "서버 왜 죽었지"다. 이전까지는 배포를 제대로 해본 적이 없어서 (과제 발표할 동안만 돌아가면 되기 때문에,, 어차피 개발해도 나만 쓰는 일회성이기 때문에,,) 로깅과 모니터링의 중요성을 느끼지 못했다.

    🙋‍♀️ : 프론트 테스트 하려고 하는데 서버 내렸어?
    🙋‍♂️ : 서비스 안 되는데 혹시 서버 터졌나요?

    하지만 이번 프로젝트에서 매주 업데이트 된 것을 배포하고, 사용자를 모으면서 로깅과 모니터링이 정말 중요하다는 걸 깨달았다.

    🤯: 엥? 서버 안 돌아가?!?!
    🤯: 엇,, 잠시만요,, 확인해보겠습니다,, (노트북 주섬주섬,,)
    서버 생존 여부는 내가 제일 궁금해. 매번 이렇게 노트북 주섬주섬 꺼내는 거 너무 힘들어. 나 배포 처음인데 가혹해. 내 서버는 개복치야.

    지금 나에게 필요한 건 딱 두 가지다.

    1. 서버가 죽는 원인 찾기
    2. 서버가 곧 터질 것 같다는 신호를 보낼 때 빠르게 캐치해서 사전 대응하기

     

    winston으로 로그 쌓기

    winston은 node에서 사용하는 로깅 라이브러리다!

     

    로그를 레벨별로 관리해야 하는 이유

    로그는 개발 및 유지보수를 도와주는 아주 중요한 기록이다. 그렇다면 로그를 많이 쌓을수록 좋을까?
    정답은 "아니다"다. 너무 많은 로그는 시스템의 성능에 영향을 미치고, 디스크 공간을 차지한다. 따라서 로그를 다양한 레벨로 나누고, 필요한 정보만 기록하도록 구현해야 한다.

    1. DEBUG 레벨
    - 개발 및 디버깅 목적
    - 보안 문제가 발생할 수 있으므로 배포 환경에서는 주의하여 사용해야 한다.

    2. INFO 레벨
    - 어플리케이션의 주요 이벤트 및 주요 마일스톤에 대한 정보 제공
    - 주요 작업이나 서비스 시작 및 종료 등에 사용 (= 어플리케이션의 정상 작동 상태)

    3. WARN 레벨
    - 잠재적인 문제나 예외적인 상황에 대한 경고
    - 시스템은 계속해서 작동하지만 조치가 필요

    4. ERROR 레벨
    - 심각한 문제 또는 오류
    - 예외가 발생하거나 예상치 못한 상황
    - 사용자에게 표시될 필요가 있는 오류 및 서비스가 중단된 경우

    5. FATAL 레벨
    - 치명적인 오류로 인해 어플리케이션이 종료될 가능성이 높은 상황에 사용
    - 심각한 예외 또는 더 이상 복구할 수 없는 상태

    winston은 다양한 로깅 레벨을 지원하기 때문에 각 레벨에 따라 서로 다른 유형의 로그 메시지를 생성할 수 있다. 게다가 파일, 콘솔, 데이터베이스 등 다양한 형식으로 로그를 남길 수 있다.
     

    로그를 날짜별로 관리해보자

    winston을 이용하면 레벨별로 로그를 관리할 수 있다. 하지만 하나의 error 로그 파일에 모든 error 로그가 다 들어있다면 어떨까?

    😑 : 어디부터 확인해야 하는 거야?

    winston-daily-rotate-file을 이용하면 날짜별로 새로운 로그 파일을 생성하고, 일정 기간이 지난 로그 파일은 자동으로 삭제할 수 있다. 즉, 하나의 파일이 너무 커지는 것을 방지하고 디스크 공간이 낭비되는 것을 막는다.
     

    프로젝트에 winston 로거 도입하기

    이제 winston과 winston-daily-rotate-file 라이브러리를 이용해서 로그를 단계별로 쌓아볼 것이다.

    function createDailyRotateFile(level: string): DailyRotateFile {
      ...
      return new DailyRotateFile({
        level: level,
        dirname: dirname,
        filename: `${level}-%DATE%.log`,
        datePattern: 'YYYY-MM-DD',
        zippedArchive: true,
        maxSize: '20m',
        maxFiles: '14d',
      });
    }
    
    export const debugTransports = new transports.Console({
      level: 'debug',
    });
    export const infoTransports: DailyRotateFile = createDailyRotateFile('info');
    export const warnTransports: DailyRotateFile = createDailyRotateFile('warn');
    export const errorTransports: DailyRotateFile = createDailyRotateFile('error');

    winston은 info, warn, error, debug 로그 레벨을 지원한다. 디스크 공간이 낭비되는 것을 막기 위해 debug 레벨은 콘솔로 남기기로 결정했다. `zippedArchive`를 통해 로그 파일을 압축하여 저장하도록 설정했고, `maxFiles`를 `14d`로 지정하여 14일이 지난 로그는 자동으로 삭제하도록 만들었다.

    🔗 로거 관련 파일 보러가기

    sentry로 모니터링 하기

    sentry는 어플리케이션에서 발생하는 오류를 추적하고 모니터링할 수 있는 플랫폼이다. 즉, 오류를 빠르게 감지하여 신속하게 대응할 수 있다.

    sentry의 주요 기능
    • 오류 추적 및 모니터링 : 어떤 오류가 언제, 어디서 발생했는지 상세한 정보를 얻을 수 있다.
    • 실시간 알림 및 경고 : 오류가 발생하면 개발자에게 알림을 제공하기 때문에 신속하게 대응할 수 있다.
    • 오류 통계 및 트렌드 분석

    sentry에 가입하고 나면 언어별로 설정하는 가이드라인을 제공하기 때문에 포스팅에서 세팅 과정을 다루진 않겠다.

    우리 서버를 죽여버린 QueryFailedError..
    이렇게 분석도 해준다!!

    로깅과 마찬가지로 너무 많은 모니터링은 성능 저하를 야기하기 때문에 적절한 비율을 설정해줘야 한다!

    sentry를 조금 더 다루고 싶지만 제대로 사용해보지 않아서 여기서 마무리,, 서비스를 운영하면서 sentry를 적극적으로 활용해보고 다른 포스팅에서 보충해보도록 하겠다!!

    +) slack webhook으로 알림 받기

    sentry로 알림 받는 것도 좋지만 개발자들은 슬랙을 많이 이용한다. (sentry에도 slack 연동이 있는 것 같은데 아직 미숙해서 못찾았다.) 따라서 슬랙 웹훅을 이용하여 알림을 받도록 설정할 것이다.
     
    Sending messages using Incoming Webhooks
    위의 링크를 따라 인커밍 웹훅을 생성하고, `@slack/client`를 설치하여 에러 인터셉터를 다음과 같이 작성했다.

    ...
    @Injectable()
    export class ErrorsInterceptor implements NestInterceptor {
      private readonly slackWebhook: IncomingWebhook;
    
      constructor(
        private readonly logger: LoggerService,
        private readonly configService: ConfigService,
      ) {
        this.slackWebhook = new IncomingWebhook(
          this.configService.get('SLACK_WEBHOOK_FOR_BE') || '',
        );
      }
    
      intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const controllerName: string = context.getClass().name;
        const methodName: string = context.getHandler().name;
        const logContext: string = `Failed to execute <${controllerName} - ${methodName}>`;
    
        return next.handle().pipe(
          catchError((err: unknown) => {
            ...
            if (err instanceof QueryFailedError) {
              return throwError(() => this.handleQueryFailedError(err, logMessage));
            }
            ...
          }),
        );
      }
      ...
      private handleQueryFailedError(
        err: QueryFailedError,
        logMessage: string,
      ): void {
        this.sendNotification('📢 QueryFailedError 버그 발생', err);
        logErrorWithStack(this.logger, logMessage, err.stack ?? '');
        throw new InternalServerErrorException();
      }
    
      private sendNotification(text: string, err: Error): void {
        Sentry.captureException(err);
        this.slackWebhook.send(makeSlackMessage(text, err));
      }
    }

    앞서 말했듯이 서버가 죽는 이유는 `QueryFailedError` 때문이다. 해당 에러에 대해 sentry에 기록하고, 슬랙 채널로 메시지를 전송함으로써 서버가 죽기 전에(?) 사전 대응을 할 수 있다! (과연 사전 대응을 할 수 있을 것인가 아니면 일종의 아웃카운트가 될 것인가)

    댓글