첫 프로젝트를 시작할 때만 해도 로깅에 대한 지식은 전무했다. 먹는거야?
'로그' 에 대해 어디선가 얼핏 들었던 기억은 있었지만, 당장 주어진 과제만으로도 허덕이고 있던 와중에
예상보다 빨리 MVP구현을 끝내자 남은 기간동안 morgan 과 winston을 사용해 보길 권장 받았었다
그 때 당시엔 스스로 만족할 만큼 나름 그럴싸하게 했다고 생각했지만
다시 살펴보니 그저 위태로운 코드상태와 젠가냐고 컨트롤러에 덕지덕지 logger가 덧발려져있었다 ㅋㅋㅋ
사실 사용법도모르고 구굴링복붙으로 작성했던거라 다음에 사용할때는 좀 더 체계적으로 사용하고 싶었다.
작성하기에 전 알아야할 express 흐름
morgan과 winston을 사용하기 앞서, 코드를 좀 더 간결하고 효율적이게 작성하기 위해선 express의 흐름부터 간략하게나마 이해하고 있어야한다. 자세히알면더좋고
// app.js
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(morganMiddleware); // 요청 처리 로깅
app.use(indexRouter); // 라우터 핸들러
app.use(errorMiddleware); // 에러 미들웨어 & 에러 로깅
express 애플리케이션에 미들웨어 배치순서는 곧 요청을 처리하는 순서이자 처리 방법에도 영향을 미칠 수 있다.
요청 처리 미들웨어
라우터 핸들러로 들어가는 요청을 분석하고 성공적인 요청만 응답을 로깅하는 미들웨어. 라우터 핸들러들 보다 상단에 위치해야 한다
라우터 핸들러
HTTP메서드 및 경로를 기반으로 요청을 처리하는 미들웨어. 중간에 배치해야한다
에러 미들웨어
모든 미들웨어 아래에 위치해야한다. 요청 처리 중에 발생하는 모든 오류를 잡아서 처리하도록 설계되어 있다. express 미들웨어가 가지는 특별한 매개변수인 req, res, next 에서 next에 error를 전달하면, 이 에러 미들웨어로 오게 되어있다.
morgan / winston
morgan
express.js 가 기본적으로 제공해주는 로깅 미들웨어
HTTP 요청 및 응답을 읽기 쉽게 설계되어있고 사용자 정의에 따라 다양한 로깅 형식을 제공한다. 설정도 간단하고 매우 가볍다.node.js 단짝 아니랄까봐 단독으로 사용할 시 코드 몇 줄이면 작성이 끝난다.
단점이라면 HTTP 요청 및 응답 로깅만을 하고 로그 회전, 로그 수준 관리기능 등이 없다
winston
Node.js를 위한 강력하고 다양한 로깅, 광범위한 기능을 제공하고 좀 더 폭넓은 사용자 정의가 가능한 범용 로깅 프레임워크
로그 수준, 다중 전송, 로그 회전, 로그 필터링 등 다양한 기능을 제공하고 원하는 형식으로 로그 메시지를 생성하고 제어한다. 로그 수준을을 정의해서 로그 메시지를 분류하고 필터링 하며 모듈식 아키텍처로 설계되어 맞춤형 전송 또는 포맷터로 기능을 쉽게 추가할 수 있다.
단점으로는 많은 기능을 지원하는 만큼 학습 곡선이 가파르다... ㅎㅎ
당신의일거수일투족을모두기록해주겠어라는다소변태같은본능이발동해버렸다
이러한 단점을 winston과 결합해 보안해본다.
winston 작성
가장 먼저 모건과 결합해 줄 winston을 작성한다
경로를 어떻게 설정할까 하다 일단 config에 작성했다. 이게맞나..?
const winston = require("winston");
const path = require("path");
const fs = require("fs");
const winstonDailyRotate = require("winston-daily-rotate-file");
// 로그를 저장할 디렉토리 생성. 없으면 생성
const logDirectory = path.join(__dirname, "../logs");
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory);
}
// 로그 레벨 정의
const logLevels = {
error: 0,
warn: 1,
info: 2,
debug: 3,
};
// 구성 정의
const logger = winston.createLogger({
levels: logLevels,
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf(({ level, message, timestamp }) => {
return `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
}),
),
transports: [
// winston-daily-rotate-file을 사용하여 각 로그 수준에 대한 파일 전송
new winstonDailyRotate({
level: "error",
dirname: path.join(logDirectory, "error"),
filename: "error-%DATE%.log",
datePattern: "YYYY-MM-DD",
zippedArchive: true, // 회전된 로그 파일에 대한 압축 활성화
maxSize: "20m", // 로그 파일 크기가 20MB를 초과하면 회전
maxFiles: "30d", // 30일 동안 로그 보관
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf(({ level, message, timestamp }) => {
return `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
}),
),
}),
new winstonDailyRotate({
level: "warn",
dirname: path.join(logDirectory, "warn"),
filename: "warning-%DATE%.log",
datePattern: "YYYY-MM-DD",
zippedArchive: true,
maxSize: "20m",
maxFiles: "30d",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf(({ level, message, timestamp }) => {
return `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
}),
),
}),
new winstonDailyRotate({
level: "info",
dirname: path.join(logDirectory, "info"),
filename: "info-%DATE%.log",
datePattern: "YYYY-MM-DD",
zippedArchive: true,
maxSize: "20m",
maxFiles: "30d",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf(({ level, message, timestamp }) => {
return `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
}),
),
}),
new winstonDailyRotate({
level: "debug",
dirname: path.join(logDirectory, "debug"),
filename: "debug-%DATE%.log",
datePattern: "YYYY-MM-DD",
zippedArchive: true,
maxSize: "20m",
maxFiles: "30d",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf(({ level, message, timestamp }) => {
return `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
}),
),
}),
],
exceptionHandlers: [
// 캐치되지 않은 (정의되지 않은 레벨) 예외를 기록하는 예외 처리기
new winstonDailyRotate({
dirname: path.join(logDirectory, "except"),
filename: "except-%DATE%.log",
datePattern: "YYYY-MM-DD",
zippedArchive: true,
maxSize: "20m",
maxFiles: "30d",
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf(({ level, message, timestamp }) => {
return `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
}),
),
}),
],
exitOnError: false, //캐치되지 않은 예외를 기록한 후 애플리케이션 실행을 계속합니다.
});
module.exports = logger;
로그를 저장할 경로와 레벨을 정의해준 뒤 사용하고 싶은 구성으로 로거를 작성해준다
예제를 보면 다양한 옵션들이 있고, 개발자마다 쓰는방법이 다 다르다
저장 경로 : 수준별로 저장될거기에 꽤나 지저분하다. 그냥 src 아래에 새로운 디렉토리를 만들었다.
레벨 정의 : 디폴트로 정의된 값이 있지만 모두 사용할 필요가 없어 필요한 수준만 사용했다.
미세먼지 팁으로, 로그가 기록될 때 해당 로그가 가지는 수준보다 낮은 수준에도 모두 기록된다.
즉, 레벨이 1인 로그가 들어오면 error을 제외한 warn, info, debug에 해당 로그가 기록된다.
logger 구성 : 작성해둔 레벨과 format, 레벨별로 작성된 transports를 넣어주고 마지막으로 정의되지않은 레벨 ( 시스템 에러 등 ) 을 캐치할 exceptionHandlers 를 넣어주면 된다.
morgan + winston
import morgan from "morgan";
import logger from "../config/logger.js";
const morganMiddleware = morgan((tokens, req, res) => {
const logMessage = `[${tokens.method(req, res)}] ${tokens.url(req, res,)} | ${tokens.status(req, res)}
| ${tokens.res(req, res, "content-length", )} - ${tokens["response-time"](req, res)} ms |
[Response] ${JSON.parse(req.body)}`;
const statusCode = res.statusCode;
// 응답 상태가 400 미만인지 확인(성공 응답)
if (statusCode < 400) {
logger.info(logMessage);
}
return null; // 로그 출력을 수정하지 않음을 나타내기 위해 null을 반환
});
module.exports = morganMiddleware;
errorMiddleware
import logger from "../config/logger.js";
function errorMiddleware(error, req, res, next) {
const statusCode = error.status;
res.status(statusCode).send(error.message);
const stackLines = error.stack.split("\n");
const truncatedStack = stackLines.slice(0, 5).join("\n");
const reqBodyString = JSON.stringify(req.body);
logger.error(
`[${req.method}] ${req.path} | ${statusCode} | [REQUEST] ${reqBodyString} | ${truncatedStack}`,
);
}
module.exports = errorMiddleware;
에러미들웨어가 미리 작성되어있다면, 그냥 logger를 불러와 넣어주기만하면 끝이다.
어떤 요청값에 에러가 났는지 확인하기위해 req.body와 에러스택을 추가해줬다. 단 에러스택이 너무 길어지면 오히려 가독성이 떨어지기에 5줄로 슬라이스 해준다.
이제 요청, 응답을 테스트하고, /logs 에 레벨별로 정리된 로그를 확인하면 된다.
debug는 모든 요청과 응답을 기록하니, 귀찮으면 그냥 이걸로만 확인한다
@todo
만약 요청이나 응답에 개인정보나 민감한 정보가 들어있을 경우 어떻게 블라인드처리 할것인가..