在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
category
(存儲器):log主項目,log4js.getLogger()
直接取得的項目appender
(存哪裡):log的輸出方式,如console、檔案、smtp、tcp…等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'
}
}
}),