在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裡面,也要單獨拉一檔案單純記錄「是誰操作了增減」
便於快速查看,而不必大海撈針

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   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就可以了

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

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

1
2
3
4
5
6
7
8
9
10
+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參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   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來寫入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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

1
2
3
4
5
6
7
8
9
10
11
12
+ 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));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 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