谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

GrafanaスタックによるSpring Bootアプリケーション監視の詳細(その2 Grafana + Loki編)

第2回目はGrafana + Lokiです。Lokiと言えば、時間移動して悪さをしたせいでタイムパトロールに捕まるという 大長編ドラえもんの悪役みたいな スピンオフドラマシリーズがありましたが、皆さんご覧になったでしょうか。あ、ディズニープラス限定配信のドラマなんてほとんどの人が観ないか、そっか。なかなかの野心作で面白かったですよ。展開はイマイチでしたけど。

そんなわけで今回は、ローカル環境(+ Docker)でGrafanaとLokiを使ってログの可視化を行います。

ソースコードなどはすべてGitHubに置いています。

https://github.com/cero-t/spring-store-2022

ログ収集の構造

ログ収集と可視化の構造は次のようになります。

ログ収集の構造

ログをファイルに出力し、それをログ収集エージェントが読み取ってログストレージに送ると、いう流れがログ収集の構造としては一般的です。ログ収集エージェントは、ここではPromtailを選んでいますが、FluentdやFilebeat、Logstashなどがよく使われています。

ただコンテナ環境においては、ログを標準出力に出力すれば、それをログエージェントが読み取ってログストレージに転送するような仕組みが一般的であるため「ログファイル」というものをあまり考えなくなります。

コンテナ環境でのログ収集の構造

ずっとコンテナ環境を使っているとむしろ「ログファイルなどという前時代的なものに縛られるとは人類はいつまで経っても愚かなものよ」などと高尚な神の視点で物事を考えるようになってしまいがちになり、ログをファイル出力したくなくなってきます。

そこで今回は(参考にしたGrzejszczakさんのサンプルがそうであったように)Loki4Jの loki-logback-appender を利用することにしました。

loki-logback-appenderを使ったログ収集の構造

loki-logback-appenderは、ログを直接Lokiに転送するというものです。微妙に賛否ありそうな構造ですが、ひとまずは「Lokiにログを集めたい」という事が主目的なので、手段は簡易なものを選ぶことにしました。

ちなみにspring-store-2022では、k8s上ではPromtailを使うようにしているので、その辺りは別途ブログを書きます。

Lokiでログを収集する

それではLokiを使ってログを収集する手順を説明します。

1. Spring Bootアプリケーションからログを送る

まずはSpring BootアプリケーションからLokiにログを転送できるようにします。

Spring Bootアプリケーションにloki-logback-appenderを追加

Spring Bootアプリケーションのdependencyloki-logback-appender を追加します。

<dependency>
    <groupId>com.github.loki4j</groupId>
    <artifactId>loki-logback-appender</artifactId>
    <scope>runtime</scope>
</dependency>

(View in GitHub)

ただし loki-logback-appenderspring-boot-dependencies に含まれていないため、バージョンを自分で指定する必要があります。spring-store-2022では親のpom.xmlで指定するようにしています。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.github.loki4j</groupId>
            <artifactId>loki-logback-appender</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
</dependencyManagement>

(View in GitHub)

logback.xmlを作る

続いて、このloki-logback-appenderを使うようLogbackの設定を書きます。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml" />
    <springProperty scope="context" name="appName" source="spring.application.name"/>

    <appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
        <http>
            <url>http://localhost:3100/loki/api/v1/push</url>
        </http>
        <format>
            <label>
                <pattern>app=${appName},host=${HOSTNAME},traceID=%X{traceId:-NONE},level=%level</pattern>
            </label>
            <message>
                <pattern>${FILE_LOG_PATTERN}</pattern>
            </message>
            <sortByTime>true</sortByTime>
        </format>
    </appender>

    <root level="INFO">
        <appender-ref ref="LOKI"/>
    </root>

(View in GitHub)

まず urlhttp://localhost:3100/loki/api/v1/push を指定して、送り先のLokiのアドレスを記載します。

またタグとして app host traceId level を追加しています。このタグを使ってLoki上で検索することができるようになります。

ちなみにこのファイルを logback-spring.xml という名前にしておけば無設定で読み込まれるのですが、なんとなくこれを標準にすることに抵抗があったため、logback-loki.xml という名前にしたうえで、application.properties でファイル名を指定ししました。

logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
logging.config=classpath:logback-loki.xml

(View in GitHub)

なんとなく、Appenderで送ることを標準として良いのかどうなのか、まだ悩んでるんですよね。まぁその辺りの悩みはブログの最後に書きます。

また logging.pattern.level でログレベルを出す箇所に、ログレベルの後ろにアプリケーション名、トレースID、スパンIDを出しています。タグがなく、ただログを見ただけでもトレースIDなどを確認できるようにするためです。

これでSpring Bootアプリケーション側の設定は完了です。

2. Lokiを構築する

続いて、Lokiの構築を行います。Lokiはdocker-composeを使って起動します。

docker-compose.ymlのうち、Lokiの起動に関する部分のみピックアップします。

services:
  loki:
    image: grafana/loki
    extra_hosts: ['host.docker.internal:host-gateway']
    command: [ "-config.file=/etc/loki/local-config.yaml" ]
    ports:
      - "3100:3100"                                   # loki needs to be exposed so it receives logs
    environment:
      - JAEGER_AGENT_HOST=tempo
      - JAEGER_ENDPOINT=http://tempo:14268/api/traces # send traces to Tempo
      - JAEGER_SAMPLER_TYPE=const
      - JAEGER_SAMPLER_PARAM=1

(View in GitHub)

environment でLoki自身のトレース情報をJaeger(実体はTempo)に送るような設定が入っているのですが、これは別になくても構いません。元のサンプルに入っていたので、そのままにしています。

また ports3100 番ポートを外部公開していますが、アプリケーションからAppenderを使ってLokiにログを転送するという構造上、LokiのポートはDockerの外に公開する必要があるのです。

設定はこれくらいで、特に追加の設定ファイルなどは使わずにデフォルト設定で動かしています。

3. GrafanaからLokiにアクセス

最後に、GrafanaでLokiに収集したログを表示できるようにするために、GrafanaのデータソースにLokiを追加します。これはGrafanaの datasources.yml で設定します(Grafanaの設定画面からも追加できます)

datasources:
- name: Loki
    type: loki
    uid: loki
    access: proxy
    orgId: 1
    url: http://loki:3100
    basicAuth: false
    isDefault: false
    version: 1
    editable: false
    apiVersion: 1
    jsonData:
      derivedFields:
        - datasourceUid: tempo
          matcherRegex: \[.+?,(.+?),
          name: TraceID
          url: $${__value.raw}

(View in GitHub)

データソースとしてLokiを指定し、url にLokiのアドレスを指定します。

jsonData.derivedFields に少し設定が入っているのですが、これを設定することで、GrafanaでLokiのログに含まれるTraceIDを使ってTempo(トレース情報)のUIを開くことができるようになります。ログを正規表現\[.+?,(.+?), でマッチングさせて TraceID を抽出し、その値を使ってTempoを開くという設定になっています。

以前のエントリー でLokiからTempoに移動できることを紹介した時の画像を再掲します。

Grafana上でLokiのログに表示されるTempoボタン

Tempoボタンを押すとTempoでトレースが表示される

これを実現するための設定になります(Tempoの詳細については次のブログエントリーで紹介します)

画像内に「Detected fields」という項目がありますが、ここにGrafana自身が自動検出したフィールドと、derivedFieldsで設定したフィールドが列挙され、ここに「TraceID」がされるという形です。

この処理はLoki側ではなくGrafana側で表示する際に行っているということになります。逆に言えば、ここで抽出されている「Detected fields」はLoki側ではindexingされていないので、高速に検索できるわけではないので注意が必要です。Elasticsearch + Kibanaも同じ感じでしたけど、最初は混乱するんですよね、このUI側でのフィールド認識。

どうあれこうやって手軽にログとトレースを行き来できるというのは、GrafanaがLokiとTempoに標準対応しているという強みですね。同じことをKibanaとZipkinで実現している人もいるのですが、ナカナカにやりこみが必要です。

4. GrafanaでLokiのログを見る

ここまでの設定をすべて終えたあと、アプリケーションとGrafanaスタックを起動してGrafanaにアクセスします。

http://localhost:3000

ExploreでLokiを選択し、Label filtersで絞り込み条件(たとえば app = bff)を指定すると、条件に合致したログが表示されます。

GrafanaからLokiのログを絞り込んで表示

Lokiは起動に少し時間が掛かったり、アプリケーションからLokiにログを送るまでタイムラグがあるため、操作してから1〜2分くらい待たないとログが表示されないとか、Label filtersの絞り項目が表示されないことがあり、その時は少し待ってから改めて行うと良いでしょう。

ちなみにLokiはElasticsearchのようにログを単語分割したり、全項目にindexを張ったりしていないので、大量のログに対して文字列一致させるような検索はあまり早くありません。そのため、実運用で利用する際にはあるていど時間帯や対象を限定して検索することになると思いますが、問題発生時にその時間帯のログを確認したいなどという用途においてはあまり困らないと思います。

まとめ

  • Javaアプリケーションに loki-logback-appender と設定ファイルを追加すればLokiにログを送れる
  • GrafanaとLokiはdockerで簡単に利用できる
  • Grafana上でLokiのログからTempoのトレースにジャンプするような設定ができる(Tempoの詳細は次回)
  • Elasticsearchほど高機能じゃないけど軽くて手軽だよ

ところで。

ログをLokiに送る方法として、Prometailのようなエージェントを使うのが良いのか、Appenderで直接転送するのが良いのか、というのはわりと悩ましい問題です。

信頼性という意味ではログエージェントに収集させた方が確実だと思いますし、ログエージェントならログのパースや加工などのパイプライン処理ができるため、よりログを扱いやすい形に加工してLokiに送ることもできます。

一方で、Zipkinなどのトレース情報はアプリケーションから直接送るのが普通ですし、ログだって同じような形で送っても悪くないように思います。

ログについては「絶対に抜けてはいけない。信頼性が重要だ」という意見もあり、そのために信頼性の高いログエージェントに任せるべきだという意見もあるとは思いますが、実際には本当に重要な情報と、さほど重要でない情報が雑多に混ざった状態です。真に重要な情報はログではなくRDBMSなどに書くよう丁寧に設計して管理し、ログはもっと「あると嬉しい」情報に倒した方が楽だと思っています。

そんなわけでなかなかどっちが良いか決めきれないログの出力方式なのですが、いったんブログを一通り書いたあとで、改めてログ収集のあるべき形について考えたいなと思います。