타입스크립트 Logger 클래스 작성

"Homor Simpson from ancient egyptian mural painting in the distance." from DALL-E 2
[원본]에서 보시는게 더 깔끔합니다.

www.notion.so/logger-5955a78345c44fb4886c88c00000bba8

 
[개발환경]
  • 운영체제: 윈도우 10 Pro 64비트
    • 빌드버전: 1903
  • CPU: Intel(R) Core(TM) i7-7700
  • 램: 32GB
  • Node.js 버전: v12.18.2
  • TypeScript 버전: 3.9.5
  • winston 버전: 3.3.3
 
 
[tsconfig.json]
{
    "compilerOptions": {
        "target": "ES2019",
        "module": "commonjs",
        "noImplicitAny": false,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
    }
}
  • 이 중에서 특히 "noImplicitAny"는 false로 설정해야 함
 
[logger.ts]
import moment from 'moment';
import winston from 'winston';
import path from 'path';
import mkdirp from 'mkdirp';
import fs from 'fs';

const enum log_level_e {
    LogLevelDebug = 'debug',
    LogLevelInfo = 'info',
    LogLevelError = 'error'
};

class logger_base {
    public static readonly kLogLevel: string = log_level_e.LogLevelDebug;
    public static readonly kMaxFileSize: number = 1024 * 1024 * 100; //100MB
    public static readonly kNumMaxFiles: number = 100; //로그파일 최대 100개
    public static readonly kFilename: string = 'mam_server.log';
    public static readonly kMaxFilenameLength: number = 20;
    //INFO: 로그 저장 폴더
    public static readonly kLogPath: string = './logs';
    //INFO: 프로젝트 최상위 폴더
    public static readonly kProjRootPath: string = path.join(__dirname, '..');
    //INFO: 파일이름만 출력할 경우 false, 경로까지 출력할 경우 true
    public static readonly kUseRelativePath: boolean = false;

    private readonly writer: winston.Logger;

    constructor(){
   		this.makeLoggerFolder();

    	this.writer = this.getLogger();
    }

    private makeLoggerFolder(){
        try{
        	mkdirp.sync(logger_base.kLogPath);
        }
        catch(ex){
            console.error(`Create logger path FAILED; ${ex.message}`);
            return;
        }

        console.info(`Create logger folder SUCCESS`);
    }

    private getTimeStampFormat(): string {
    	return moment().format('YYYY-MM-DD HH:mm:ss.SSS ZZ').trim();
    }

    private getLogger(){
        if(this.writer !== undefined){
        	return this.writer;
    	}

        return winston.createLogger({
            transports: [
                new winston.transports.File({
                    filename: path.join(logger_base.kLogPath, './mam_server.log'),
                    level: logger_base.kLogLevel,
                    maxsize: logger_base.kMaxFileSize,
                    maxFiles: logger_base.kNumMaxFiles,
                    format: winston.format.printf(info => `${this.getTimeStampFormat()} [${info.level.toUpperCase()}] ${info.message}`),
                    tailable: true //INFO: 최신 로그 파일의 이름이 항상 동일하게 유지됨 (직전 로그 파일은 가장 높은 번호의 파일)
                }),
                new winston.transports.Console({
                    level: logger_base.kLogLevel,
                    format: winston.format.printf(info => `${this.getTimeStampFormat()} [${info.level.toUpperCase()}] ${info.message}`)
                }),
            ]
        });
    }

    private createFinalMessage(message: string){
        let stackInfo = this.getStackInfo(1);
        let filenameInfo: string = (logger_base.kUseRelativePath ? stackInfo?.relativePath : stackInfo?.file) as string;
        let finalMessage: string = `[${filenameInfo}:${stackInfo?.line}] ${message}`;
        return finalMessage;
    }

    public info(...args: any[]){
    	this.writer.info(this.createFinalMessage(this.getLogString(args)));
    }

    public warn(...args: any[]){
    	this.writer.warn(this.createFinalMessage(this.getLogString(args)));
    }

    public error(...args: any[]){
    	this.writer.error(this.createFinalMessage(this.getLogString(args)));
    }

    public debug(...args: any[]){
    	this.writer.debug(this.createFinalMessage(this.getLogString(args)));
    }

    private getLogString(args: any[]){
        let resultStr: string = '';
        for(let i=1;i<args.length;i++){
            //INFO: 객체 타입
            if(typeof(args[i]) === 'object'){
                resultStr += JSON.stringify(args[i]) + '\t';
            }
            else {
                resultStr += args[i] + '\t';
            }
        }

    	return args[0] + '\t' + resultStr;
    }

    /**
    * Parses and returns info about the call stack at the given index.
    */
    private getStackInfo (stackIndex: number) {
        // get call stack, and analyze it
        // get all file, method, and line numbers
        let stacklist = (new Error(undefined)).stack?.split('\n').slice(3);

        // stack trace format:
        // http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
        // do not remove the regex expresses to outside of this method (due to a BUG in node.js)
        let stackReg = /at\s+(.*)\s+\((.*):(\d*):(\d*)\)/gi
        let stackReg2 = /at\s+()(.*):(\d*):(\d*)/gi

        let s = stacklist?.[stackIndex] || stacklist?.[0];
        if(s === undefined){
        	throw new Error();
        }
        s = s.toString();
        let sp = stackReg.exec(s) || stackReg2.exec(s)

        if (sp && sp.length === 5) {
        	return {
        		method: sp[1],
        		relativePath: path.relative(logger_base.kProjRootPath, sp[2]),
        		line: sp[3],
        		pos: sp[4],
        		file: path.basename(sp[2]),
        		stack: stacklist?.join('\n')
        	}
        }
	}
}

const loggerBase: logger_base = new logger_base();
export default loggerBase;

 

 
[index.ts]
import logger from './logger';

logger.info('This is info log');
logger.error('This is error log');
logger.debug('This is debug log');
logger.warn('This is warn log');
 
[output]
2020-07-02 18:58:23.579 +0900 [INFO] [index.ts:3] This is info log
2020-07-02 18:58:23.583 +0900 [ERROR] [index.ts:4] This is error log
2020-07-02 18:58:23.584 +0900 [DEBUG] [index.ts:5] This is debug log
2020-07-02 18:58:23.598 +0900 [WARN] [index.ts:6] This is warn log