在Nest.Js使用log4js記錄並分流log

前言

Apache 提供的開源log框架 log4xx 系列,想必是滿容易見到的
而log4js則是Node.Js版本的log框架

npm上現成的套件@quickts/nestjs-log4js

若只是單純把所有log寫檔,並14天自動清除的話
推薦直接使用@quickts/nestjs-log4js
此套件引入後,預設就有已經寫好的基本設定了
只需要簡單使用的話,單純引入後就可以使用了

若想看預設的設定,可以再此處查看

隨著系統的架構越擴越大,功能越來越複雜,log就會面臨特殊業務因較複雜,要單獨寫入一個log檔便於追查問題、各登入user有自己的獨立log檔
app.module傳入自己的設定,覆蓋掉套件的預設設定檔就可以了

log4js 基本核心概念說明

log4js分下列3個核心類別
其層級關係為:category > appender > layout
log4js instance可以有多個category;一個category可以有多個appender;一個appender可以有多個layout

  1. category(存儲器):log主項目,log4js.getLogger()直接取得的項目
  2. appender(存哪裡):log的輸出方式,如console、檔案、smtp、tcp…等
  3. layout(如何存):log檔案存放路徑、命名、保留天數…等

較完整使用方式可以參照參考資料的連結

範例

若要覆寫@quickts/nestjs-log4js的預設設定,而他預設設定本身就是符合自己需求的話
要記得拷貝他的設定檔,再額外增加自己的部份
原始碼可以知道他是以傳入的為主,不會跟預設的範例組合在一起!

針對上述提到的二個情境,應該也是大家較容易碰到的,提供範例程式,再自行修改

特殊業務因較複雜,要單獨寫入一個log檔便於追查問題

某些業務有其複雜性,混在all裡面追查造成追查困難的話,將其操作獨立成一份檔案,是最方便的作法了!
或著訂單因有其重要性,每一筆的增減除了all裡面,也要單獨拉一檔案單純記錄「是誰操作了增減」
便於快速查看,而不必大海撈針

   Log4jsModule.forRoot({
      appenders: {
          // ... 省略由預設範例拷貝過來的部份 ...
+         ordersLogger: {
+             type: 'dateFile',
+             filename: join(basePath, 'orders/orders'),
+             alwaysIncludePattern: true,
+             pattern: 'yyyy-MM-dd.log',
+             daysToKeep: 14
          },
      },
      categories: {
          default: {
+              appenders: ['consoleLogger', 'appLogger', 'errorLogger', 'ordersLogger'],
              level: 'all'
          },
+         orders: {
+           appenders: ['ordersLogger'],
+           level: 'all'
+         },
      }
  }),

如果希望有個all來記錄所有log資訊的話,記得在default裡也加入ordersLogger
若沒有的話,則不必

在需要將內容單獨記錄在log檔裡的,第2個參數傳入orders就可以了

this.logger.log('要寫入的訊息', 'orders');

若整個service所有log都要寫進獨立檔案,不想要每行都傳參數
則在最上面new Logger('orders')直接入參
services內使用時就不需要逐一傳參了!

+import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class OrdersService {
+  private readonly logger = new Logger('orders');

   test() {
+       this.logger.log('測試寫入log');
   }
 }

各登入user有自己的獨立log檔

若是有權限控管的系統,各USER有自己的LOG檔,就相當重要了
使用log4js本身提供的multiFile參數

   Log4jsModule.forRoot({
      appenders: {
          // ... 省略由預設範例拷貝過來的部份 ...
+          multi: { 
+          type: 'multiFile',
+          base: 'logs/users',
+          property: 'username', // 重點,會由此參數決定LOG是否要寫到multiFile裡
+          alwaysIncludePattern: true,
+          pattern: 'yyyy-MM-dd.log',
+          daysToKeep: 14,
+          extension: ''
          }
      },
      categories: {
          default: {
+             appenders: ['consoleLogger', 'appLogger', 'errorLogger', 'multi'],
              level: 'all'
          },
      }
  }),

這種屬於全面性的LOG,建議使用Interceptor來寫入

@Injectable()
export class PathLoggerInterceptor implements NestInterceptor {
  
  private logger = new Logger('PathLoggerInterceptor');

  constructor(private readonly log4jsService: Log4jsService) { }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp().getRequest();
    const url = ctx.url;
    const method = ctx.method;
    const user = ctx.user as UsersEntity;
    
    let functionInfo = '';
    if (user) {
      functionInfo = `[${context.getClass().name} -> ${context.getHandler().name}()] [${user.username} -> ${user.name}]`; // [ordersController -> addOrder()] [admin -> 管理者]
      this.log4jsService.getLogger(functionInfo).addContext('username', user.username); // log4js增加username
      // this.log4jsService.getLogger('RolesGuard').addContext('username', user.username); // 若是已知固定名字要單獨分流多檔,則直接傳入字串
    } else {
      // 在開發時期可能會省略登入行為,避免因無user而噴exception。故多這一行便於開發期使用
      functionInfo = `[${context.getClass().name} -> ${context.getHandler().name}()] [無法取得使用者]`; 
    }

    const userIp = ctx.headers['x-forwarded-for'] || ctx.connection.remoteAddress;
    const connectionInfo = `client IP: ${userIp} [${method}] -> ${url}`;
    const requestInfo = `Request: params=${ctx.params}, body=${ctx.body}`;

    // 因第2參數入參`functionInfo`,而log4js設定檔中,並未定義這麼多名字,所以全部都會回到default來寫log
    // 在default的 appender 中有增加`multi`,故log4js會自動判斷若有username,則依username,寫入各帳號的獨立log檔。若無,就單純寫到all裡
    this.logger.log(`${connectionInfo} ${requestInfo} `, functionInfo);

    return next.handle();
  }
}

完整設定資料

main.ts裡,將log預設使用log4js

+ import   {   Log4jsService   }   from   '@quickts/nestjs-log4js';

(async () => {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {cors: true});
+  const logger = app.get(Log4jsService);
+  app.useLogger(logger);
   
   // 以下省略…
   
   await app.listen(3000);
   logger.log(`Nest Server is listening on http://localhost:3000`, 'bootstrap');
})().catch(err => console.error(err));
 Log4jsModule.forRoot({
    appenders: {
        logToErrorFile: {
            type: 'dateFile',
            filename: join(basePath, 'err/err'),
            alwaysIncludePattern: true,
            pattern: 'yyyy-MM-dd.log',
            daysToKeep: 14
        },
        errorLogger: {
            type: 'logLevelFilter',
            appender: 'logToErrorFile',
            level: 'error'
        },
        appLogger: {
            type: 'dateFile',
            filename: join(basePath, 'all/all'),
            alwaysIncludePattern: true,
            pattern: 'yyyy-MM-dd.log',
            daysToKeep: 14
        },
        consoleLogger: {
            type: 'console',
            layout: {
                type: 'colored'
            }
        },
        // 以上是直接由預設範例拷貝過來
        ordersLogger: {
            type: 'dateFile',
            filename: join(basePath, 'orders/orders'),
            alwaysIncludePattern: true,
            pattern: 'yyyy-MM-dd.log',
            daysToKeep: 14
        },
        multi: { 
          type: 'multiFile',
          base: 'logs/users',
          property: 'username',
          alwaysIncludePattern: true,
          pattern: 'yyyy-MM-dd.log',
          daysToKeep: 14,
          extension: ''
        }
    },
    categories: {
        default: {
            appenders: ['consoleLogger', 'appLogger', 'errorLogger', 'multi'],
            level: 'all'
        },
        orders: {
          appenders: ['ordersLogger'],
          level: 'all'
        }
    }
}),

參考資料

Node.js 日誌系統 log4js 介紹
npm / log4js