くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Fluent BitでCloud Loggingに転送する

前回の続き。システムの一部をCloud RunからVPS化のために、
VPS上のログをGCPのCloud Loggingに送信したい。。

利用するプラグイン

利用するのはこのあたり。

アプリのログファイルをtailで読み込んで、
Cloud Logging(旧Stackdriver)に送信する。

ローカルでの環境

開発・確認用の環境は前回と同じ感じ。

myapp-io.conf[OUTPUT]部分を設定していく。

とりあえず、転送する

まずは、とりあえず、転送するだけの設定。
追加した[OUTPUT]以外は前回と同じ。

# myapp-io.conf
[INPUT]
    name tail
    path ./logs/application.log
    read_from_head   true
    multiline.parser myapp-multiline
    tag myapp.applog

[FILTER]
    name             parser
    match            *.applog
    key_name         log
    parser           myapp-multiline-capture

[OUTPUT]
    name  stdout
    match *
    
[OUTPUT]
    Name        stackdriver
    Match       *

# myapp-parser.conf
[MULTILINE_PARSER]
    name myapp-multiline
    type regex
    flush_timeout 1000
    # rules |   state name  | regex pattern                  | next state
    # ------|---------------|--------------------------------------------
    rule      "start_state"   "/^\[.+\] [a-zA-Z]+ .+/"  "cont"
    rule      "cont"          "/^\s+.*/"                "cont"

[PARSER]
    name myapp-multiline-capture
    format regex
    regex /^\[(?<time>[^ ]+)\] (?<type>[a-zA-Z]+) +(?<message>.*)\n/m
    time_key time
    time_format %Y-%m-%dT%H:%M:%S.%L%z

google_service_credentialsを設定する必要があるけど、
デフォルトは環境変数(GOOGLE_APPLICATION_CREDENTIALS)で設定できる。

GCPのコンソール上でサービスアカウントを作成し、
「ログ書き込み」権限を付与した上で鍵(.json)をダウンロード。

あとは、環境変数にキーファイルのパスを指定すればOK

#!/bin/bash
export GOOGLE_APPLICATION_CREDENTIALS="./credentials.json"
fluent-bit -c myapp.conf

Cloud Logging上で確認すると、ログが転送されていることがわかる。

Cloud Loggingでのログの詳細はこんな感じ。
logNameには、Fluent側のTagが使われるっぽい。

{
  "insertId": "c7yrtzfh5u8wj",
  "jsonPayload": {
    "message": "message info",
    "type": "INFO"
  },
  "resource": {
    "type": "global",
    "labels": {
      "project_id": "myproject"
    }
  },
  "timestamp": "2024-01-17T04:36:17.654Z",
  "logName": "projects/myproject/logs/myapp.applog",
  "receiveTimestamp": "2024-01-17T04:36:18.407382698Z"
}

ログレベルを反映する

なにも指定していないので、ログレベル(severity)がない状態。
severity_keyを使えばOK。

FILTERのPARSERで?<type>と名前をつけているので、

[PARSER]
    name myapp-multiline-capture
    regex /^\[(?<time>[^ ]+)\] (?<type>[a-zA-Z]+) +(?<message>.*)\n/m
    # ...略

OUTPUTでそのキー名を指定するかたち。

[OUTPUT]
    Name        stackdriver
    Match       *
    severity_key type

設定した状態だとこんな感じ。

{
  "insertId": "u7gxdeb6i2f",
  "jsonPayload": {
    "message": "message info"
  },
  "resource": {
    "type": "global",
    "labels": {
      "project_id": "myproject"
    }
  },
  "timestamp": "2024-01-17T06:54:36.083Z",
  "severity": "INFO",
  "logName": "projects/myproject/logs/myapp.applog",
  "receiveTimestamp": "2024-01-17T06:54:37.155753816Z"
}

絞り込むための情報を追加する

Cloud Functionsでのログを見ると、こんな感じになっていて、
関数名などでも絞り込みできるようになっている。

{
  "textPayload": "message cloud function log...",
  "insertId": "65a77280000b92741134b1ba",
  "resource": {
    "type": "cloud_function",
    "labels": {
      "function_name": "my_foo_function",
      "project_id": "myproject",
      "region": "asia-northeast1"
    }
  },
  "timestamp": "2024-01-17T06:24:00.758388Z",
  "severity": "INFO",
  "labels": {
    "runtime_version": "nodejs20_20231231_20_10_0_RC00",
    "execution_id": "hv2mzdad42fl",
    "instance_id": "0087599d42325f0791879e25b0b66159bb57157855155a263ba635c671e1f8d42a60150e295dff896773521f08fa3a955ae28311f8733a1da8dea95f7bf859472008d2"
  },
  "logName": "projects/myproject/logs/cloudfunctions.googleapis.com%2Fcloud-functions",
  "trace": "projects/myproject/traces/7abc1b0a9c4923b83d12c081cfa31d12",
  "receiveTimestamp": "2024-01-17T06:24:01.085097754Z"
}

それぞれなにを設定すればよいかなどは、このあたりを参照。

resourceはログエクスプローラで絞り込みするときに使うやつ。

設定の例

設定はこんな感じ。

[OUTPUT]
    Name        stackdriver
    Match       *
    severity_key type
    
    ### resource関連の設定
    # resourceのtype
    resource generic_node
    # location/region
    location asia-northeast1
    # 実行環境やclaster名などの識別子
    namespace my_foo_func
    # namespace内ノードのID。IPやhost名など
    node_id my_vpn_host
    
    ### labelsの設定
    # key=valueをカンマ区切りで指定
    labels machine_name=my_vpn_host,app_version=1.0.0

設定した状態で実行するとこんな感じのログになる。

{
  "insertId": "6xuk1uf767oyw",
  "jsonPayload": {
    "message": "message info"
  },
  "resource": {
    "type": "generic_node",
    "labels": {
      "project_id": "myproject",
      "location": "asia-northeast1",
      "namespace": "my_foo_func",
      "node_id": "my_vpn_host"
    }
  },
  "timestamp": "2024-01-17T07:20:58.129Z",
  "severity": "INFO",
  "labels": {
    "machine_name": "my_vpn_host",
    "app_version": "1.0.0"
  },
  "logName": "projects/myproject/logs/myapp.applog",
  "receiveTimestamp": "2024-01-17T07:20:58.890547211Z"
}

コンソールにも「汎用ノード」が増え、フィルタリングできる。
(未指定だったのは「グローバル」)

resource.labelsについて

resource.labelsで指定できるものは、
resource.typeごとに決まっているので、
それ以外はlabelsに追加するといい感じ。

resource.typeは、k8sやgceなど指定できるけど、
globalgeneric_nodegeneric_taskを使いそう。

それぞれの詳細は、以下に書かれている。

ラベルでの変数の利用

各ラベルの値は、
severity(type)のようにログ自体の値や、
環境変数の値を埋め組むことができる。

Fluent側のログがこんな感じの場合。

{
  "keyA": "valA",
  "toplevel": {
    "keyB": "valB"
  }
}

こんな感じで$<キー名>を指定すると、
ログ内のデータを使ってくれる。

[OUTPUT]
    name   stackdriver
    match  *
    labels keyC=$keyA,keyD=$toplevel['keyB'],keyE=valC

細かい文法はこのあたり。

なので、アプリのバージョンなどで絞り込みたい場合には、
ログに含めておいて、labelsにわたす感じだとよい感じ。


以上!! やっとやりたいことができた気がするぞ。。(*´ω`*)