CLOVER🍀

That was when it all began.

Bridge Logback into OpenTelemetry(Appender Instrumentation for Logback)を使って、OpenTelemetry Collectorにログを送信する

これは、なにをしたくて書いたもの?

久しぶりのOpenTelemetryまわりを見てみようかなということで、今まで触ってこなかったログを見てみることにしました。

今回はAppender Instrumentation for Logbackを使ってOpenTelemetry Collectorにログを送信してみます。

OpenTelemetryとログ

OpenTelemetryのログに関するコンセプトはこちら。

Logs | OpenTelemetry

ログはタイムスタンプ付きのテキストレコードで、構造化(推奨)または非構造化されています。オプションとしてメタデータ
持ちます。

A log is a timestamped text record, either structured (recommended) or unstructured, with optional metadata.

OpenTelemetryはログを作成するためのAPISDKを定義しません。OpenTelemetryログはロギングフレームワーク
インフラストラクチャーコンポーネントが持つ、すでに存在するログになります。

OpenTelemetry does not define a bespoke API or SDK to create logs. Instead, OpenTelemetry logs are the existing logs you already have from a logging framework or infrastructure component.

Logs / OpenTelemetry logs

OpenTelemetryはログを作成するためのAPISDKは提供しませんが、ログとトレースを自動的に関連付けるためのいくつかの
コンポーネントを使い、OpenTelemetry SDKや自動instrumentationを提供します。

OpenTelemetry SDKs and autoinstrumentation utilize several components to automatically correlate logs with traces.

OpenTelemetryのログサポートは、すでに使用しているであろう既存のものと完全に互換性があるように設計されており、
それらの追加のコンテキストでラップする機能と、様々なソース間でログをパース、操作して共通の形式とするための
ツールキットを提供するものです。

OpenTelemetry’s support for logs is designed to be fully compatible with what you already have, providing capabilities to wrap those logs with additional context and a common toolkit to parse and manipulate logs into a common format across many different sources.

つまり、OpenTelemetryのログに関するAPIはアプリケーションが直接利用するものではなさそうですね。そしてトレースと
紐付けることが重要視されているようです。

OpenTelmetryのログに関するコンポーネントはこちら。

  • Log Record
    • イベントのデータを表すもの
    • 特定の型と意味を持つ名前付きのトップレベルフィールドと、任意の値と型のリソースと属性フィールドの2つで構成される
    • データモデル定義はこちら
  • Log Record Exporter
    • Log Recordをコンシューマーに送信する
    • コンシューマーには標準出力、OpenTelemetry Collector、任意のオープンソースやベンダーのバックエンドを利用可能
  • Logger
    • Log Recordを作成するもの
    • LoggerはLogger Providerにより作成される
  • Logger Provider
    • Loggerのファクトリー
    • 通常は1度初期化され、ライフサイクルはアプリケーションと一致する
    • Logger Providerの初期化にはResourceやExporterの初期化も含まれる
  • Log Appender/Bridge
    • ログライブラリー開発者が使用するもので、アプリケーション開発者が直接利用するものではない
    • OpenTelemetry SDKがこのコンポーネントを利用する

Logs / OpenTelemetry logging components

ここで出てくるログのコンポーネントは、ロギングフレームワークを開発する人向けであることがわかります。

Logs API | OpenTelemetry

つまり、通常のロギングフレームワークを使うと、その裏でこのようなコンポーネントを使ってコンシューマーにLog Recordを
送るということですね。

OpenTelemetry Javaとログ

ここでJavaに関するOpenTelemetry APISDKのドキュメントを見てみます。

Instrumentation ecosystem / Log instrumentation

OpenTelemetryのログ用のコンポーネントはアプリケーション開発者が直接利用するものではなく、
既存のロギングフレームワークを使用して出力するログをOpenTelemetryにブリッジするための機能を作成するためのものです。

While the LoggerProvider / Logger APIs are structurally similar to the equivalent trace and metric APIs, they serve a different use case. As of now, LoggerProvider / Logger and associated classes represent the Log Bridge API, which exists to write log appenders to bridge logs recorded through other log APIs / frameworks into OpenTelemetry. They are not intended for end user use as a replacement for Log4j / SLF4J / Logback / etc.

ここで、OpenTelemetryのログinstrumentationを使うアプローチは次の2つがあるとされています。

  • Collectorにログを直接送信する
  • ファイルまたは標準出力経由でログを送信する

この2つのアプローチの特徴は、こんな感じです。

  • Collector(コンシューマー)にログを直接送信する(Instrumentation ecosystem / Log instrumentation/ Direct to collector
    • ログをOTLPなどのネットワークプロトコルで、アプリケーションから直接コンシューマーへ送信する
    • 追加のログ転送コンポーネントを必要としないため、セットアップが簡単
    • アプリケーションがログをキューに入れてネットワークにエクスポートするというオーバーヘッドが発生する
    • この使い方をするには、以下を行う
      • ロギングフレームワークからOpenTelemetry Log SDKにログをブリッジするShimsの一種である、Log Appenderをインストールする
        • 「Bridge Log4j into OpenTelemetry」、「Bridge Logback into OpenTelemetry」の2つが提供されている
      • 適切なコンシューマーにログを送信するためにOpenTelemetry Log SDKを構成する
  • ファイルまたは標準出力経由でログを送信するInstrumentation ecosystem / Log instrumentation/ Via file or stdout
    • アプリケーションがログをファイルや標準出力に書き出し、別のコンポーネント(例:Fluent Bit)がログの読み込み、パース、変換などを行い、コンシューマーへの送信を行う
    • アプリケーションが直接コンシューマーにログを送信する場合に発生するオーバーヘッドが許容できない場合などに適している
    • ただし、ログを扱うコンポーネントがログをデータモデルの形式に変換する必要がある
    • トレースとのログの関連付けはOpenTelemetryのコンテキストをロギングフレームワークにブリッジするShimsを使うことで実現できる
      • 「Bridge OpenTelemetry context into Log4j」、「Bridge OpenTelemetry context into Logback」の2つが提供されている

今回はLogbackのAppenderからOpenTelemetry Collectorにログを送信するので、「Bridge Logback into OpenTelemetry」を
使うことになります。

Bridge Logback into OpenTelemetry(Appender Instrumentation for Logback

Bridge Logback into OpenTelemetry(Appender Instrumentation for Logback)はこちらです。

opentelemetry-java-instrumentation/instrumentation/logback/logback-appender-1.0/library at main · open-telemetry/opentelemetry-java-instrumentation · GitHub

LogbackのAppenderとして使うことで、コンシューマーへログを送信できます。

  <appender name="OpenTelemetry"
            class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
  </appender>

OpenTelemetry SDKのセットアップが必要になりますが、

    OpenTelemetrySdk openTelemetrySdk = // Configure OpenTelemetrySdk

    // Find OpenTelemetryAppender in logback configuration and install openTelemetrySdk
    OpenTelemetryAppender.install(openTelemetrySdk);

手動で構成する場合はこちらを参照します。

Manage Telemetry with SDK | OpenTelemetry

自動構成に任せる場合はこちらですね。基本的には自動構成することになるでしょう。

Configure the SDK | OpenTelemetry

というわけで、使っていってみましょう。

準備

今回の環境はこちら。

$ java --version
openjdk 21.0.7 2025-04-15
OpenJDK Runtime Environment (build 21.0.7+6-Ubuntu-0ubuntu124.04)
OpenJDK 64-Bit Server VM (build 21.0.7+6-Ubuntu-0ubuntu124.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.10 (5f519b97e944483d878815739f519b2eade0a91d)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.7, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-64-generic", arch: "amd64", family: "unix"

OpenTelemetry Collectorをインストールする

アプリケーションを作成する前に、OpenTelemetry Collectorをインストールしましょう。

$ curl -LO https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.130.0/otelcol-contrib_0.130.0_linux_amd64.tar.gz
$ tar xf otelcol-contrib_0.130.0_linux_amd64.tar.gz

Install the Collector | OpenTelemetry

バージョン。

$ ./otelcol-contrib --version
otelcol-contrib version 0.130.0

設定はこちらを参考にしておきます。

Configuration | OpenTelemetry

Exporterとしてはdebugのみ、パイプラインはLogのみとしました。今回はこれくらいしか使わないので。

onfig.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:

exporters:
  debug:
    verbosity: detailed
    #verbosity: normal
    #verbosity: basic

extensions:
  health_check:
  pprof:
  zpages:

service:
  extensions: [health_check, pprof, zpages]
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

起動。

$ ./otelcol-contrib --config=config.yaml

これでOpenTelemetry Collectorの準備はできました。

アプリケーション側の準備

Maven依存関係などはこちら。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.opentelemetry</groupId>
                <artifactId>opentelemetry-bom</artifactId>
                <version>1.52.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.opentelemetry.instrumentation</groupId>
                <artifactId>opentelemetry-instrumentation-bom</artifactId>
                <version>2.18.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.opentelemetry.instrumentation</groupId>
                <artifactId>opentelemetry-instrumentation-bom-alpha</artifactId>
                <version>2.18.0-alpha</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-api</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-sdk</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-otlp</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.opentelemetry</groupId>
                    <artifactId>opentelemetry-exporter-sender-okhttp</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-sender-jdk</artifactId>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.17</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.18</version>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-logback-appender-1.0</artifactId>
        </dependency>

BOMはこちらを参照して設定しています。

Intro to OpenTelemetry Java | OpenTelemetry

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.opentelemetry</groupId>
                <artifactId>opentelemetry-bom</artifactId>
                <version>1.52.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.opentelemetry.instrumentation</groupId>
                <artifactId>opentelemetry-instrumentation-bom</artifactId>
                <version>2.18.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.opentelemetry.instrumentation</groupId>
                <artifactId>opentelemetry-instrumentation-bom-alpha</artifactId>
                <version>2.18.0-alpha</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Bridge Logback into OpenTelemetry(Appender Instrumentation for Logback)はこちらです。BOMとしては
opentelemetry-instrumentation-bom-alphaに含まれています。

        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-logback-appender-1.0</artifactId>
        </dependency>

こちらはOpenTelemetry SDKを自動構成するモジュールです。

        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
        </dependency>

opentelemetry-sdk-extension-autoconfigureがあればopentelemetry-apiとopentelemetry-sdkは明示的に含めなくてもいいのですが、
なんとなくということで。

こちらはログのExporterと、送信するプロトコルをgRPCからhttp/protobufに切り替えるために使っています。

        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-otlp</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.opentelemetry</groupId>
                    <artifactId>opentelemetry-exporter-sender-okhttp</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-sender-jdk</artifactId>
        </dependency>

通常、こういうことをせずにgRPCを使えばいいと思うのですが、今回はここでハマったのでこういう処置になりました。

Bridge Logback into OpenTelemetry(Appender Instrumentation for Logback)を使って、OpenTelemetry Collectorにログを送信する

それでは、Bridge Logback into OpenTelemetry(Appender Instrumentation for Logback)を使ってOpenTelmetry Collectorに
ログを送信してみます。

アプリケーションの雛形はこんな感じにしておきます。

src/main/java/org/littlewings/logback/opentelemetry/App.java

package org.littlewings.logback.opentelemetry;

import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ServiceAttributes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
    public static void main(String... args) {
        // あとで

            Logger logger = LoggerFactory.getLogger(App.class);

            logger.info("Hello SLF4J with OpenTelemetry!");
            logger.warn("Hello Logback with OpenTelemetry!");
        }
    }
}
手動構成する

最初はOpenTelemetry SDKを手動構成する場合から。こんな感じになります。

    public static void main(String... args) {
        Resource resource = Resource.getDefault().toBuilder()
                .put(ServiceAttributes.SERVICE_NAME, "app")
                .build();

        try (SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder()
                .setResource(resource)
                .addLogRecordProcessor(SimpleLogRecordProcessor.create(
                        OtlpHttpLogRecordExporter.builder()
                                .setEndpoint("http://localhost:4318/v1/logs")
                                .build()))
                .build();
             OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder()
                     .setLoggerProvider(sdkLoggerProvider)
                     .build()) {
            OpenTelemetryAppender.install(openTelemetrySdk);

            Logger logger = LoggerFactory.getLogger(App.class);

            logger.info("Hello SLF4J with OpenTelemetry!");
            logger.warn("Hello Logback with OpenTelemetry!");
        }
    }

参考にしているのはこちら。

Manage Telemetry with SDK | OpenTelemetry

https://github.com/open-telemetry/opentelemetry-java-examples/tree/main/http

https://github.com/open-telemetry/opentelemetry-java-examples/tree/main/log-appender

今回のプログラムの場合、起動してすぐに終了するのでSdkLoggerProviderOpenTelemetrySdkをクローズしないと
ログがOpenTelemetry Collectorまで送信されないので(溜め込まれて送信待ちになります)注意しましょう。

OpenTelemetry SDKを設定したら、Bridge Logback into OpenTelemetry(Appender Instrumentation for Logback)に
インストールします。

            OpenTelemetryAppender.install(openTelemetrySdk);

Lobackの設定はこちら。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>
        %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
      </pattern>
    </encoder>
  </appender>

  <appender name="opentelemetry" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
  </appender>

  <root level="INFO">
    <appender-ref ref="console"/>
    <appender-ref ref="opentelemetry"/>
  </root>

</configuration>

OpenTelemetryAppenderを設定してはいますが、OpenTelemetryAppender#install(OpenTelemetrySdk)を行わないと
ログがコンシューマーに送信されません。

これでアプリケーションを実行してみます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.logback.opentelemetry.App

すると、OpenTelemetry Collector側ではこんな出力が得られます。

2025-07-20T16:06:34.063+0900    info    Logs    {"resource": {"service.instance.id": "60289890-88eb-4554-b01b-82b6b2b193b1", "service.name": "otelcol-contrib", "service.version": "0.130.0"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs", "resource logs": 2, "log records": 2}
2025-07-20T16:06:34.064+0900    info    ResourceLog #0
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(app)
     -> telemetry.sdk.language: Str(java)
     -> telemetry.sdk.name: Str(opentelemetry)
     -> telemetry.sdk.version: Str(1.52.0)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope org.littlewings.logback.opentelemetry.App
LogRecord #0
ObservedTimestamp: 2025-07-20 07:06:33.941853461 +0000 UTC
Timestamp: 2025-07-20 07:06:33.941695759 +0000 UTC
SeverityText: WARN
SeverityNumber: Warn(13)
Body: Str(Hello Logback with OpenTelemetry!)
Trace ID:
Span ID:
Flags: 0
ResourceLog #1
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(app)
     -> telemetry.sdk.language: Str(java)
     -> telemetry.sdk.name: Str(opentelemetry)
     -> telemetry.sdk.version: Str(1.52.0)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope org.littlewings.logback.opentelemetry.App
LogRecord #0
ObservedTimestamp: 2025-07-20 07:06:33.91585833 +0000 UTC
Timestamp: 2025-07-20 07:06:33.911750168 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(Hello SLF4J with OpenTelemetry!)
Trace ID:
Span ID:
Flags: 0
        {"resource": {"service.instance.id": "60289890-88eb-4554-b01b-82b6b2b193b1", "service.name": "otelcol-contrib", "service.version": "0.130.0"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs"}

このあたりがResourceにしていた設定ですね。

Resource attributes:
     -> service.name: Str(app)
     -> telemetry.sdk.language: Str(java)
     -> telemetry.sdk.name: Str(opentelemetry)
     -> telemetry.sdk.version: Str(1.52.0)

この部分。

        Resource resource = Resource.getDefault().toBuilder()
                .put(ServiceAttributes.SERVICE_NAME, "app")
                .build();

ログメッセージやレベルなど。

ObservedTimestamp: 2025-07-20 07:06:33.941853461 +0000 UTC
Timestamp: 2025-07-20 07:06:33.941695759 +0000 UTC
SeverityText: WARN
SeverityNumber: Warn(13)
Body: Str(Hello Logback with OpenTelemetry!)


ObservedTimestamp: 2025-07-20 07:06:33.91585833 +0000 UTC
Timestamp: 2025-07-20 07:06:33.911750168 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(Hello SLF4J with OpenTelemetry!)

よく見ると出力順は入れ替わっているのですが、Timestamp(イベント発生時刻)やObservedTimestamp(イベント観測時刻)は
順番どおりなので大丈夫でしょう。Timestampが守られていればいいのだと思いますが。

自動構成する

次はOpenTelemetry SDKを自動構成して実行します。

自動構成する場合は、こんな感じでだいぶシンプルになります。

    public static void main(String... args) {
        try (OpenTelemetrySdk openTelemetrySdk = AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk()) {
            OpenTelemetryAppender.install(openTelemetrySdk);

            Logger logger = LoggerFactory.getLogger(App.class);

            logger.info("Hello SLF4J with OpenTelemetry!");
            logger.warn("Hello Logback with OpenTelemetry!");
        }
    }

OpenTelemetryAppender#installは必要です。

参考にしたのはこちら。

Configure the SDK | OpenTelemetry

logback.xmlは手動構成の時と同じものを使います。

また、実行前に以下のように環境変数を設定します。

$ export OTEL_TRACES_EXPORTER=none
$ export OTEL_METRICS_EXPORTER=none
$ export OTEL_LOGS_EXPORTER=otlp
$ export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf
$ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
$ export OTEL_SERVICE_NAME=app

トレースとメトリクスは使わないので無効にします。というか無効にしないとgRPCでOpenTelemetry Collectorに接続しようと
します。

また今回の構成の場合、ログ送信に使うプロトコルをhttp/protobufにする必要があります。

$ export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf

OTEL_LOGS_EXPORTERはデフォルト値を明示的に指定して、OTEL_EXPORTER_OTLP_ENDPOINT
プロトコルhttp/protobufを使う場合は4318(gRPCの場合は4317)なのですがこちらも指定しておきます。

このあたりも以下に載っています。

Configure the SDK | OpenTelemetry

実行。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.logback.opentelemetry.App

結果。

2025-07-20T16:32:11.893+0900    info    Logs    {"resource": {"service.instance.id": "60289890-88eb-4554-b01b-82b6b2b193b1", "service.name": "otelcol-contrib", "service.version": "0.130.0"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs", "resource logs": 1, "log records": 2}
2025-07-20T16:32:11.893+0900    info    ResourceLog #0
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(app)
     -> telemetry.sdk.language: Str(java)
     -> telemetry.sdk.name: Str(opentelemetry)
     -> telemetry.sdk.version: Str(1.52.0)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope org.littlewings.logback.opentelemetry.App
LogRecord #0
ObservedTimestamp: 2025-07-20 07:32:11.773433885 +0000 UTC
Timestamp: 2025-07-20 07:32:11.769007298 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(Hello SLF4J with OpenTelemetry!)
Trace ID:
Span ID:
Flags: 0
LogRecord #1
ObservedTimestamp: 2025-07-20 07:32:11.777225417 +0000 UTC
Timestamp: 2025-07-20 07:32:11.777105834 +0000 UTC
SeverityText: WARN
SeverityNumber: Warn(13)
Body: Str(Hello Logback with OpenTelemetry!)
Trace ID:
Span ID:
Flags: 0
        {"resource": {"service.instance.id": "60289890-88eb-4554-b01b-82b6b2b193b1", "service.name": "otelcol-contrib", "service.version": "0.130.0"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs"}

結果はほとんど変わらないので説明を省略します。

こんなところでしょうか。

オマケ: gRPCを使っていない理由

ところで今回はプロトコルをhttp/protobufにしていますが、デフォルトはgRPCです。どうしてこういう構成になったかと
いうと、OkHttp3まわりの扱いが微妙だったからです…。

自動構成の場合で実行してみます。

依存関係は最終的にこうなっていますが

        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-otlp</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.opentelemetry</groupId>
                    <artifactId>opentelemetry-exporter-sender-okhttp</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-sender-jdk</artifactId>
        </dependency>

ひとまずこれにしてみます。

        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-otlp</artifactId>
            <!--
            <exclusions>
                <exclusion>
                    <groupId>io.opentelemetry</groupId>
                    <artifactId>opentelemetry-exporter-sender-okhttp</artifactId>
                </exclusion>
            </exclusions>
            -->
        </dependency>
        <!--
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-sender-jdk</artifactId>
        </dependency>
        -->

これで以下の最低限の環境変数を設定して

$ export OTEL_TRACES_EXPORTER=none
$ export OTEL_METRICS_EXPORTER=none
$ export OTEL_LOGS_EXPORTER=otlp
$ export OTEL_SERVICE_NAME=app

実行します。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.logback.opentelemetry.App

すると、okhttp3.RequestBodyが見つからないという例外に悩まされることになります…。

[WARNING]
java.lang.NoClassDefFoundError: okhttp3/RequestBody
    at io.opentelemetry.exporter.sender.okhttp.internal.OkHttpGrpcSenderProvider.createSender (OkHttpGrpcSenderProvider.java:23)
    at io.opentelemetry.exporter.internal.grpc.GrpcExporterBuilder.build (GrpcExporterBuilder.java:236)
    at io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder.build (OtlpGrpcLogRecordExporterBuilder.java:313)
    at io.opentelemetry.exporter.otlp.internal.OtlpLogRecordExporterProvider.createExporter (OtlpLogRecordExporterProvider.java:77)
    at io.opentelemetry.sdk.autoconfigure.internal.SpiHelper.lambda$loadConfigurable$0 (SpiHelper.java:76)
    at io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager.tryLoadImplementationForName (NamedSpiManager.java:51)
    at java.util.concurrent.ConcurrentHashMap.computeIfAbsent (ConcurrentHashMap.java:1708)
    at io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager.getByName (NamedSpiManager.java:41)
    at io.opentelemetry.sdk.autoconfigure.LogRecordExporterConfiguration.configureExporter (LogRecordExporterConfiguration.java:87)
    at io.opentelemetry.sdk.autoconfigure.LogRecordExporterConfiguration.configureLogRecordExporters (LogRecordExporterConfiguration.java:62)
    at io.opentelemetry.sdk.autoconfigure.LoggerProviderConfiguration.configureLoggerProvider (LoggerProviderConfiguration.java:49)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder.configureSdk (AutoConfiguredOpenTelemetrySdkBuilder.java:553)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder.buildImpl (AutoConfiguredOpenTelemetrySdkBuilder.java:488)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder.lambda$build$12 (AutoConfiguredOpenTelemetrySdkBuilder.java:435)
    at io.opentelemetry.api.GlobalOpenTelemetry.set (GlobalOpenTelemetry.java:130)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder.build (AutoConfiguredOpenTelemetrySdkBuilder.java:433)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk.initialize (AutoConfiguredOpenTelemetrySdk.java:37)
    at org.littlewings.logback.opentelemetry.App.main (App.java:34)
    at org.codehaus.mojo.exec.ExecJavaMojo.doMain (ExecJavaMojo.java:371)
    at org.codehaus.mojo.exec.ExecJavaMojo.doExec (ExecJavaMojo.java:360)
    at org.codehaus.mojo.exec.ExecJavaMojo.lambda$execute$0 (ExecJavaMojo.java:280)
    at java.lang.Thread.run (Thread.java:1583)
Caused by: java.lang.ClassNotFoundException: okhttp3.RequestBody
    at org.codehaus.mojo.exec.URLClassLoaderBuilder$ExecJavaClassLoader.loadClass (URLClassLoaderBuilder.java:211)
    at java.lang.ClassLoader.loadClass (ClassLoader.java:526)
    at io.opentelemetry.exporter.sender.okhttp.internal.OkHttpGrpcSenderProvider.createSender (OkHttpGrpcSenderProvider.java:23)
    at io.opentelemetry.exporter.internal.grpc.GrpcExporterBuilder.build (GrpcExporterBuilder.java:236)
    at io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder.build (OtlpGrpcLogRecordExporterBuilder.java:313)
    at io.opentelemetry.exporter.otlp.internal.OtlpLogRecordExporterProvider.createExporter (OtlpLogRecordExporterProvider.java:77)
    at io.opentelemetry.sdk.autoconfigure.internal.SpiHelper.lambda$loadConfigurable$0 (SpiHelper.java:76)
    at io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager.tryLoadImplementationForName (NamedSpiManager.java:51)
    at java.util.concurrent.ConcurrentHashMap.computeIfAbsent (ConcurrentHashMap.java:1708)
    at io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager.getByName (NamedSpiManager.java:41)
    at io.opentelemetry.sdk.autoconfigure.LogRecordExporterConfiguration.configureExporter (LogRecordExporterConfiguration.java:87)
    at io.opentelemetry.sdk.autoconfigure.LogRecordExporterConfiguration.configureLogRecordExporters (LogRecordExporterConfiguration.java:62)
    at io.opentelemetry.sdk.autoconfigure.LoggerProviderConfiguration.configureLoggerProvider (LoggerProviderConfiguration.java:49)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder.configureSdk (AutoConfiguredOpenTelemetrySdkBuilder.java:553)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder.buildImpl (AutoConfiguredOpenTelemetrySdkBuilder.java:488)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder.lambda$build$12 (AutoConfiguredOpenTelemetrySdkBuilder.java:435)
    at io.opentelemetry.api.GlobalOpenTelemetry.set (GlobalOpenTelemetry.java:130)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder.build (AutoConfiguredOpenTelemetrySdkBuilder.java:433)
    at io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk.initialize (AutoConfiguredOpenTelemetrySdk.java:37)
    at org.littlewings.logback.opentelemetry.App.main (App.java:34)
    at org.codehaus.mojo.exec.ExecJavaMojo.doMain (ExecJavaMojo.java:371)
    at org.codehaus.mojo.exec.ExecJavaMojo.doExec (ExecJavaMojo.java:360)
    at org.codehaus.mojo.exec.ExecJavaMojo.lambda$execute$0 (ExecJavaMojo.java:280)
    at java.lang.Thread.run (Thread.java:1583)

OkHttp3自体はopentelemetry-exporter-otlpによる依存関係に含まれるのですが、これではどうも動かないようです…。

[INFO] +- io.opentelemetry:opentelemetry-exporter-otlp:jar:1.52.0:compile
[INFO] |  +- io.opentelemetry:opentelemetry-exporter-otlp-common:jar:1.52.0:runtime
[INFO] |  |  \- io.opentelemetry:opentelemetry-exporter-common:jar:1.52.0:runtime
[INFO] |  \- io.opentelemetry:opentelemetry-exporter-sender-okhttp:jar:1.52.0:runtime
[INFO] |     \- com.squareup.okhttp3:okhttp:jar:5.1.0:runtime
[INFO] |        +- org.jetbrains.kotlin:kotlin-stdlib:jar:2.2.0:runtime
[INFO] |        |  \- org.jetbrains:annotations:jar:13.0:runtime
[INFO] |        \- com.squareup.okio:okio:jar:3.15.0:runtime
[INFO] |           \- com.squareup.okio:okio-jvm:jar:3.15.0:runtime

で、これをどうにかするのがめんどうになったので、デフォルトのhttp/protobufからhttp/protobufに切り替えることにしました。
依存関係はこのように整理したわけですね。

        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-otlp</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.opentelemetry</groupId>
                    <artifactId>opentelemetry-exporter-sender-okhttp</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-sender-jdk</artifactId>
        </dependency>

ちなみにトレースとメトリクスを切らない場合は、ここでもgRPCを使おうとして阻まれます…。

オマケでした。

おわりに

Bridge Logback into OpenTelemetry(Appender Instrumentation for Logback)を使って、OpenTelemetry Collectorにログを
送信してみました。

最初はいきなりLobackのinstrumentationから見始めたのでかなりてこずりましたが、OpenTelemetryのログの位置づけや
APIの立ち位置を見ていてだいぶ整理できました。

ドキュメントを見るって大事ですね(笑)。

今までOpenTelemetryといえばトレースやメトリクスに寄っていたので、ここにログも絡められるようになるとよいかなと
思います。