728x90

⚠️ 문제

지금까지 배포 후 웹 서버에서 문제가 발생하면 인스턴스에 들어가서 에러를 보고 해결하는 방법으로 프로젝트를 진행해왔었다.

오류 해결이야 이렇게 하면 되지만 실 서비스를 운영할 때는 로그 관리도 해야하기 때문에 로그 기록의 필요성을 느꼈다.

 

 


🏃 시도

Try 1 : 로그 파일 만들기

  • morgan : HTTP 요청을 로깅해주는 node.js 의 서드파티 모듈
  • winston : 로그 파일 저장 및 로그 레벨 관리를 도와주는 node.js 의 서드파티 모듈

 

우선 기본적인 로거 파일은 아래와 같이 작성했다.

📄 logger.js

require('dotenv').config();
const winston = require('winston');
const winstonDaily = require('winston-daily-rotate-file');
const appRoot = require('app-root-path');
const process = require('process');

const logDir = `${appRoot}/logs`;

const { combine, timestamp, printf, colorize, simple } = winston.format;

const logFormat = printf((info) => { // 로그 출력 형식
  return `${info.timestamp} ${info.level}: ${info.message}`;
});

/*
 * Log Level
 * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
 */
const logger = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    new winstonDaily({ // 로그파일을 일자별로 저장해줌
      level: 'info',
      datePattern: 'YYYY-MM-DD', // 파일 이름에 넣을 날짜 형식 정의
      dirname: logDir, // 로그 파일을 저장할 디렉토리
      filename: `%DATE%.log`, // 파일 이름
      maxFiles: 30, // 30일 단위
      zippedArchive: true, 
    }),

    new winstonDaily({
      level: 'error',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/error',
      filename: `%DATE%.error.log`,
      maxFiles: 30,
      zippedArchive: true,
    }),
  ],

  exceptionHandlers: [ // 예외 발생 시, 로그 레벨로 처리한다.
    new winstonDaily({
      level: 'error',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/error',
      filename: `%DATE%.exception.log`,
      maxFiles: 30,
      zippedArchive: true,
    }),
  ],
});

logger.stream = { // 외부에서 로거 파일을 불러올 때 실행할 스트림
  write: (message) => {
    logger.info(message);
  },
};

// 배포 환경이 아닐 때는 로그를 간단히 나타내서 파일의 크기를 줄인다.
if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new winston.transports.Console({
      format: combine(colorize(), simple()),
    }),
  );
}

module.exports = logger;

 

📄 index.js

require('dotenv').config();
const express = require('express');
const app = express();
const morgan = require('morgan');
const logger = require('./src/config/logger');

...

const morganFormat = process.env.NODE_ENV !== 'production' ? 'dev' : 'combined';
app.use(morgan(morganFormat, { stream: logger.stream })); // morgan

...

app.listen(process.env.SERVER_PORT, () => {
  logger.info(
    `Server On : http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}/`,
  );
});

메인 파일에서 morgan을 불러오고 애플리케이션에서 http 요청이 올때마다 로깅을 해준다.

  • morganFormat은 morgan 라이브러리 상에서 미리 정의된 포맷대로 출력하기 위한 인자이다.
    • combined, common, dev, short, tiny 의 다섯가지 포맷이 있으며, 이 프로젝트에서는 production 환경과 dev 환경에 따라 다르게 출력되게끔 설정해주었다.

그리고 stream은 로그를 파일로 저장하기 위해 logger 파일과 연결해놓은 것이다.

 

📄 logger.js

...

logger.stream = { // 외부에서 로거 파일을 불러올 때 실행할 스트림
  write: (message) => {
    logger.info(message);
  },
};

...

해당 부분의 ‘message’의 인자로 morgan 출력이 전달되고 로그가 파일로 저장되는 것이다.

 

  • production 포맷 예시
2022-10-04 16:19:34 info: ::1 - - [04/Oct/2022:07:19:34 +0000] "GET /auth HTTP/1.1" 401 16 "http://localhost:4000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
  • dev 포맷 예시
2022-10-04 16:16:22 info: [0mGET /auth [33m401[0m 1.254 ms - 16[0m

 

✨ 이렇게 하면 로컬 환경에서의 로깅 완료!

  • 로그데이터가 📄 logger.js 에서 정의해준대로 잘 쌓여있는 걸 확인할 수 있다!

 

 

Try 2 : AWS S3 버킷에 로그 파일 저장하기

  • 처음에 생각한 건 📄 logger.jslogDir 을 S3 버킷으로 연결해주면 되겠다는 생각을 했다.
    But,,, 버킷이 무슨 내 컴퓨터에 있는 디렉토리도 아니고 주소만 입력한다고 해서 로그가 자동으로 들어갈 순 없었다.
    ( 버킷 설정과 버킷에 파일에 데이터를 쓰는 로직이 필요하다.)
  • 이걸 일일이 구현하기는 무리가 있겠다고 판단해서 찾아보다가 s3-streamlogger 모듈을 발견했다.
    역시 갓PM

 

  • 우선 AWS 계정에서 IAM 권한으로 S3FullAccess을 지정해준 계정을 하나 만들어서 액세스 아이디, 액세스 키를 발급받았다.

 

그리고 내가 만든 버킷이 잘 돌아가는 지 확인하기 위해 우선 공식 도큐먼트에 있는 예제를 돌려서 테스트 해봤다.

s3_stream = new S3StreamLogger({
    bucket: `${process.env.BUCKET_NAME}`,
    access_key_id: `${process.env.ACCESS_KEY_ID}`,
    secret_access_key: `${process.env.SECRET_ACCESS_KEY}`,
});

오잉,, 근데 계속 permission denied 에러가 떠서 버킷 설정을 다시 해줬다.

 

알고보니 기본 디폴트 값인 'ACL 비활성화'로 되어 있어서 IAM 계정이 해당 버킷에 접근을 못하는 것이었고 ACL을 활성화해서 ‘S3 Full Access’ 권한을 부여해준 이 IAM 계정에서도 버킷에 접근할 수 있게끔 해줬다.

이제 로그 파일이 버킷 내에 잘 쌓이는 것을 확인했고 로커 파일 설정을 만져줬다.

s3_stream = new S3StreamLogger({
    bucket: `${process.env.BUCKET_NAME}`,
    access_key_id: `${process.env.ACCESS_KEY_ID}`,
    secret_access_key: `${process.env.SECRET_ACCESS_KEY}`,
});

해당 형태로 로그 생성 스트림을 만들어줬다.

 

로그 내용은 정상적으로 버킷에 저장이 되는 데, 버킷에 저장된 파일이름이 이상하게 저장이 된다…

공식 도큐먼트를 뜯어보다가 name_format 옵션을 발견했고 이름 형식이 ‘strftime’에 기반해서 명명된다는 걸 발견했다.

 

무튼 다시 또 strftime을 타고 들어가서 우리나라 시간으로 바꿔주는 포맷이 있나 찾아봤다.

KOR은 없었다…😂

 

var strftime = require('strftime') // not required in browsers
    var strftimePDT = strftime.timezone('-0700')
    var strftimeCEST = strftime.timezone('+0200')
    console.log(strftimePDT('%F %T', new Date(1307472705067))) // => 2011-06-07 11:51:45
    console.log(strftimeCEST('%F %T', new Date(1307472705067))) // => 2011-06-07 20:51:45

“타임존 포맷을 ISO 8601의 ’+HHMM’ 나 ’-HHMM’ 이런식으로 바꿔라” 라고 친절하게 나와있어서 해당 방법을 프로젝트에 적용시켜 봤다.

 

const strftime = require('strftime');

const strftimeKOR = strftime.timezone('+0900'); // 시간을 한국 시간으로 변경
const time_data = strftimeKOR('%F %T', new Date()); // 현재 시간에 날짜 포맷을 적용 시키고 한국 시간으로 변경

이런식으로 한국 시간으로 변경해줬고 도큐먼트에서 본 몇 가지 옵션을 더 추가해서 로그 생성 스트림을 완성 시켰다.

 

...

const info_stream = new S3StreamLogger({
  bucket: `${process.env.BUCKET_NAME}`,
  tags: { type: 'log', version: 'alpha' },
  folder: 'info',
  name_format: `${time_data}.log`,
  access_key_id: `${process.env.ACCESS_KEY_ID}`,
  secret_access_key: `${process.env.SECRET_ACCESS_KEY}`,
});

const error_stream = new S3StreamLogger({
  bucket: `${process.env.BUCKET_NAME}`,
  tags: { type: 'log', version: 'alpha' },
  folder: 'error',
  name_format: `${time_data}.log`,
  access_key_id: `${process.env.ACCESS_KEY_ID}`,
  secret_access_key: `${process.env.SECRET_ACCESS_KEY}`,
});

...

const logger = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    new winstonDaily({
      level: 'info',
      stream: s3_stream('info'),
    }),

    new winstonDaily({
      level: 'error',
      stream: s3_stream('error'),
    }),
  ],

  exceptionHandlers: [
    new winstonDaily({
      level: 'error',
      stream: s3_stream('error'),
    }),
  ],
});

 

로그 레벨에 따라 다른 디렉토리로 분리되어 저장되게끔 구현했는데, 코드가 반복되는 게 더러워서..😅 함수 형태로 다시 바꿔줬다.

 

...

function s3_stream(level) {
  const time_data = strftimeKOR('%F %T', new Date()); // set Time in Seoul, South Korea

  return new S3StreamLogger({
    bucket: `${process.env.BUCKET_NAME}`,
    tags: { type: 'log', version: `${process.env.VERSION}` },
    folder: `${level}`,
    name_format: `${time_data}.log`,
    access_key_id: `${process.env.ACCESS_KEY_ID}`,
    secret_access_key: `${process.env.SECRET_ACCESS_KEY}`,
  });
}

...

const logger = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    new winstonDaily({
      level: 'info',
      stream: s3_stream('info'),
    }),

    new winstonDaily({
      level: 'error',
      stream: s3_stream('error'),
    }),
  ],

  exceptionHandlers: [
    new winstonDaily({
      level: 'error',
      stream: s3_stream('error'),
    }),
  ],
}); 

 


✨ 해결

원래는 하나의 로거 파일에 배포용 로거와 개발용 로거 둘 다 넣어놓으려고 했지만 따로 분리해서 각 파일에서 스트림만 갖다 쓰는 게 더 깔끔할 것 같아서 📄 devLogger.js 📄 productionLogger.js 로 분리했다.

📄 devLogger.js

const winston = require('winston');
const winstonDaily = require('winston-daily-rotate-file');
const appRoot = require('app-root-path');

const logDir = `${appRoot}/logs`;

const { combine, timestamp, printf, colorize, simple } = winston.format;

const logFormat = printf((info) => {
  return `${info.timestamp} ${info.level}: ${info.message}`;
});

/*
 * Log Level
 * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
 */
const logger = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir,
      filename: `%DATE%.log`,
      maxFiles: 30,
      zippedArchive: true,
    }),

    new winstonDaily({
      level: 'error',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/error',
      filename: `%DATE%.error.log`,
      maxFiles: 30,
      zippedArchive: true,
    }),
  ],

  exceptionHandlers: [
    new winstonDaily({
      level: 'error',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/error',
      filename: `%DATE%.exception.log`,
      maxFiles: 30,
      zippedArchive: true,
    }),
  ],
});

logger.stream = {
  write: (message) => {
    logger.info(message);
  },
};

logger.add(
  new winston.transports.Console({
    format: combine(colorize(), simple()),
  }),
);

module.exports = logger;

 

📄 productionLogger.js

require('dotenv').config();
const winston = require('winston');
const winstonDaily = require('winston-daily-rotate-file');
const S3StreamLogger = require('s3-streamlogger').S3StreamLogger;
const strftime = require('strftime');

const strftimeKOR = strftime.timezone('+0900');

function s3_stream(level) {
  const time_data = strftimeKOR('%F %T', new Date()); // set Time in Seoul, South Korea

  return new S3StreamLogger({
    bucket: `${process.env.BUCKET_NAME}`,
    tags: { type: 'log', version: `${process.env.VERSION}` },
    folder: `${level}`,
    name_format: `${time_data}.log`,
    access_key_id: `${process.env.ACCESS_KEY_ID}`,
    secret_access_key: `${process.env.SECRET_ACCESS_KEY}`,
  });
}

const { combine, timestamp, printf } = winston.format;

const logFormat = printf((info) => {
  return `${info.timestamp} ${info.level}: ${info.message}`;
});

/*
 * Log Level
 * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
 */
const logger = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    new winstonDaily({
      level: 'info',
      stream: s3_stream('info'),
    }),

    new winstonDaily({
      level: 'error',
      stream: s3_stream('error'),
    }),
  ],

  exceptionHandlers: [
    new winstonDaily({
      level: 'error',
      stream: s3_stream('error'),
    }),
  ],
});

logger.stream = {
  write: (message) => {
    logger.info(message);
  },
};

module.exports = logger;

 

 


🔗 참고

 

 


💡 깨달은 점

  • 서버 시작 후 바로 로그 파일이 s3 버킷에 저장되지 않는다! → aws 버킷과 연결하는 과정 필요
  • S3 버킷 권한 및 정책 설정 제대로 체킹하기 정책 설정은 무조건 다시 만져줘야함
  • info, error, exception 계층화 해서 저장하면 로그 관리가 훨씬 쉬워진다.
  • morgan은 http 요청에 대한 log 남기는 모듈, winston은 로그를 파일 형태로 저장하는 모듈
  • s3에 로그를 기록할 때 S3StreamLogger를 쓸 수 있는데, 이때 파일의 rotation 범위 및 업로드 시간, 버퍼 크기 등 상세한 부분까지 지정해줄 수 있다.
 
728x90