티스토리 뷰

nodejs

express에서 log 기록하기(winston)

안양사람 2021. 11. 9. 03:33
728x90
SMALL

배경

보통 우리들은 코딩할 대 로그를 찍는다. 개발할 때는 로그를 직접 볼 수 있지만 배포하고 나서는 그렇지 않다. 우리는 24시간 컴퓨터 앞에서 어디서 오류가 나는지 확인할 수 없다. 이걸 효율적으로 하기 위해서는 어떻게 해야할까?? 바로 기록이다. 로그를 기록하면 된다. 사실 이전에 node교과서를 보면서 공부했었지만 대충 쓰고 넘어갔던 기억이 난다. 그때 제대로 공부해놨다면 우아한테크캠프에서 프로젝트를 하면서 winston으로 로그를 기록했을 것이다. 앞으로 그런 기회가 있다면 내가 나서서 winston을 적용해야겠다.

 

winston 무작정 사용해보기

일단 문서를 보다가 생각보다 좀 길어서 적용부터 해보기로 했다. 일단 해보고 이런거구나 먼저 알아보자

 

logger.js

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'user-service' },
  transports: [
    //
    // - Write all logs with level `error` and below to `error.log`
    // - Write all logs with level `info` and below to `combined.log`
    //
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});

//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}

 

엔트리 파일

logger.info('hello');
logger.error('error');

 

이렇게 실행해보면 신기하게도 로그 파일이 생성된다. 대략 설명하자면 level이라는 것이 존재하는데 error, info 등 여러가지가 있는 것 같다. 그리고 로그파일에 서비스가 있는 것을 보니 그냥 이름을 적어주면 되는 것 같다. 그리고 transports에서는 생성할 파일이나 로그의 옵션을 지정해주는 것 같다.

 

사실 지금까지 얻은 정보만으로도 충분하다. 여기서 충분하다고 생각한다면 일단 밑의 글은 보지말자. 필요성을 느꼈을 때 다시 보자.

 

winston 세부 설정

당연하지만 충분하지 않다. 위에서는 그저 공식문서의 사용예시를 보고 사용하는 법을 익혔을 뿐이다. 우리가 로그를 찍을 때는 시간이라던가 날짜별로 파일을 만든다던가 하는 조금 더 세부적인 정보를 저장하고 싶다. 조금 더 살펴보자

 

createLogger

createLogger로 logger를 생성할 수 있다. 다음은 logger의 파라미터다.

level 'info' Log only if info.level less than or equal to this level
levels winston.config.npm.levels Levels (and colors) representing log priorities
format winston.format.json Formatting for info messages (see: Formats)
transports [] (No transports) Set of logging targets for info messages
exitOnError true If false, handled exceptions will not cause process.exit
silent false If true, all logs are suppressed

 

level

먼저 level에 대해 살펴보자. 다음과 같이 7단계의 레벨이 있다. 각자 진행하는 프로젝트의 규모에 따라 나누면 될 것 같다. 나는 솔직히 토이 프로젝트 수준에서는 error, warn, info 정도면 충분할 것 같다. 그러면 level: 'info'로 설정하면 된다.

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

 

levels

level을 커스텀할수도 있다. 있는 그대로 사용해도 당연하지만 문제 없다. 하지만 이왕 사용하는거 커스텀해서 사용하는 것도 나쁘지는 않다고 생각한다. 아래와 같이 사용하면 된다. 사용할 수 있는 스타일은 아래와 같다. 

  • Font styles: bold, dim, italic, underline, inverse, hidden, strikethrough.
  • Font foreground colors: black, red, green, yellow, blue, magenta, cyan, white, gray, grey.
  • Background colors: blackBG, redBG, greenBG, yellowBG, blueBG magentaBG, cyanBG, whiteBG
const myCustomLevels = {
  levels: {
    error: 0,
    info: 1,
  },
  colors: {
    info: 'bold red cyanBG',
    error: 'green',
  },
};

const logger = createLogger({
  level: 'info',
  levels: myCustomLevels.levels,
  ~~
})

if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new transports.Console({
      format: format.combine(format.colorize(), format.simple()),
    }),
  );
}

winston.addColors(myCustomLevels.colors);

 

Formats

printf

winston.format으로 접근할 수 있다. 포맷을 지정하고 싶다면 winston.format.printf를 사용하면 된다. 기본 포맷을 사용해도 괜찮지만 각자 자신만의 스타일로 포맷을 지정한다면 조금 더 쉽게 알아볼 수 있을 것 같다. 아래와 같이 말이다.

const myFormat = printf(({ level, message, label, timestamp }) => {
  return `${timestamp} [${label}] ${level}: ${message}`;
});

const logger = createLogger({
  format: combine(
    label({ label: 'right meow!' }),
    timestamp(),
    myFormat
  ),
  transports: [new transports.Console()]
});

 

timestamp

말 그대로 시간이다. 나는 시간은 꼭 필요하다고 생각해서 넣어준다. 우리가 흔히 원하는 yyyy mm~~ 형태로 출력하길 원한다면 아래와 같이 넣어주면 된다.

timestamp({ format: 'YYYY-MM-DD HH:mm:ss', }),

 

splat

%d, %s를 가능하게 해준다.

 

json, simple

json형태로 출력하거나 간단한 형태로 출력한다. 보통 로그는 simple형태로 찍어주고 저장은 json으로 하는 것 같다.

 

prettyPrint

예쁘게 프린트해준다. 당연히 printf로 설정해줄때는 안해줌

 

combine

여러 포맷들을 하나로 합친다. 필수기능!!

 

Transports

말그대로 운송수단이다. 여기서 파일로 저장할지 로그를 찍을지 선택할 수도 있고 파일에 어떤 level위의 로그를 저장할지 지정할 수도 있다. 

  transports: [
    new transports.File({ filename: 'error.log', level: 'error' }),
    new transports.File({ filename: 'combined.log', level: 'info' }),
  ],

 

winston-daily-rotate-file

이제 사용법은 다 익혔다. 그런데 폴더별로 나누거나 파일을 나누고 싶다. 이런 세부적인 설정은 winston-daily-ratate-file을 설치해서 하면 된다. 참 간편하다. 그냥 설치해서 사용법만 익히면 끝이다.

다음과 같은 옵션이 있다.

  • frequency: A string representing the frequency of rotation. This is useful if you want to have timed rotations, as opposed to rotations that happen at specific moments in time. Valid values are '#m' or '#h' (e.g., '5m' or '3h'). Leaving this null relies on datePattern for the rotation times. (default: null)
  • datePattern: A string representing the moment.js date format to be used for rotating. The meta characters used in this string will dictate the frequency of the file rotation. For example, if your datePattern is simply 'HH' you will end up with 24 log files that are picked up and appended to every day. (default: 'YYYY-MM-DD')
  • zippedArchive: A boolean to define whether or not to gzip archived log files. (default: 'false')
  • filename: Filename to be used to log to. This filename can include the %DATE% placeholder which will include the formatted datePattern at that point in the filename. (default: 'winston.log.%DATE%')
  • dirname: The directory name to save log files to. (default: '.')
  • stream: Write directly to a custom stream and bypass the rotation capabilities. (default: null)
  • maxSize: Maximum size of the file after which it will rotate. This can be a number of bytes, or units of kb, mb, and gb. If using the units, add 'k', 'm', or 'g' as the suffix. The units need to directly follow the number. (default: null)
  • maxFiles: Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)
  • options: An object resembling https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options indicating additional options that should be passed to the file stream. (default: { flags: 'a' })
  • auditFile: A string representing the name of the audit file. This can be used to override the default filename which is generated by computing a hash of the options object. (default: '..json')
  • utc: Use UTC time for date in filename. (default: false)
  • extension: File extension to be appended to the filename. (default: '')
  • createSymlink: Create a tailable symlink to the current active log file. (default: false)
  • symlinkName: The name of the tailable symlink. (default: 'current.log')

 

내가 사용하지 않는 기능들

filtering

info 객체를 필터링하기 원한다면 로그를 저장하지 않을 수도 있다.

const { createLogger, format, transports } = require('winston')

// Ignore log messages if they have { private: true }
const ignorePrivate = format((info, opts) => {
  if (info.private) {
    return false
  }
  return info
})

const logger = createLogger({
  format: format.combine(ignorePrivate(), format.json()),
  transports: [new transports.Console()],
})

// Outputs: {"level":"error","message":"Public error to share"}
logger.log({
  level: 'error',
  message: 'Public error to share',
})

// Messages with { private: true } will not be written when logged.
logger.log({
  private: true,
  level: 'error',
  message: 'This is super secret - hide it.',
})

 

custom format 만들기

이런 방법도 있다고 한다. 근데 솔직히 나는 지금 이걸 사용하지 않을거라 자세히 보지 않았다.

const { format } = require('winston');

const volume = format((info, opts) => {
  if (opts.yell) {
    info.message = info.message.toUpperCase();
  } else if (opts.whisper) {
    info.message = info.message.toLowerCase();
  }

  return info;
});

// `volume` is now a function that returns instances of the format.
const scream = volume({ yell: true });
console.dir(scream.transform({
  level: 'info',
  message: `sorry for making you YELL in your head!`
}, scream.options));
// {
//   level: 'info'
//   message: 'SORRY FOR MAKING YOU YELL IN YOUR HEAD!'
// }

// `volume` can be used multiple times to create different formats.
const whisper = volume({ whisper: true });
console.dir(whisper.transform({
  level: 'info',
  message: `WHY ARE THEY MAKING US YELL SO MUCH!`
}, whisper.options));
// {
//   level: 'info'
//   message: 'why are they making us yell so much!'
// }

 

logger 제거 및 추가

다음과 같이 logger를 추가하고 제거할 수 있다. 나는 지금 쓸 일이 없을 것 같다.

const files = new winston.transports.File({ filename: 'combined.log' });
const console = new winston.transports.Console();

logger
  .clear()          // Remove all transports
  .add(console)     // Add console transport
  .add(files)       // Add file transport
  .remove(console); // Remove console transport

 

이외에도 많은 기능들이 있다. 근데 일단은 이정도만 하고 넘어가야겠다. 이정도면 충분하다

 

실제 적용하기

logger.ts

import { createLogger, format, transports } from 'winston';
import WinstonDaily from 'winston-daily-rotate-file';

const myFormat = format.printf(({ level, message, timestamp }) => {
  return `${timestamp} ${level}: ${message}`;
});

const logger = createLogger({
  level: 'info',
  format: format.combine(
    format.timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    format.splat(),
    format.json(),
    myFormat,
  ),
  transports: [
    new WinstonDaily({
      level: 'error',
      datePattern: 'YYYY-MM-DD',
      dirname: 'logs/error',
      filename: `%DATE%.error.log`,
      maxFiles: '30d',
      zippedArchive: true,
    }),
    new WinstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: 'logs',
      filename: `%DATE%.debug.log`,
      maxFiles: '30d',
      zippedArchive: true,
    }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new transports.Console({
      format: format.combine(format.colorize(), format.simple()),
    }),
  );
}

export default logger;

 

후기

꼭 해봐야겠다고 생각한 기능이였고 설정도 좀 꼼꼼하게 읽어보려고 했는데 글을 쓰고보니 특별한 내용은 없다. 더 깊게 파고들자니 지금은 때가 아닌것같고 사용하다가 부족함이 느껴진다면 throw error부분이나 이런 부분을 조금 더 찾아보고 글을 보완해야겠다. 지금 프로젝트에 적용해서 테스트해봤는데 정상적으로 작동한다. 앞으로는 winston을 애용해야겠다.

728x90
LIST
댓글
공지사항