部署Nest.Js至GAE(三):搭配 winston 將log寫在GCP

前言

由於GAE(Google App Engine)無法將檔案寫在本地端
需要搭配GCP另個logging 服務,記錄log
預設GCP只有針對HTTP連入/連出寫log,如果有批次作業、自行額外記錄的log,都不會寫入

從GAE頁面 > 服務 > 診斷下方的工具點開 > 記錄檔 進入
他有另個名字叫stack driver

但在GCP網頁上並沒標名清楚,而GCP官方文件也寫得不清不楚
再找相關套件時真的會一頭霧水,不知道能不能使用

GCP官方文件在Node.Js推薦使用winstonBunyan

我當初開發時是直接使用熟悉的log4js
由npm 上可以搜尋到log4js-stackdriver-appender
不過我當初並不確定stackdriver 就是 上述路徑的記錄檔
而此套件3年前發佈後就再也沒更新

所以我是照官方的改用winston寫log,GCP有提供@google-cloud/logging-winston支援

改寫winston成功後看其傳入參數,也如同log4js套件所要求的。故推測log4js-stackdriver-appender應也能順利連接

由於改winston成功了,就沒再花心力回去測試log4js了

使用nest-winston界接

nest-winston使用方式可以參考官方教學:npm / nest-winston

由於我期望Nest.Js初始化的所有資訊也寫入GCP,以避免有執行錯誤時未記錄到
故我是將logger 寫在main.ts

一、安裝所有相關套件

npm install --save nest-winston winston @google-cloud/logging-winston

二、增加winston.config.ts參數檔

避免main看起來太過凌亂,我有增一winston.config.ts專門放置參數檔
由於平常開發時期不會希望log都寫上GCP,畢竟超過太多量的話,是會被收費的…
故我有增加邏輯,平常開發時不會寫入,有需要測試到這段時,再放開註解就好

  1. 要記得將程式碼裡的「專案id」改成您的專案id
  2. logging.json,為GCP上IAM的權限,建立一新的角色增加權限下載下來就可以直接使用了
  3. 在GAE上不必特別設定,填好專案ID後,GCP上可以直接連通
import { LoggingWinston } from '@google-cloud/logging-winston';
import winston from 'winston';
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';

export default () => {
    const transports: any[] = [new winston.transports.Console()];
    if (process.env.NODE_ENV === 'production') {
        transports.push(new LoggingWinston({ projectId: '專案id', labels: { name: '專案id', version: '1.0.0' } }));
    } else {
        const logPath = `${process.cwd()}/logs`;
        // 只有 error 等級的錯誤 , 才會將訊息寫到 error.log 檔案中
        transports.push(new winston.transports.File({ filename: `${logPath}/error.log`, level: 'error' }));
        // info or 以上的等級的訊息 , 將訊息寫入 combined.log 檔案中
        transports.push(new winston.transports.File({ filename: `${logPath}/info.log`, tailable: true }));
        // 測試寫到GCP時才需要
        // process.env.GOOGLE_APPLICATION_CREDENTIALS = `${process.cwd()}/environment/logging.json`;
        // transports.push(new LoggingWinston({ projectId: '專案id' }));
    }
    return {
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.ms(),
          winston.format.prettyPrint(),
          nestWinstonModuleUtilities.format.nestLike(),
        ),
        transports,
    }
}

三、調整main.ts

const app = await NestFactory.create<NestExpressApplication>(AppModule, {
  logger: WinstonModule.createLogger(winstonConfig())
});

四、使用方式

不必修改太過原先程式碼,直接用Nest.Js官方教學的的logger寫法就好
如下範例

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './products.entity';

@Injectable()
export class ProductsService {

    logger = new Logger('ProductsService');
    
    constructor(@InjectRepository(Product)
        private readonly productRepository: Repository<Product>) {}
    
    async getMany(): Promise<Product[]> {
        const products = await this.productRepository.find();
        this.logger.log(poroducts);
        return products;
    }
}

注意事項

winston是使用基本的stdout、stderr產生log
this.logger.debugthis.logger.verbose不會顯示在console上,亦不會寫入到GCP

查了許久都說是增加VSCode的setting.json,但我加了仍是不會顯示= =
須避開使用這2個,否則開發期用logger來判斷程式執行狀況的話,會以為程式都沒有成功執行

參考資料

官方文件 / @google-cloud/logging-winston

系列文章

部署Nest.Js至GAE系列文章