30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Kubernetes クラスタでホスティングしている Metabase が出力するログを JSON 形式にする

ウェブアプリケーションのモニタリングやオブザーバビリティを実現していくための要素のひとつに、ログがある。
現在ではログファイルなどを直接見ることは稀で、 Datadog などのサービスを使うことが多い。そういったサービスには様々な機能があり、ただログを眺める以上のことができる。

しかしアプリケーション(今回の場合は Metabase)が出力するログが適切に構造化されていないと、 Datadog などのサービスが上手くログを扱うことができず、せっかくの機能を利用できなくなってしまう。
そして Metabase が出力するログはデフォルトでは構造化されていない。
Datadog のドキュメントから引用する。

一般的な Java ログのスタックトレースは複数の行に分割されているため、元のログイベントに関連付けることが困難です。
この問題を解決するには、ログを JSON 形式で生成するようにログライブラリを構成します。JSON にログすると、次のことができます。
Java ログ収集

この記事では、 Kubernetes クラスタで運用している Metabase のログを JSON 形式で出力させる方法について書いていく。

この記事の内容は以下の環境で動作確認している。

  • Kubernetes
    • 1.34
  • Metabase
    • v0.56.13

デフォルトでの挙動

以下の内容のmanifest.yamlを用意して$ kubectl apply -f manifest.yamlすると、 Metabase がデプロイされる。今回は Amazon EKS と Amazon Aurora を使っているが、この記事で扱う内容はそれらの環境に依存したものではないので、環境は何でもよい。

apiVersion: v1
kind: Secret
metadata:
  name: metabase-secret
type: Opaque
stringData:
  MB_DB_TYPE: "mysql"
  MB_DB_DBNAME: "mydb"
  MB_DB_PORT: "3306"
  MB_DB_USER: "admin"
  MB_DB_PASS: "qwerty123"
  MB_DB_HOST: "***.cluster-***.ap-northeast-1.rds.amazonaws.com" # データベースのホストを記述する
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: metabase
spec:
  replicas: 1
  selector:
    matchLabels:
      app: metabase
  template:
    metadata:
      labels:
        app: metabase
    spec:
      containers:
        - name: metabase
          image: docker.io/metabase/metabase:v0.56.13
          ports:
            - containerPort: 3000
          envFrom:
            - secretRef:
                name: metabase-secret
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: metabase
spec:
  type: LoadBalancer
  selector:
    app: metabase
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

$ kubectl logs -f <pod-name>でログを見れるので確認してみると、以下のように表示される。

これを JSON 形式に変えるのが、今回やりたいことである。

Log4j 2 の設定を変える

Metabase は内部でLog4j 2というロギングライブラリを使用しており、その設定ファイルを上書きすることで、出力形式を変えることができる。

デフォルトの設定ファイルは以下で公開されている。
https://github.com/metabase/metabase/blob/master/resources/log4j2.xml

これを書き換えたファイルをコンテナ内に配置し、そのファイルを参照するように Metabase に指示すれば、 JSON 形式で出力されるようになる。

コンテナ内へのファイルの配置

今回は ConfigMap と Volume を使う。

まずmanifest.yamlの末尾に以下を追記し ConfigMap を作成する。

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: log4j2-config
data:
  log4j2.xml: |
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration>
      <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT" follow="true">
          <JsonTemplateLayout eventTemplateUri="classpath:JsonLayout.json"/>
        </Console>

        <!-- This file appender is provided as an example -->
        <!--
        <RollingFile name="FILE" fileName="${logfile.path}/metabase.log" filePattern="${logfile.path}/metabase.log.%i">
          <Policies>
            <SizeBasedTriggeringPolicy size="500 MB"/>
          </Policies>
          <DefaultRolloverStrategy max="2"/>
          <PatternLayout pattern="%d [%t] %-5p%c - %m%n">
            <replace regex=":basic-auth \\[.*\\]" replacement=":basic-auth [redacted]"/>
          </PatternLayout>
        </RollingFile>
        -->
      </Appenders>

      <Loggers>
        <Logger name="com.mchange" level="ERROR"/>
        <Logger name="liquibase" level="INFO"/>
        <Logger name="metabase" level="INFO"/>
        <Logger name="metabase-enterprise" level="INFO"/>
        <Logger name="metabase.metabot" level="DEBUG"/>
        <Logger name="metabase.plugins" level="DEBUG"/>
        <Logger name="metabase.query-processor.async" level="DEBUG"/>
        <Logger name="metabase.server.middleware" level="DEBUG"/>
        <Logger name="org.quartz" level="INFO"/>
        <Logger name="net.snowflake.client.jdbc.SnowflakeConnectString" level="ERROR"/>
        <Logger name="net.snowflake.client.core.SessionUtil" level="FATAL"/>

        <Root level="WARN">
          <AppenderRef ref="STDOUT"/>
        </Root>
      </Loggers>
    </Configuration>

これでlog4j2.xmlというファイルを定義できた。
ファイルの中身だが、先程示した「デフォルトの設定ファイル」をコピーした上で、Console要素の子要素を<JsonTemplateLayout eventTemplateUri="classpath:JsonLayout.json"/>に変えている。
ちなみにJsonLayout.jsonは以下で公開されている。
https://github.com/apache/logging-log4j2/blob/rel/2.25.2/log4j-layout-template-json/src/main/resources/JsonLayout.json

次に Deployment を編集する。volumesvolumeMountsを書き足す。

              limits:
                memory: "1Gi"
                cpu: "500m"
+          volumeMounts:
+            - name: log4j2-config-volume
+              mountPath: /etc/log4j2
+      volumes:
+        - name: log4j2-config-volume
+          configMap:
+            name: log4j2-config
  ---
  apiVersion: v1
  kind: Service

これで、コンテナに/etc/log4j2/log4j2.xmlというファイルが作られるようになる。

Metabase への指示

JAVA_OPTSという環境変数を定義しその値を-Dlog4j.configurationFile=file:/etc/log4j2/log4j2.xmlにすることで、Log4j 2の設定ファイルとして/etc/log4j2/log4j2.xmlを使うように指定できる。

            envFrom:
              - secretRef:
                  name: metabase-secret
+          env:
+            - name: JAVA_OPTS
+              value: "-Dlog4j.configurationFile=file:/etc/log4j2/log4j2.xml"
            resources:
              requests:
                memory: "512Mi"

JSON 形式で出力されることを確認する

この状態で再び$ kubectl apply -f manifest.yamlしてデプロイする。

これで JSON 形式で出力されるようになる。

JSON 形式に変えるだけならこれで終わりだが、ログに色をつけるための情報(\u001B[0mなど)は不要になったので、これを消すための設定も行っておく。
そもそも上述したように Datadog などのサービスでログを見ることが多いはずであり、その場合、この情報はノイズになりやすい。

環境変数MB_COLORIZE_LOGSfalseにすることで消せる。

            env:
              - name: JAVA_OPTS
                value: "-Dlog4j.configurationFile=file:/etc/log4j2/log4j2.xml"
+            - name: MB_COLORIZE_LOGS
+              value: "false"
            resources:
              requests:
                memory: "512Mi"

再度デプロイして確認すると、消えている。

ちなみにターミナルエミュレータなどで JSON 形式のログを見る場合、jqコマンドを使うと綺麗に表示できる。
以下は$ kubectl logs -f <pod-name> | jq -R 'fromjson? // .'した例。

参考資料