日記マン

動画広告プロダクトしてます。Go, Kubernetesが好きです。

Lokiとpromtailことはじめ

Grafana チームから、ログ収集ソリューションとして Loki がOSSとして公開された。
GAに向けてベータリリースであるがすでに GitHub Star も 7000 に届く勢い。
Prometheus のようなラベリングが可能で、軽量なログソリューションと謳ってる。

https://github.com/grafana/loki

Kubernetes でログ周りどうしようかなーとツールを探していたころに、
Loki を知ったので色々触ってみた。
そして、Lokiの構成要素である promtail について調べてみた。

Loki 概要

| log | <-- | promtail | --> | loki | <-- | Grafana (Explore) | <-- | User |

Loki は ログを収集するエージェントの promtail と、そのログを受け取り保存・クエリエンジンを搭載する Loki で構成される。
Like, Prometheus, but for logs. のキャッチコピー通り、Prometheus のラベリングなど似た機能を持つ。
また、Grafana (>=6.0) にネイティブサポートされており、
Grafanaのダッシュボードで、Prometheus のメトリクスを眺め、Lokiでログを分析し、
問題となるコードを発見・修正する、といった一連の動きを親和性のあるこれらのツールでインテグレーションし、
コンテキストスイッチを減らすことが期待できるとしている。

f:id:i101330:20190818141913p:plain
loki-slide

https://speakerdeck.com/grafana/grafana-loki-like-prometheus-but-for-logs

promtail

promtail エージェントは、 Applicationが出力するログをtailし、loki サーバに送信する役割を持つ。

Kubernetes の場合、Podの標準出力/エラー出力はノード上の /var/log/pods/ に書き込まれる。
promtail を DaemonSet リソースとして立ち上げ、PodのログをtailしLokiに送信させる。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: promtail
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: promtail
  template:
    metadata:
      labels:
        app: promtail
    spec:
      dnsPolicy: ClusterFirstWithHostNet
      hostNetwork: true
      hostPID: true
      serviceAccountName: sa-promtail
      containers:
      - name: promtail
        image: grafana/promtail:latest
        args:
        - --config.file=/etc/promtail/config.yaml
        ports:
        - containerPort: 9080
          name: http
        volumeMounts:
        - name: config-volume
          mountPath: /etc/promtail
        - name: varlog
          mountPath: /var/log
        - name: secret-volume
          mountPath: /var/run/secrets
        - name: run
          mountPath: /run/promtail
      volumes:
      - name: config-volume
        configMap:
          name: cm-promtail-config
      - name: varlog
        hostPath:
          path: /var/log
      - name: secret-volume
        hostPath:
          path: /var/run/secrets
      - name: run
        hostPath:
          path: /run/promtail

もちろんノード上の /var/log/ パスをマウントしておく。

Scrape

scrape_configs:
- job_name: kubernetes-pods-app
- job_name: kubernetes-systems

Service Discovery (inspired by Prometheus)

どのログファイルをtailする対象(Targets)にするかを決めるために、
Prometheus でおなじみの Service Discovery を promtail でも利用できる。
https://github.com/prometheus/prometheus/blob/master/discovery/config/config.go

scrape_configs:
- job_name: kubernetes-pods-app
  kubernetes_sd_configs:
  - role: pod

kubernetes_sd_configs は指定した role に応じて Kubernetes API Server からPodの情報を取得する。
Kubernetes の Controller デーモンたちと似た振る舞いをしており、
API Server からリソースイベントをウォッチしイベントを検知している。
例えば role: pod の場合は、
Pod リソースのイベントを検知する。新たなPodが作成されたり、更新されたり、削除されたりすると
TargetGroup を構築し、ServiceDiscovery の Manager が channel を利用して受信する。
https://github.com/prometheus/prometheus/blob/master/discovery/kubernetes/pod.go

| kube-apiserver | <-(watch)- | Pod Discovery | -(create)-> | TargetGroup | <-recieve- | Manager |

TargetManager

scrape_configs に記述した job ごとに TargetSyncer を用意する。

| discovery.Manager | <-sync- | TargetSyncer |

各Target に対し、 relabel Process を経て、keep された Target のみ、 tailer ワーカーを実行する。

| relabel | -(filter)-> | target | -(run)-> | tailer |

tailer はその名の通り、ファイルを tail し、新たにログエントリ(行)を検知した場合に、
EntryHandler にログエントリを渡す。

| client | -implement-> | EntryHandler | <-- | tailer|

client はログエントリをハンドルする具象オブジェクトである。
送られたログエントリを毎回一つずつLokiサーバへPushするのではなく、バッチにまとめて送信している。

| Loki | <-batch Push- | client |

Relabeling

こちらも Prometheus の Relabeling がそのまま実装されている。

scrape_configs:
- job_name: kubernetes-pods-app
  kubernetes_sd_configs:
  - role: pod
  relabel_configs:
  # Pod の Labels に `name` キーがある場合、除外する
  # kube-system 系が該当する
  - action: drop
    regex: .+
    source_labels: [__meta_kubernetes_pod_label_name]
  # Pod の Labels の `app` キーに値がセットされていない場合、除外する
  - action: drop
    regex: ^$
    source_labels: [__meta_kubernetes_pod_label_app]
  # Pod の Labels の `app` キーが値である __service__ という label を作成する
  - target_label: __service__
    source_labels: [__meta_kubernetes_pod_label_app]
  # __host__ は現在ドキュメントに記載がないが、設定した値と同一のノードのみを対象にする
  - target_label: __host__
    source_labels: [__meta_kubernetes_pod_node_name]
  # namespace と __service__ を / で繋げた値をラベル名 job として利用する
  - action: replace
    separator: /
    replacement: $1
    target_label: job
    source_labels: [__meta_kubernetes_namespace, __service__]
  # ポッド名 を可視ラベルに
  - action: replace
    target_label: pod_name
    source_labels: [__meta_kubernetes_pod_name]
  # コンテナ名 を可視ラベルに
  - action: replace
    target_label: container_name
    source_labels: [__meta_kubernetes_pod_container_name]
  # Pod の Labels のキーバリューを label にする
  - action: labelmap
    regex: __meta_kubernetes_pod_label_(.+)
  # 実際にtailするログのPathを __path__ にセットする
  - target_label: __path__
    separator: /
    replacement: /var/log/pods/*$1/*.log
    source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]

重要な点をあげると、最後の __path__ で実際にtailするログの場所を指定する必要がある。
また、DaemonSet で promtail を立ち上げた場合、
ServiceDiscovery で取得できるPodはクラスタにある全てのPodである事に対し、
DaemonSetであるpromtail がマウントできるログは自身のノード上のみであることから、
ノードが一致するPodのみを対象にするために __host__ を設定する。

https://github.com/grafana/loki/blob/v0.3.0/pkg/promtail/targets/filetargetmanager.go#L226

Pipeline Stages

Pipeline Stages は実際にログを取得したあと、 Loki に送信するまでに加工するステージ。
出力を変換したり、ログデータからラベリングすることができる。

scrape_configs:
- job_name: kubernetes-pods-app
  kubernetes_sd_configs:
  - role: pod
  relabel_configs: # 省略
  pipeline_stages:
  - docker:
  - match:
      selector: '{app=~"payment-.+", namespace="payment"}'
      stages:
      - json:
          expressions:
            level: level
            error: error
      - labels:
          level:
          error:

docker コンテナのログ用ステージの docker が用意されているのでまず使う。
stdout/stderr の区別が可能な stream ラベルなどを作成してくれたりする。
match を利用したステージでは、ログに付与した label でフィルタできる selector
そしてログデータの加工やラベリングができる stages などを記述できる。
この例ではアプリケーションはJSONフォーマットでログを吐いており、
フィールド level, error をそれぞれ同名でラベリングしている。