2019年6月22日 星期六

在多節點環境將 logback 導出至 CloudWatch

在很久以前有寫了一篇文章 [1] 紀錄要如何用 logback 把 log 導出到 AWS CloudWatch,那時是用 logback.xml 直接設定,不過有個缺點是設定中指定在 logback.xml 裡的 Log Stream 的名字是個寫死的固定值,這在多節點的狀況可能會有問題。因為 CloudWatch 對於任一 Log Stream 同時只能允許一個執行緒存取,這會導致當應用程式會擴展成兩個以上的節點的時候,輸出 log 可能會有問題。

實務上想要解決這個問題,可能有多種解法吧,這邊紀錄的方法也不見得是最好的方法,不過起碼是可運行的選擇。

這裡的概念是~我想要讓 logback 做類似 AWS Lambda 的行為,也就是隨機產生 UUID 作為 Log Stream 的名稱,然後讓應用程式在啟動的時候自動決定。不過實際執行時,在 logback.xml 裡想這麼做總是遇到一些阻礙,可能只是我對於 CI/CD 的程序還不是那麼熟練,所以想不到合適的方法。總之最後我選擇的方式是用程式碼動態生成 Appender 的設定,並且動態加入到 Logger 中。

Maven 設定

在進行前要記得先在 Maven 加入相關的 dependency [2]:

<dependency>
    <groupId>co.wrisk.logback</groupId>
    <artifactId>logback-ext-cloudwatch-appender</artifactId>
    <version>1.0.9</version>
</dependency>

同時要稍微注意,這個函式庫使用的是 AWS SDK v1,預設使用的版本是 1.11.35。

動態加入 Logger

動態加入 Logger 的程式碼內容如下:

protected void initiateCloudWatchLogs() {
  LOGGER.info("Initiating the log output for AWS CloudWatch.");
  String nameOfLogStream = "application-" + UUID.randomUUID().toString();

  LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

  PatternLayoutEncoder patternLayout = new PatternLayoutEncoder();
  patternLayout.setContext(context);
  patternLayout.setPattern("[%thread] %-5level %logger{35} - %msg%n%xThrowable");
  patternLayout.start();

  CloudWatchAppender cloudWatchAppender = new CloudWatchAppender();
  cloudWatchAppender.setContext(context);
  cloudWatchAppender.setRegion("us-west-2");
  cloudWatchAppender.setLogGroup("/my-app/loggroup");
  cloudWatchAppender.setLogStream(nameOfLogStream);
  cloudWatchAppender.setCharset(StandardCharsets.UTF_8);
  cloudWatchAppender.setEncoder(patternLayout);
  cloudWatchAppender.start();

  AsyncAppender asyncAppender = new AsyncAppender();
  asyncAppender.setContext(context);
  asyncAppender.addAppender(cloudWatchAppender);
  asyncAppender.setQueueSize(DEFAULT_LOG_QUEUE_SIZE);
  asyncAppender.setDiscardingThreshold(DEFAULT_LOG_DISCARDING_THRESHOLD);
  asyncAppender.start();

  ch.qos.logback.classic.Logger logbackLogger =
          (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
  logbackLogger.addAppender(asyncAppender);
  LOGGER.info("Log initiation is complete. The log stream is set to '{}'.", nameOfLogStream);
}

上述的 Appender 設定基本上行為跟 [1] 很相似,只是多了在外層又包裝了非同步的 AsyncAppender 而已。最後實際要使用時,需要在某個適當的位置把這段 method 放進去呼叫,最好是在應用程式剛啟動的時候就呼叫它。

不過目前在實驗這個設定方式時,遇到一點奇怪的問題,有些 Logger 的輸出反應好像跟我預期的略為不同。比如說我的 Root Logger 是設定 INFO level,照理說應該所有 WARN 和 ERROR 的錯誤訊息也會輸出到 CloudWatch 才對,但實際執行時好像不會。這部份現在還沒搞清楚是哪邊弄錯了,如果之後有找到原因的話再回來補上。

參考資料
  1. Logback 輸出 Log 到 CloudWatch
  2. WriskHQ/logback-ext

沒有留言: