⚠️ 문제
지금까지 배포 후 웹 서버에서 문제가 발생하면 인스턴스에 들어가서 에러를 보고 해결하는 방법으로 프로젝트를 진행해왔었다.
오류 해결이야 이렇게 하면 되지만 실 서비스를 운영할 때는 로그 관리도 해야하기 때문에 로그 기록의 필요성을 느꼈다.
🏃 시도
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.js 의
logDir
을 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;
🔗 참고
- morgan - npm
- (https://www.npmjs.com/package/morgan)
- winston - npm
- (https://www.npmjs.com/package/winston)
- [ Node ] Express 에서 Morgan과 Winston 으로 Logging 관리하기
- (https://velog.io/@soshin_dev/Node-Express-에서-Morgan과-Winston-으로-Logging-관리하기)
- Express winston morgan 로그 관리
- (https://velog.io/@gwon713/Express-winston-morgan-로그-관리)
- s3-streamlogger - npm
- (https://www.npmjs.com/package/s3-streamlogger)
- node js 에서 로그 파일 저장하기 (2) - AWS S3
- (https://dong-queue.tistory.com/39)
💡 깨달은 점
- 서버 시작 후 바로 로그 파일이 s3 버킷에 저장되지 않는다! → aws 버킷과 연결하는 과정 필요
- S3 버킷 권한 및 정책 설정 제대로 체킹하기 정책 설정은 무조건 다시 만져줘야함
- info, error, exception 계층화 해서 저장하면 로그 관리가 훨씬 쉬워진다.
- morgan은 http 요청에 대한 log 남기는 모듈, winston은 로그를 파일 형태로 저장하는 모듈
- s3에 로그를 기록할 때 S3StreamLogger를 쓸 수 있는데, 이때 파일의 rotation 범위 및 업로드 시간, 버퍼 크기 등 상세한 부분까지 지정해줄 수 있다.
'🎨 Project' 카테고리의 다른 글
[BE/GitLab] GitLab Pipelines로 배포 자동화하기 (0) | 2022.10.01 |
---|---|
[BE/Git] Git push 파일 용량 초과 문제 해결하기 (2) | 2022.09.06 |
[BE/Node.js] apidoc으로 api document 만들기 (0) | 2022.09.02 |
[BE/Node.js] bcrypto를 이용한 비밀번호 암호화 (0) | 2022.09.02 |