ロギにログを送信する

現在のある講演では、一般的な可観測性と特に分散トレーシングに焦点を当てており、OpenTelemetryの実装を紹介しています。デモンストレーションでは、Apache APISIX API Gateway、Spring Bootを使用したKotlinアプリ、Flaskを使用したPythonアプリ、そしてAxumを使用したRustアプリで構成される単純な分散システムのトレースをどのように見ることができるかを示します。

今年の初め、FOSDEMの可観測性部屋で講演を行い、参加しました。その中で、Grafanaスタックのデモが行われました:メトリクスにはMimir、トレースにはTempo、ログにはLokiを使用していました。どのようにしてこれらの間を移動できるかに驚かされました。そこで、私も同じことをデモで実現したいと考えましたが、OpenTelemetryを通じてGrafanaスタックに依存しない方法で行います。

このブログ記事では、ログとLokiに焦点を当てたいと思います。

Lokiの基本と最初のプログラム

Lokiの核心はログストレージエンジンです:

LokiはPrometheusに触発された水平方向にスケーラブルで、高可用性、マルチテナントのログ集約システムです。非常にコスト効果が高く、運用が容易に設計されています。ログの内容をインデックス化するのではなく、各ログストリームのラベルのセットをインデックス化します。

Loki

Lokiはログの保存と読み取りのためのRESTful APIを提供します。Javaアプリからログをプッシュする方法を見てみましょう。Lokiは以下のようなペイロード構造を期待しています:

I’ll use Java, but you can achieve the same result with a different stack. The most straightforward code is the following:

Java

 

public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
    var template = "'{' \"streams\": ['{' \"stream\": '{' \"app\": \"{0}\" '}', \"values\": [[ \"{1}\", \"{2}\" ]]'}']'}'"; //1
    var now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
    var nowInEpochNanos = NANOSECONDS.convert(now.getEpochSecond(), SECONDS) + now.getNano();
    var payload = MessageFormat.format(template, "demo", String.valueOf(nowInEpochNanos), "Hello from Java App");           //1
    var request = HttpRequest.newBuilder()                                                                                  //2
            .uri(new URI("http://localhost:3100/loki/api/v1/push"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();
    HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());                                         //3
}

  1. 昔の文字列補間の方法はこんな感じでした
  2. リクエストを作成する
  3. それを送信する

プロトタイプはGrafanaで確認できるように動作しています:

しかし、コードには多くの制限があります:

  • ラベルはハードコーディングされています。単一のラベルを送信することができ且つ必要です
  • すべてがハードコーディングされており、何も設定可能ではありません、例えばURL
  • コードは各ログごとにリクエストを送信しており、バッファリングがないため非常に非効率的です
  • HTTPクライアントは同期処理であり、Lokiが応答を待っている間スレッドをブロックしています
  • 全くエラー処理がありません
  • Lokiはgzip圧縮とProtobufの両方を提供していますが、私のコードではどちらもサポートされていません
  • 最後に、これは私たちがログを使用する方法と全く関係ありません、例えば:

    Java

     

    var logger = // ロガーを取得
    logger.info("パラメータ付きのメッセージ {}, {}", foo, bar);

ステロイドでの定期的なロギング

上記のステートメントを使用するためには、ロギング実装を選択する必要があります。私はSLF4JとLogbackに慣れているので、それらを使用します。心配しないでください。同じアプローチはLog4J2でも機能します。

関連する依存関係を追加する必要があります:

XML

 

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>             <!--1-->
    <version>2.0.7</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>       <!--2-->
    <version>1.4.8</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.github.loki4j</groupId>
    <artifactId>loki-logback-appender</artifactId> <!--3-->
    <version>1.4.0</version>
    <scope>runtime</scope>
</dependency>

  1. SLF4Jはインターフェースです
  2. Logbackは実装です
  3. Logbackアペンダー、SLF4J専用

次に、特定のLokiアペンダーを追加します:

XML

 

<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">                   <!--1-->
    <http>
        <url>http://localhost:3100/loki/api/v1/push</url>                                 <!--2-->
    </http>
    <format>
        <label>
            <pattern>app=demo,host=${HOSTNAME},level=%level</pattern>                     <!--3-->
        </label>
        <message>
            <pattern>l=%level h=${HOSTNAME} c=%logger{20} t=%thread | %msg %ex</pattern>  <!--4-->
        </message>
        <sortByTime>true</sortByTime>
    </format>
</appender>
<root level="DEBUG">
    <appender-ref ref="STDOUT" />
</root>

  1. lokiアペンダー
  2. Loki URL
  3. 必要なだけ多くのラベル
  4. 通常のLogbackパターン

私たちのプログラムははるかに簡単になりました:

Java

 

var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);

Grafanaは以下を表示します:

Dockerロギング

I’m running most of my demos on Docker Compose, so I’ll mention the Docker logging trick. When a container writes on the standard out, Docker saves it to a local file. The docker logs  command can access the file content.

ただし、ローカルファイルに保存する以外のオプションがあります。例えばsyslog、Google Cloud、Splunkなど。別のオプションを選択するには、ロギングドライバーを設定します。ドライバーは全体的なDockerレベルであるか、コンテナごとに設定できます。

Lokiは独自のプラグインを提供します。インストールするには:

Shell

 

docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions

この時点で、コンテナアプリで使用できます:

YAML

 

services:
  app:
    build: .
    logging:
      driver: loki                                                    #1
      options:
        loki-url: http://localhost:3100/loki/api/v1/push              #2
        loki-external-labels: container_name={{.Name}},app=demo       #3

  1. Lokiロギングドライバー
  2. プッシュ先URL
  3. 追加のラベル

結果は以下の通りです。デフォルトのラベルに注意してください。

結論

鳥瞰図から見ると、Lokiは特別なものではありません。それは、RESTful APIが搭載されたプレーンストレージエンジンです。

APIを使用するためのいくつかのアプローチがあります。ナイーブな方法を超えて、JavaロギングフレームワークアペンダーとDockerを見てきました。他のアプローチには、ログファイルをスクレイピングする方法があります。例えば、Promtail、Kubernetesサイドカー経由。アプリとLokiの間にOpenTelemetry Collectorを追加して変換を実行することもできます。

オプションは事実上無限です。あなたの状況に最も適したものを選択することに注意してください。

さらに進むには:

Source:
https://dzone.com/articles/send-your-logs-to-loki