iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
Modern Web

Line Bot × NestJS:30 天開發日記系列 第 5

Day 5:整合 Pino Logger 提升 LINE Bot 專案的可維護性

  • 分享至 

  • xImage
  •  

2025 鐵人賽背景圖

前言

看著專案逐漸成型,現在該開始重視日誌記錄的規劃了。在實際部署時,我們通常會區分 develop(開發環境)production(正式環境)兩種不同的執行環境,針對不同環境採用相應的日誌策略。

  • 開發環境:希望日誌輸出能提供詳盡且易讀的資訊,方便開發過程中的除錯和問題排查
  • 生產環境:需要格式化完整的日誌訊息,並將其傳送至專用的 Log Server 進行集中收集。這樣在後續進行系統分析或發生問題時,才能有效追溯並找到對應的記錄。

本日程式碼的範例連結


整合 Nest Logger 與 Pino 優化日誌系統

在前面的開發過程中,我們都是使用 console.log 來輸出調試訊息。

接下來,讓我們進一步優化專案的日誌管理,導入 pino 來建立更專業的日誌系統。

Pino 是什麼?

Pino 是一個高效能的 Node.js 日誌函式庫,以快速、低開銷和結構化 JSON 輸出著稱,而 nest-pino 提供與 NestJS 的完美整合,讓我們能在專案中輕鬆建立日誌系統。

Step 1:安裝 nest-pino 套件

pnpm i nestjs-pino pino-http

Step 2:main.ts 中導入 Pino 日誌設定

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();

Step 3:在根模組導入 nest-pino 日誌模組

在根模組中加入 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');
  }
}

Step 4:注入 line-webhook-service.ts 使用看看

透過設定 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 的日誌訊息
    // 略
  }

先來看看修改之後的成果:
nest-pino 日誌紀錄

可以發現在日誌輸出中出現了 上下文(context) 訊息,但這樣的日誌相較於 NestJS 預設的 Logger,缺少了顏色標示,在閱讀上也不夠直觀。因此接下來我們使用 pino-pretty 來為日誌加上顏色,並調整整體的顯示格式,讓開發過程中的日誌更好閱讀一些。

Step 5:安裝 pino-pretty 套件

pnpm i pino-pretty

Step 6:調整日誌成功與失敗顯示自定義欄位格式

這邊改使用 forRootAsync 的方式,動態從 configService 取得環境變數確認當前是開發環境還是生產環境,決定 log level 及是否啟用 pino-pretty

主要設定包含:

  • 環境區分:根據 NODE_ENV 自動調整日誌等級(開發環境顯示 trace 以上,生產環境僅顯示 info 以上)
  • 請求追蹤:使用 uuidv4() 為每個請求產生唯一識別碼,便於追蹤整個請求鏈路
  • 客製化資訊收集:自動記錄 IP 位址、User-Agent、查詢參數等重要請求資訊
  • 成功/失敗訊息客製化:使用表情符號(✅❌)清楚標示請求結果,包含狀態碼和錯誤訊息
  • 開發環境美化:透過 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,
          },
        };
      },
    }),
  ],
})
// 略

顯示成果如下:
Nest-pino pino-pretty 效果

Step 7:將 logger config 放置獨立檔案管理並注入根模組使用

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: {
        // 略
    },
  };
};

Step 8:將 LoggerModule 透過工廠函數注入根模組使用

透過將 Logger 的複雜設定抽離成獨立的工廠函數,可以讓 app.module.ts 保持乾淨。

app.module.ts:

// 略
@Module({
  imports: [
    // 略
    LoggerModule.forRootAsync({
      inject: [ConfigService],
      useFactory: getLoggerModuleConfig,
    }),
  ],
})
// 略

Nest Service 如何使用 log 印出訊息

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-pinoNestJS 的日誌系統,我們不僅為後續將生產環境的日誌記錄傳送至 Grafana Loki 做好了準備,同時也讓開發環境能夠顯示更清晰、易讀的資訊,提升問題排查的便利性。


上一篇
Day 4:掌握 LINE Bot 七種訊息類型與 NestJS 架構優化
下一篇
Day 6:整合第三方 API 天氣查詢服務
系列文
Line Bot × NestJS:30 天開發日記7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言