...
Winston 모듈
어떤 서버든지 실제로 서비스를 운영하려면 로그를 꼼꼼히 남기는 것은 필수이다.
Log는 에러를 파악할 수 있는 열쇠이기 때문에 서버를 운영한다고 하면 로그 시스템을 구축해서 시스템을 운영해야 한다.
예를들어 어떤 사람이, 어떤 이벤트, 어떤 데이터를 요청했는지, 어떤 동작을 하다가, 어떤 트랜잭션을 돌리다가, 어떤 에러가 났는지 이를 정밀 관리할수 있다.
Node.js에서는 log를 효율적으로 관리할 수 있게 도와주는 모듈인 winston.js가 았다.
윈스톤(winston.js)는 실제 서버를 운영할 때 console.log 와 console.error 를 대체하기 위한 모듈이다.
console.log 와 console.error 도 개발 중에는 편리하게 콘솔 로그로 서버의 상황을 파악할 수 있지만, 실제 배포 시에는 사용하기 어렵다.
왜냐하면 console 객체의 메서드들이 언제 호출되었는지 파악하기 힘들 뿐만 아니라, 서버가 종료되는 순간 쌓여있던 로그들도 사라져 버리기 때문이다.
이와 같은 상황을 방지하려면 로그를 외부로 파일에 저장해서 관리하는 것이 이상적인데, 이때 winston을 사용한다고 보면 된다.
Winston 모듈 사용법
$ npm i winston winston-daily-rotate-file # 2개 설치
winston-daily-rotate-file은 로그 파일을 관리해주는 모듈이다.
기본적으로 하루(1일) 단위로 새 로그 파일을 생성해주고, 날짜별로 로그파일을 관리하게 구분해 주며, 로그 파일의 최대 크기와 최대 저장 파일 개수 등을 설정할 수 있다.
1. winston 로그 경로 & 출력형식 설정
winston을 설치한 뒤, logger.js 파일을 생성해준다.
const winston = require('winston');
const winstonDaily = require('winston-daily-rotate-file');
const process = require('process');
const { combine, timestamp, label, printf } = winston.format;
//* 로그 파일 저장 경로 → 루트 경로/logs 폴더
const logDir = `${process.cwd()}/logs`;
//* log 출력 포맷 정의 함수
const logFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} [${label}] ${level}: ${message}`; // 날짜 [시스템이름] 로그레벨 메세지
});
우선 필요 모듈들을 불러와준다.
그리고 winston.format 에서 필요한 메소드와 파라미터들을 객체 변수에 구조분해로 저장해준다.
process 모듈은 process.cwd() 값인 루트 경로를 얻기위해 불러와주었다.
로그 파일 저장 경로를 루트경로/logs 폴더로 설정해준다.
logFormat 이라는 로그 출력 포맷 모양을 지정해준다.
timestamp [label] level: message 형식으로 로그를 출력해줄 것인데, 다음 사진과 같이 출력 포맷으로 기록된다고 보면 된다.
2. winston 로거 생성 (createLogger)
// ...
const logger = winston.createLogger({
//* 로그 출력 형식 정의
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
label({ label: 'Winston 연습 어플리케이션' }), // 어플리케이션 이름
logFormat, // log 출력 포맷
//? format: combine() 에서 정의한 timestamp와 label 형식값이 logFormat에 들어가서 정의되게 된다. level이나 message는 콘솔에서 자동 정의
),
});
이제 본격적으로 로그 기록(logging)을 하는 메서드를 생성해볼 차례이다.
winston 패키지의 createLogger 메서드로 logger를 만들 수 있다.
그리고 format 인자를 줘서 메세지에 대한 기본 설정을 한다.
format은 로그의 형식을 지정할 수 있다.
json, label, timestamp, printf, simple, combine 등의 다양한 형식으로 지정이 가능하다.
기본적으로는 JSON 형식으로 기록하지만, combine은 여러 형식을 혼합해서 사용할 때 쓴다.
timestamp로 날짜 형식을 정하고, label로 어플리케이션 이름을 지정해준다. 그리고 위에서 지정한 logFormat 을 넣어, 설정한 timestamp와 label을 인자로 받아 printf 되게 설정해준다.
나머지 인자의 level과 message는 자동으로 콘솔에서 지정된다.
3. winston 로깅 형식 (transports)
// ...
/*
* Log Level
* error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
*/
const logger = winston.createLogger({
//* 로그 출력 형식 정의
format: combine(
// ...
),
//* 실제 로그를 어떻게 기록을 한 것인가 정의
transports: [
//* info 레벨 로그를 저장할 파일 설정 (info: 2 보다 높은 error: 0 와 warn: 1 로그들도 자동 포함해서 저장)
new winstonDaily({
level: 'info', // info 레벨에선
datePattern: 'YYYY-MM-DD', // 파일 날짜 형식
dirname: logDir, // 파일 경로
filename: `%DATE%.log`, // 파일 이름
maxFiles: 30, // 최근 30일치 로그 파일을 남김
zippedArchive: true, // 아카이브된 로그 파일을 gzip으로 압축할지 여부
}),
//* error 레벨 로그를 저장할 파일 설정 (info에 자동 포함되지만 일부러 따로 빼서 설정)
new winstonDaily({
level: 'error', // error 레벨에선
datePattern: 'YYYY-MM-DD',
dirname: logDir + '/error', // /logs/error 하위에 저장
filename: `%DATE%.error.log`, // 에러 로그는 2020-05-28.error.log 형식으로 저장
maxFiles: 30,
zippedArchive: true,
}),
],
});
그리고 다음 파라미터에 transports 를 설정한다.
transports는 로그 저장 방식을 정의한다.
그리고 winston-daily-rotate-file 에서 불러온 new winstonDaily 를 정의해 어떤 레벨의 로그를 저장할때 어떤 형식으로 몇일동안 보관할지를 상세히 설정 할 수 있다.
winston의 로그 레벨은 순서가 있는데, 다음과 같이 구성되어있다.
error: 0 , warn: 1 , info: 2 , http: 3 , verbose: 4 , debug: 5 , silly: 6
숫자가 낮을 수록 priority가 높다고 보면 된다. (error 가 가장 위험한 로그)
winston 로그의 level을 설정하면 해당 레벨 이상의 priority를 가지는 즉, 숫자가 같거나 낮은 로그를 함께 출력하게 된다. (만일 level을 info로 설정하면 2보다 낮은 error와 warn 로그도 같이 출력)
위의 코드에서는 info와 error를 따로 지정해서 로깅 형식을 설정해 주었다. 보다 관리를 위해 error 로깅은 다른 파일명과 다른 폴더에 저장하도록 추가로 지정했다.
이외의 winston daily rotate 로깅 설정은 다음과 같다.
new winstonDaily({
frequency: 회전 빈도를 나타내는 문자열입니다. 이는 특정 시간에 발생하는 회전과 달리 시간이 지정된 회전을 원하는 경우에 유용합니다. 유효한 값은 '#m' 또는 '#h'(예: '5m' 또는 '3h')입니다. 이 null을 남겨두는 datePattern것은 회전 시간 에 의존합니다 . (기본값: null)
datePattern: 회전에 사용할 moment.js 날짜 형식 을 나타내는 문자열 입니다. 이 문자열에 사용된 메타 문자는 파일 회전 빈도를 나타냅니다. 예를 들어, datePattern이 단순히 'HH'인 경우 매일 선택하여 추가되는 24개의 로그 파일로 끝납니다. (기본값: 'YYYY-MM-DD')
zippedArchive: 아카이브된 로그 파일을 gzip으로 압축할지 여부를 정의하는 부울입니다. (기본값: '거짓')
filename: 로그에 사용할 파일 이름입니다. 이 파일 이름은 파일 이름의 %DATE%해당 지점에 서식이 지정된 datePattern을 포함하는 자리 표시자를 포함할 수 있습니다 . (기본값: 'winston.log.%DATE%')
dirname: 로그 파일을 저장할 디렉터리 이름입니다. (기본: '.')
stream: 사용자 지정 스트림에 직접 쓰고 회전 기능을 우회합니다. (기본값: null)
maxSize: 회전할 파일의 최대 크기입니다. 바이트 수 또는 kb, mb 및 GB 단위가 될 수 있습니다. 단위를 사용하는 경우 접미사로 'k', 'm' 또는 'g'를 추가합니다. 단위는 숫자를 직접 따라야 합니다. (기본값: null)
maxFiles: 보관할 최대 로그 수입니다. 설정하지 않으면 로그가 제거되지 않습니다. 이는 파일 수 또는 일 수일 수 있습니다. 일을 사용하는 경우 접미사로 'd'를 추가합니다. (기본값: null)
options: 파일 스트림에 전달되어야 하는 추가 옵션을 나타내는 'https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options' 와 유사한 객체 . (기본값: { flags: 'a' })
auditFile : 감사 파일의 이름을 나타내는 문자열. 옵션 개체의 해시를 계산하여 생성된 기본 파일 이름을 재정의하는 데 사용할 수 있습니다. (기본값: '..json')
utc : 파일 이름의 날짜에 UTC 시간을 사용합니다. (기본값: 거짓)
extension : 파일 이름에 추가할 파일 확장자. (기본: '')
createSymlink : 현재 활성 로그 파일에 대한 tailable symlink를 만듭니다. (기본값: 거짓)
symlinkName : tailable symlink의 이름입니다. (기본값: 'current.log')
}),
4. winston 예외 로그 (exceptionHandlers)
추가로 코드 에러 뿐만 아니라 try catch 에러도 잡기 위해 밑에 exceptionHandlers 를 설정해 주었다.
/*
* Log Level
* error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
*/
const logger = winston.createLogger({
//* 로그 출력 형식 정의
format: combine(
// ...
),
//* 실제 로그를 어떻게 기록을 한 것인가 정의
transports: [
//* info 레벨 로그를 저장할 파일 설정 (info: 2 보다 높은 error: 0 와 warn: 1 로그들도 자동 포함해서 저장)
new winstonDaily({
level: 'info', // info 레벨에선
// ...
}),
//* error 레벨 로그를 저장할 파일 설정 (info에 자동 포함되지만 일부러 따로 빼서 설정)
new winstonDaily({
level: 'error', // error 레벨에선
// ...
}),
],
//* uncaughtException 발생시 파일 설정
exceptionHandlers: [
new winstonDaily({
level: 'error',
datePattern: 'YYYY-MM-DD',
dirname: logDir,
filename: `%DATE%.exception.log`,
maxFiles: 30,
zippedArchive: true,
}),
],
});
5. winston 개발 환경설정
// ...
//* Production 환경이 아닌, 개발 환경일 경우 파일 들어가서 일일히 로그 확인하기 번거로우니까 화면에서 바로 찍게 설정 (로그 파일은 여전히 생성됨)
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(), // log level별로 색상 적용하기
winston.format.simple(), // `${info.level}: ${info.message} JSON.stringify({ ...rest })` 포맷으로 출력
),
}),
);
}
module.exports = logger;
Production 환경이 아닌, 개발 환경일 경우 파일 들어가서 일일히 로그 확인하기 번거로우니까 화면에서 바로 찍게 설정해준다. 그래도 로그 파일은 여전히 생성된다.
그리고 마지막으로 모듈 배포를 위해 module.exports 를 해준다.
이렇게 logger 객체를 만들어 다른 파일에서 사용하면 된다.
winston 실행하기
다음은 위에서 구성한 logger.js 의 전체 코드이다. 모듈이 잘 작동하는지 테스트해보자.
const winston = require('winston');
const winstonDaily = require('winston-daily-rotate-file');
const process = require('process');
const { combine, timestamp, label, printf } = winston.format;
//* 로그 파일 저장 경로 → 루트 경로/logs 폴더
const logDir = `${process.cwd()}/logs`;
//* log 출력 포맷 정의 함수
const logFormat = printf(({ level, message, label, timestamp }) => {
return `${timestamp} [${label}] ${level}: ${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' }),
label({ label: 'Winston 연습 어플리케이션' }), // 어플리케이션 이름
logFormat, // log 출력 포맷
//? format: combine() 에서 정의한 timestamp와 label 형식값이 logFormat에 들어가서 정의되게 된다. level이나 message는 콘솔에서 자동 정의
),
//* 실제 로그를 어떻게 기록을 한 것인가 정의
transports: [
//* info 레벨 로그를 저장할 파일 설정 (info: 2 보다 높은 error: 0 와 warn: 1 로그들도 자동 포함해서 저장)
new winstonDaily({
level: 'info', // info 레벨에선
datePattern: 'YYYY-MM-DD', // 파일 날짜 형식
dirname: logDir, // 파일 경로
filename: `%DATE%.log`, // 파일 이름
maxFiles: 30, // 최근 30일치 로그 파일을 남김
zippedArchive: true,
}),
//* error 레벨 로그를 저장할 파일 설정 (info에 자동 포함되지만 일부러 따로 빼서 설정)
new winstonDaily({
level: 'error', // error 레벨에선
datePattern: 'YYYY-MM-DD',
dirname: logDir + '/error', // /logs/error 하위에 저장
filename: `%DATE%.error.log`, // 에러 로그는 2020-05-28.error.log 형식으로 저장
maxFiles: 30,
zippedArchive: true,
}),
],
//* uncaughtException 발생시 파일 설정
exceptionHandlers: [
new winstonDaily({
level: 'error',
datePattern: 'YYYY-MM-DD',
dirname: logDir,
filename: `%DATE%.exception.log`,
maxFiles: 30,
zippedArchive: true,
}),
],
});
//* Production 환경이 아닌, 개발 환경일 경우 파일 들어가서 일일히 로그 확인하기 번거로우니까 화면에서 바로 찍게 설정 (로그 파일은 여전히 생성됨)
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(), // 색깔 넣어서 출력
winston.format.simple(), // `${info.level}: ${info.message} JSON.stringify({ ...rest })` 포맷으로 출력
),
}),
);
}
module.exports = logger;
const logger = require("./logger");
logger.info("hello world");
logger.error("hello world");
logger.warn("hello world");
logger.debug("hello world");
logger.verbose("hello world");
logger.silly("hello world");
info, warn, error 등의 메서드를 사용하면 해당 심각도가 적용된 로그가 기록되게 된다.
만일 메세지를 여러번 써야할 경우에는 다음과 같이 구성해준다.
// console.log('naver profile : ', profile);
logger.info('naver profile : ', { message: profile }); // console.log와 달리 뒤에 message 객체로 써주어야 한다.
모듈 작동에 문제없다면, 다음과 같이 간단하게 서버 코드를 작성해주고 노드 서버를 실행해본다.
const express = require('express');
const logger = require('./logger');
const app = express();
app.get('/', (req, res) => {
logger.info('GET /');
res.sendStatus(200);
});
app.get('/error', (req, res) => {
logger.error('Error message');
res.sendStatus(500);
});
app.listen(3000, () => {
logger.info('Server listening on port 3000');
});
http://localhost:3000 에 접속하면 logger.info() 가 로그파일에 적히게 된다.
그리고 http://localhost:3000/error 에 접속하면 logger.error 가 /logs/error 폴더에 로그 파일이 적히게 된다.
info 이상 단계의 모든 로그를 기록하도록 되어 있는 2022-05-30.log 파일에는 info와 error 단계의 로그가 출력 된다.
그리고 error 단계의 로그만 기록하도록 되어 있는 2022-05-30.error.log에는 error 단계의 로그만 출력 되었다.
이렇게 로그를 콘솔에만 출력하는 것이 아니라, 파일로도 저장할 수 있어 실제 서비스를 운영할 때 유용하다.
.42ebfd898bd6a617661d9d083ce1a25eb6d87249-audit.json은 Winston에서 로그 파일의 맵을 만드는 데 사용하는 중요한 파일이다. 만일 이 파일이 없으면 매일 날짜 갱신기능 을 사용할 수 없다.
Winston + Morgan 같이 사용하기
http 요청 응답을 위한 로깅인 morgan을 winston으로 관리해 해당 로그를 외부 로그 파일에 출력되도록 해보자.
위에서 winston 로그 level에 http 레벨이 있다는걸 봤을텐데 바로 여기에 쓰이는 것이다.
만일 예제 코드의 morgan 확장 옵션에 대해 잘 모른다면 다음 포스팅을 참고하길 바란다.
다음과 같이 custom morgan middleware를 만든다.
const morgan = require('morgan');
const logger = require('../config/logger');
const dotenv = require('dotenv');
dotenv.config(); // 노드 환경 변수 사용
const format = () => {
const result = process.env.NODE_ENV === 'production' ? 'combined' : 'dev';
return result;
};
// 로그 작성을 위한 Output stream옵션.
const stream = {
write: (message) => {
// console.log(message);
logger.info(message);
},
};
// 로깅 스킵 여부 (만일 배포환경이면, 코드가 400 미만라면 함수를 리턴해 버려서 로그 기록 안함. 코드가 400 이상이면 로그 기록함)
const skip = (_, res) => {
if (process.env.NODE_ENV === 'production') {
return res.ststusCode < 400;
}
return false;
};
//? 적용될 moran 미들웨어 형태
const morganMiddleware = morgan(format(), { stream, skip });
/*
morgan('dev', {
stream = {
write: (message) => {
// console.log(message);
logger.info(message);
},
},
skip = (_, res) => {
if (process.env.NODE_ENV === 'production') {
return res.ststusCode < 400;
}
return false;
};
})
*/
module.exports = morganMiddleware;
그리고 custom morgan middleware를 서버 파일 app.js에 다음과 같이 적용해주면 된다.
//app.js
const morganMiddleware = require('morganMiddleware');
// ...
app.use(morganMiddleware)
이상한 특수 문자 출력 해결하기
만일 winston과 morgan을 함께 적용했더이 아래 사진과 같이 이상한 특수문자 ESC[0m 가 출력 된다면, logger.info(message)의 message 부분 로그 문자열을 정규식으로 제거하면 된다.
(지적해주신 개발 토끼 님께 감사의 인사를 드린다)
// 로그 작성을 위한 Output stream옵션.
const stream = {
write: (message) => {
logger.info(
message.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "")
);
},
};
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.