看著專案逐漸成型,現在該開始重視日誌記錄的規劃了。在實際部署時,我們通常會區分 develop(開發環境)
和 production(正式環境)
兩種不同的執行環境,針對不同環境採用相應的日誌策略。
Log Server
進行集中收集。這樣在後續進行系統分析或發生問題時,才能有效追溯並找到對應的記錄。本日程式碼的範例連結
在前面的開發過程中,我們都是使用 console.log 來輸出調試訊息。
接下來,讓我們進一步優化專案的日誌管理,導入 pino 來建立更專業的日誌系統。
Pino 是一個高效能的 Node.js 日誌函式庫,以快速、低開銷和結構化 JSON 輸出著稱,而 nest-pino
提供與 NestJS 的完美整合,讓我們能在專案中輕鬆建立日誌系統。
pnpm i nestjs-pino pino-http
app.useLogger:將預設的
Nest logger
替換成整合了pino
的自定義 logger
main.ts
/// 略
import { Logger } from 'nestjs-pino';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bodyParser: false,
bufferLogs: true, // 應用程式初始化日誌緩衝保留
});
app.use('/webhook', express.raw({ type: 'application/json' }));
app.use(express.json());
app.useLogger(app.get(Logger)); // 設定使用自定義的 Logger 服务
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
在根模組中加入 LoggerModule.forRoot()
後,Logger 會在整個應用程式中全域生效,這代表所有的 HTTP 請求都會自動記錄日誌,且每個 Controller、Service 等都可以直接注入並使用 Logger,無需在每個模組中重複匯入。
app.module.ts
// 略
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
LoggerModule.forRoot(), // 加入這一行
LineWebhookModule,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// 針對 webhook 路由使用 LINE 中介軟體驗證
consumer.apply(LineMiddleware).forRoutes('webhook');
}
}
透過設定 this.logger.setContext()
的方式,可以讓 NestJS 執行的日誌訊息都加上一個上下文標籤,可以更方便辨識這條日誌是從哪個服務產生。
line-webhook-service.ts
// 略
import { PinoLogger } from 'nestjs-pino';
@Injectable()
export class LineWebhookService {
private readonly lineClient: messagingApi.MessagingApiClient;
constructor(
@Inject(LINE_CONFIG) private readonly lineConfig: ClientConfig,
private readonly logger: PinoLogger,
) {
this.lineClient = new messagingApi.MessagingApiClient({
channelAccessToken: this.lineConfig.channelAccessToken,
});
this.logger.setContext(LineWebhookService.name); // 設定日誌上下文
}
async processWebhook(body: WebhookRequestBody): Promise<string> {
const { events } = body;
this.logger.info({ LineWebhookEvent: events }); // 印出 info level 的日誌訊息
// 略
}
先來看看修改之後的成果:
可以發現在日誌輸出中出現了 上下文(context)
訊息,但這樣的日誌相較於 NestJS 預設的 Logger,缺少了顏色標示,在閱讀上也不夠直觀。因此接下來我們使用 pino-pretty
來為日誌加上顏色,並調整整體的顯示格式,讓開發過程中的日誌更好閱讀一些。
pnpm i pino-pretty
這邊改使用 forRootAsync
的方式,動態從 configService
取得環境變數確認當前是開發環境
還是生產環境
,決定 log level
及是否啟用 pino-pretty
。
主要設定包含:
更多關於 log level 可直接參照 nest-pino 官方說明
調整 app.module.ts
// 略
@Module({
imports: [
// 略
LoggerModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const nodeEnv = configService.get<string>('NODE_ENV');
return {
pinoHttp: {
// 日誌等級,開發階段顯示 trace 等級以上的日誌訊息
level: nodeEnv === 'production' ? 'info' : 'trace',
// 產生一個事務編號,可以在請求鏈路上都攜帶著,便於紀錄
genReqId: () => uuidv4(),
// 自定義想呈現的客製化參數
customProps: (req: Request) => ({
reqId: req.id,
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
userAgent: req.headers['user-agent'],
query: req.query,
params: req.params,
}),
// 自定義 HTTP 請求成功顯示訊息方式
customSuccessMessage: (req: Request, res: Response) =>
`✅ ${req.method} ${req.url} completed with ${res.statusCode}`,
// 自定義 HTTP 請求失敗顯示訊息方式
customErrorMessage: (req, res, err) =>
`❌ ${req.method} ${req.url} failed: ${err?.message}`,
// 開發環境專用 pino-pretty 美化 log 輸出
transport:
nodeEnv !== 'production'
? {
target: 'pino-pretty',
options: {
colorize: true, // 顏色輸出
singleLine: true, // 單行日誌
messageFormat: // 日誌顯示格式
'{if context}【{context}】- {end}{if msg}{msg}{end} {if responseTime}(took {responseTime}ms){end}',
ignore: 'context,pid,hostname,responseTime,req,res', // 忽略屬性
translateTime: 'yyyy-mm-dd HH:MM:ss', //時間格式
levelFirst: true, // 日誌等級在最前面
},
}
: undefined,
},
};
},
}),
],
})
// 略
顯示成果如下:
pinoHttp 的設定與 Step 6 相同,僅將設定轉移至config/logger.config.ts
統一管理日誌相關設定。
config/logger.config.ts:
// 略
// 匯出工廠函數給 app.module.ts 使用
export const getLoggerModuleConfig = (configService: ConfigService): Params => {
const nodeEnv = configService.get<string>('NODE_ENV');
return {
pinoHttp: {
// 略
},
};
};
透過將 Logger
的複雜設定抽離成獨立的工廠函數,可以讓 app.module.ts
保持乾淨。
app.module.ts:
// 略
@Module({
imports: [
// 略
LoggerModule.forRootAsync({
inject: [ConfigService],
useFactory: getLoggerModuleConfig,
}),
],
})
// 略
在 Service
中使用 Pino Logger
時,只需透過建構函式注入 PinoLogger
即可使用各種等級的日誌記錄功能。
// 略
import { PinoLogger } from 'nestjs-pino';
@Injectable()
export class LineWebhookService {
// 略
constructor(
private readonly logger: PinoLogger,
) {
// 可以在這邊設置 log context,設定日誌【】內的內容
this.logger.setContext(LineWebhookService.name);
}
async processWebhook(body: WebhookRequestBody): Promise<string> {
const { events } = body;
// 紀錄 trace msg
this.logger.trace(JSON.stringify(events)); // 服務內使用 log 印出 trace level 訊息
// 略
}
透過整合 nest-pino
與 NestJS
的日誌系統,我們不僅為後續將生產環境的日誌記錄傳送至 Grafana Loki
做好了準備,同時也讓開發環境能夠顯示更清晰、易讀的資訊,提升問題排查的便利性。