본문 바로가기

웹개발/웹 개발 & 프로그래밍

[NodeJS] 로그 남기는 Winston, 그리고 morgan과 결합

프로그래밍을 하면서 장애가 나지 않는 게 가장 좋지만, 그럴 수는 없다. 분명 장애는 나기 마련이다!!

 

그럴 때 로그가 없다면... 좌절하고 말 것이다. 에러가 난 원인을 파악할 수 없기 때문에 당장 문제를 해결하기도 어려울 뿐더러 재발 방지도 힘들기 때문이다. 

 

NodeJS에도 Winston이라는 로그 모듈이 있다. 가장 많이 쓰인다. 프로젝트는 아래 프로젝트를 사용한다. 청소년이 참여적으로 문학을 하길 바라는 마음으로 만들고있는 프로젝트다. 관심있는 분은 함께 해 주시라!

https://github.com/JaeMiGarden/MunhakMap

 

JaeMiGarden/MunhakMap

Contribute to JaeMiGarden/MunhakMap development by creating an account on GitHub.

github.com

 

 

Winston은 이 친구랑 별 상관없다.

1. Install

우선 사이트를 들어가보면 다음과 같은 화면을 만날 수 있다.

(https://www.npmjs.com/package/winston)

와우 Weekly Downloads가 무려 643만건!!!

 

 

npm i --save winston

 

다운로드 받는다. 그리고 우리는 날짜별로 로그를 저장할 것이므로 winston-daily-rotate-file도 설치한다.

추가로 시간을 Asia/Seoul로 맞춰주기위해 moment와 moment-timezone을 다운로드 받는다.

 

npm i --save moment moment-timezone winston-daily-rotate-file

 

 

폴더구조. winston.js를 추가했다.

2. Import

그 후에 winston.js를 추가했다. 

import {createLogger, format, transports, info} from 'winston';
import winstonDaily from 'winston-daily-rotate-file';
import moment from 'moment';
import fs from 'fs';
import 'moment-timezone';

const { combine, printf } = format;

winston.js의 첫 화면에 다운로드받은 모듈을 Import했다.

 

3. Use 

우리는 Log 파일을 만드려고 한다. 그러면 뭘 해야할까?

 

일단 어디에 저장할 지를 정해야한다. logs 폴더에 저장하면 찾기 편할 것 같고 파일 이름은 프로젝트명 + 오늘 날짜로 하는게 좋겠다. 그리고 로그 파일이 계속 쌓이기만하면 이것도 언젠간 문제가 될 것 같다. 14일이 지나면 삭제되도록 해보자.

import {createLogger, format, transports, info} from 'winston';
import winstonDaily from 'winston-daily-rotate-file';
import moment from 'moment';
import fs from 'fs';
import 'moment-timezone';

const { combine, printf } = format;

const dailyRotateFileTransport = new transports.DailyRotateFile({
    filename: logDir + 'Munhak-%DATE%.log',
    datePattern: 'YYYY-MM-DD-HH',
    zippedArchive: true,
    maxFiles: '14d'
});

DailyRotateFile 함수의 다양한 옵션은 https://github.com/winstonjs/winston-daily-rotate-file에서 확인할 수 있다.

 

지금까지 필요한 모듈을 불러오고, 로그를 어디에 저장할 지 옵션을 정했다. 그럼 이제 진짜 log를 남길 차례다.

 

log의 레벨은 총 7개가 있다. 이 로그레벨은 RFC5424에서 정해졌고 winston도 이를 준수한다. 가장 중요한 것부터 덜 중요한 것 순으로 가며 어떤 사건이 발생해서 로그를 남기려할 때 그 Logger의 레벨이하만 기록한다. 즉 Logger의 레벨이 2라면 debug의 log는 기록하지 않는다. 

const levels = { 
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  verbose: 4,
  debug: 5,
  silly: 6
};

 

Logger를 만들기 전에 시간 설정을 하자. 난 정확한 시간을 보고싶다!

moment.tz.setDefault('Asia/Seoul');
const timeStamp = () => moment().format('YYYY-MM-DD HH:mm:ss');

 이 코드를 추가하면 timeStamp는 현재 대한민국 표준시를 나타내게된다.

 

const loggingFormat = printf(({ level, message }) => {
    return `${timeStamp()} ${level} : ${message}`;
})

로그의 형식을 지정해준다. 

 

로그를 저장할 폴더가 없다면 만들어주고, Logger를 만들어서 출력하도록 해볼것이다.

 

if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir);
}

const infoTransport = new transports.Console({
  level: 'info'
})

const errorTransport = new transports.Console({
  level: 'error'
})

const logger = createLogger({
  format: combine(
    loggingFormat
    ),
  transports: [infoTransport, errorTransport, dailyRotateFileTransport]
})

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

export { logger, stream };

이렇게하면 winston.js의 작성은 끝이 난다. 풀 코드는 다음과 같다.

 

import {createLogger, format, transports, info} from 'winston';
import winstonDaily from 'winston-daily-rotate-file';
import moment from 'moment';
import fs from 'fs';
import 'moment-timezone';

const { combine, printf } = format;

const logDir = __dirname + "/../logs";
const dailyRotateFileTransport = new transports.DailyRotateFile({
    filename: 'logs/Munhak-%DATE%.log',
    datePattern: 'YYYY-MM-DD-HH',
    zippedArchive: true,
    maxFiles: '14d'
});

moment.tz.setDefault('Asia/Seoul');
const timeStamp = () => moment().format('YYYY-MM-DD HH:mm:ss');

const loggingFormat = printf(({ level, message }) => {
    return `${timeStamp()} ${level} : ${message}`;1
})

if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir);
}

const infoTransport = new transports.Console({
  level: 'info'
})

const errorTransport = new transports.Console({
  level: 'error'
})

const logger = createLogger({
  format: combine(
    loggingFormat
    ),
  transports: [infoTransport, errorTransport, dailyRotateFileTransport]
})

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

export { logger, stream };

 

이렇게 운영해도 되지만 morgan이랑 결합하면 더 좋다. 그럴려면 app.js로 가자

 

import express from 'express';
import route from './router/routes';
import path from 'path';
import passport from 'passport';
import session from 'express-session';
import cookie_parser from 'cookie-parser';
import bodyParser from 'body-parser';
import helmet from 'helmet';
import morgan from 'morgan';
import MongoStore from 'connect-mongo';
import mongoose from 'mongoose';
import './auth/passport'
import { localsMiddleware } from './middleware/middleware';

import globalRouter from './router/globalRouter';
import boardRouter from './router/boardRouter';
import apiRouter from './router/apiRouter';

import { stream } from './config/winston';

const app = express();

const CookieStore = MongoStore(session);

app.use(helmet());
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, "templates/"));
app.use(express.static(path.join(__dirname, "/templates")));
app.use('/uploads', express.static(path.join(__dirname, "/uploads")));
app.use('/assets', express.static(path.join(__dirname, '/assets')));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cookie_parser()); 
app.use(morgan('HTTP/:http-version :method :remote-addr :url :remote-user :status :res[content-length] :referrer :user-agent :response-time ms', { stream }));

app.use(session({
    secret: 'munhak',
    resave: true,
    saveUninitialized: false,
    store: new CookieStore({mongooseConnection: mongoose.connection })
}));

app.use(passport.initialize());
app.use(passport.session());
app.use(localsMiddleware);

app.use(route.home, globalRouter);
app.use(route.board, boardRouter);
app.use(route.api, apiRouter);
export default app;

 

뭐가 엄청나게 많아보이지만 중요한 건 중간에 있는 app.use(morgan~~ 부분이다

 

morgan은 본인이 만들어내는 로그를 Customizing 할 수 있도록 지원하고 있다.

https://www.npmjs.com/package/morgan

 

morgan

HTTP request logger middleware for node.js

www.npmjs.com

 

:http-version --> 이 요청의 HTTP Version

:method --> Request의 방법이 Get Post Put Delete 등 무엇인가.

:remote-addr --> request의 ip

:url --> 어떤 url로 온 request인지

...

...

 

사이트에 옵션이 많으므로 나타내고 싶은 정보를 표현해주면된다. 이제 실행해보자!

 

 

4. Conclusion

 

log가 생겼다!!!!
로그도 잘 떳다!!

2020-08-07 18:07:36 info : HTTP/1.1 GET ::1 / - 304 - - Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Whale/2.8.105.18 Safari/537.36 378.029 ms

2020-08-07 18:07:36 info : HTTP/1.1 GET ::1 /api/totalBoard - 200 11329 http://localhost:4000/ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Whale/2.8.105.18 Safari/537.36 91.941 ms

2020-08-07 18:07:36 info : HTTP/1.1 GET ::1 /favicon.ico - 404 150 http://localhost:4000/ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Whale/2.8.105.18 Safari/537.36 1.861 ms

 로그 저장도 잘됐다!!! 지금은 GET 다음에 ::1이 나오지만 실제 웹서비스에 올리면 IP주소가 뜰 것이다. 지금은 localhost 환경에서 동작하기 때문에 저렇게 나온다

 

이로써 Log를 저장할 수 있게됐다 ㅎㅎ!!

 

그런데 이렇게 쌓인 로그를 어떻게 분산할 수 있을지 궁금하다.

 

다음엔 이걸 공부하고 공유해보려한다!