## 目次
- 想定読者
- AI Influence Level (AIL)
- 前提環境
- OpenTelemetry eBPF Instrumentation (OBI) とは
- 導入
- 動作確認
- もう少し手を加えてみる
- まとめ
## 想定読者
以下のような読者を想定しています。
- OpenTelemetry、eBPFに興味がある
- RaspberryPi4環境でeBPFを動かしたい
## AI Influence Level (AIL)
この記事におけるAIの活用度はDaniel MiesslerのAI Influence Levelにおいて、
誤字脱字のチェックにのみ利用し、1: Human Created, Minor AI Assistanceに相当します。

AI Influence Level (AIL) v1.0より引用
## 前提環境
我が家には簡易的な空気監視システムがあります。
BME280から取得した温湿度および大気圧をESP32経由でRaspberryPi4(以下、Raspi4)に送信しています。
Raspi4ではMosquitto, Node-RED, InfluxDB, Prometheus, Grafanaを利用し可視化しています。
| 項目 | バージョン |
|---|---|
| Raspberry Pi OS | Debian GNU/Linux 12 (bookworm) |
| Linux kernel | 6.12.25+rpt-rpi-v8 |
| Docker | 28.5.2 |
## OpenTelemetry eBPF Instrumentation (OBI) とは
OpenTelemetryでは、これまでにも言語ごとのSDKが提供されており、既に組み込んでいる方もいるかと思われます。その中でもSDKを用いた手動での実装ではなく、Zero-code Instrumentationと呼ばれるAPIやSDKを利用しない実装方法があります。これにより、利用しているサービスに対して素早く可観測性を担保することが可能となります。
執筆時点の2025年11月現在、以下の言語に対応しています。
- .NET
- Go
- Java
- JavaScript
- PHP
- Python
ただし、分散トレーシングを始めるに当たっては、GoやRustと言ったコンパイル言語では実装に対して手動でトレースポイントを追加する必要があります。
そこで出てきたOpenTelemetry eBPF Instrumentation (OBI) はより幅広いアプリケーションに対して、可観測性を担保するための自動計測ツールです。OBIはeBPFを利用することで、アプリケーションの実行ファイルとOSにおけるネットワーク層を自動的に検査します。
そしてWebトランザクションに関連するトレーススパンと、Linux HTTP/SおよびgRPCサービスのRate Errors Duration (RED) メトリクスをキャプチャすることが可能となります。
なお、eBPFを利用する関係上、制約がいくつか存在します。
- Linux kernelのバージョンが5.8以上(RHELにおいては4.18以上)
- x86_64ないしARM64のプロセッサの利用
- ランタイムがeBPFのサポートをしていること
- ルートアクセス権限および一部のケイパビリティ
## 導入
### Docker環境での導入
OBIはDocker, Kubernetes, スタンドアロンでの動かし方が紹介されています。
ここでは既にDockerでの環境で動作させていたため、Docker環境での導入を行います。
docker run --rm \
-e OTEL_EBPF_OPEN_PORT=8443 \
-e OTEL_EBPF_TRACE_PRINTER=text \
--pid=host \
--privileged \
docker.io/otel/ebpf-instrument:main
-
OTEL_EBPF_TRACE_PRINTER
計測されたトレースを出力するためのフォーマットを指定します。
disabled,text,json,json_indentから選択出来ます。 -
OTEL_EBPF_OPEN_PORT
OBIが記載されたポートに対してトレーシングを行います。
カンマ区切りのみならず、範囲指定も可能です。
8000-8999のように指定した場合、8000~8999番のポートも対象となります。
なお、実行すると以下のようにエラーとなります。
time=2025-11-08T08:35:39.786Z
level=ERROR
msg="Unable to load eBPF watcher for process events"
component=discover.ProcessWatcher
interval=5s
error="loading and assigning BPF objects: field ObiKprobeSysBind:
program obi_kprobe_sys_bind: apply CO-RE relocations: load kernel spec:
no BTF found for kernel version 6.12.25+rpt-rpi-v8: not supported"
### Raspi4のLinux kernelでBTFを有効にする
そもそもBTFとは、BPF Type Formatの略称であり、BPFプログラムとマップに関連するデバッグ情報をエンコードするために設計されたメタデータの形式を表します。
このBTFを有効にするためには設定を変更した上で、Linux kernelをビルドする必要があります。今回はRaspi4内でビルドしましたが、コンパイル速度の問題からよりスペックの良い環境で、クロスコンパイルをした方が良いかもしれません。手元の環境では約3時間要しました。
### raspberrypi/linuxをクローンする
公式レポジトリからクローンを行います。
git clone --depth=1 --branch rpi-6.12.y https://github.com/raspberrypi/linux
### ビルドに必要な依存パッケージのインストール
ビルド時に必要なパッケージをインストールします。
sudo apt install bc bison flex libssl-dev make
### コンフィグの生成
設定変更に必要なコンフィグを生成します。今回は64bit版のRaspi4のため以下のようにします。
cd linux
KERNEL=kernel8
make bcm2711_defconfig
Raspi5や32bit版の際はコマンドが変更となるため注意が必要です。
# 64bit Raspi5
cd linux
KERNEL=kernel_2712
make bcm2712_defconfig
# 32bit Raspi4
cd linux
KERNEL=kernel7l
make bcm2711_defconfig
### 設定変更
設定変更にはmake menuconfigで行いますが、事前にlibncurses5-devが必要です。
更にBTFを有効にするためにはpaholeも必要となります。
sudo apt install libncurses5-dev dwarves
make menuconfig
成功していれば以下のような画面が表示されます。
まずは、設定値の変更を可能にするためDebug informationを有効にします。
Kernel hacking -> Compile-time checks and compiler options -> Debug information (Disable debug information)と進みます。
Rely on the toolchain's implicit default DWARF versionを選択します。
これによりBTFのオプション変更を行うことが可能となり、Compile-time checks and compiler optionsに戻るとGenerate BTF type informationおよびGenerate BTF type information for kernel modules (NEW)が表示されるため有効にします。
Disable debug informationを選択されている、dwarvesがインストールされていない場合は表示されません。
### コンパイル
設定を保存しコンパイルを行います。
make -j6 Image.gz modules dtbs
### インストール
コンパイルが正常完了した後にインストールを行います。
sudo make -j6 modules_install
sudo cp /boot/firmware/$KERNEL.img /boot/firmware/$KERNEL-backup.img
sudo cp arch/arm64/boot/Image.gz /boot/firmware/$KERNEL.img
sudo cp arch/arm64/boot/dts/broadcom/*.dtb /boot/firmware/
sudo cp arch/arm64/boot/dts/overlays/*.dtb* /boot/firmware/overlays/
sudo cp arch/arm64/boot/dts/overlays/README /boot/firmware/overlays/
sudo reboot
## 動作確認
対象のポートはNode-RED, Grafana, InfluxDB, Mosquittoが動作するポートとしています。今回はエラーが出ることなく動作しています。
docker run --rm \
-e OTEL_EBPF_OPEN_PORT=1880,3000,8086,1883 \
-e OTEL_EBPF_TRACE_PRINTER=text \
--pid=host \
--privileged \
docker.io/otel/ebpf-instrument:main
time=2025-11-08T14:56:23.103Z level=INFO msg="OpenTelemetry eBPF Instrumentation" Version=main Revision=104fdd0 "OpenTelemetry SDK Version"=1.37.0
time=2025-11-08T14:56:24.136Z level=INFO msg="starting Application Observability mode"
time=2025-11-08T14:56:24.136Z level=INFO msg="using hostname" component=traces.ReadDecorator function=instance_ID_hostNamePIDDecorator hostname=73f14c57a2d7
time=2025-11-08T14:56:24.146Z level=INFO msg="using hostname" component=traces.ReadDecorator function=instance_ID_hostNamePIDDecorator hostname=73f14c57a2d7
time=2025-11-08T14:56:24.146Z level=INFO msg="Starting main node" component=obi.Instrumenter
time=2025-11-08T14:56:28.851Z level=INFO msg="instrumenting process" component=discover.traceAttacher cmd=/usr/bin/docker-proxy pid=2454 ino=47298 type=generic service=dockerd
2025-11-08 14:47:51.11824751 (0s[0s]) ProcessAlive(subType=0) 0 [ as :0]->[ as dockerd:0] contentLen:0B responseLen:0B svc=[dockerd generic] traceparent=[]
2025-11-08 14:47:51.11824751 (0s[0s]) ProcessAlive(subType=0) 0 [ as :0]->[ as dockerd:0] contentLen:0B responseLen:0B svc=[dockerd generic] traceparent=[]
time=2025-11-08T14:56:30.136Z level=INFO msg="Launching p.Tracer" component=generic.Tracer
2025-11-08 14:47:51.11824751 (0s[0s]) ProcessAlive(subType=0) 0 [ as :0]->[ as dockerd:0] contentLen:0B responseLen:0B svc=[dockerd generic] traceparent=[]
2025-11-08 14:47:51.11824751 (0s[0s]) ProcessAlive(subType=0) 0 [ as :0]->[ as dockerd:0] contentLen:0B responseLen:0B svc=[dockerd generic] traceparent=[]
time=2025-11-08T14:56:30.151Z level=INFO msg="instrumenting process" component=discover.traceAttacher cmd=/usr/sbin/mosquitto pid=2364 ino=524375 type=generic service=""
2025-11-08 14:47:51.11824751 (0s[0s]) ProcessAlive(subType=0) 0 [ as :0]->[ as dockerd:0] contentLen:0B responseLen:0B svc=[dockerd generic] traceparent=[]
time=2025-11-08T14:56:30.160Z level=INFO msg="loading NodeJS instrumentation" component=nodejs.Injector pid=2687
2025-11-08 14:47:51.11824751 (0s[0s]) ProcessAlive(subType=0) 0 [ as :0]->[ as mosquitto:0] contentLen:0B responseLen:0B svc=[mosquitto generic] traceparent=[]
time=2025-11-08T14:56:30.373Z level=INFO msg="Script successfully injected" component=nodejs.Injector
一定時間放置してみると、定期的にトレース結果が表示されていることが分かります。
これはESP32から30秒間隔で送信しているデータが、Raspi4のNode-REDに送信されている部分が見えています。
2025-11-08 14:57:00.1182570 (17.069032ms[5.976471ms]) HTTP(subType=0) 200 GET /(/) [127.0.0.1 as 127.0.0.1:46486]->[127.0.0.1 as node:1880] contentLen:64B responseLen:1973B svc=[node nodejs] traceparent=[00-dd663fa576d6fdb8abec6894f5317772-5a7708a3834bcebe[0000000000000000]-01]
2025-11-08 14:57:30.11825730 (11.073297ms[3.747382ms]) HTTP(subType=0) 200 GET /(/) [127.0.0.1 as 127.0.0.1:41272]->[127.0.0.1 as node:1880] contentLen:64B responseLen:1973B svc=[node nodejs] traceparent=[00-d490966277a39afaa0d1ed37024630b8-b54dee2d6e26b297[0000000000000000]-01]
2025-11-08 14:58:00.1182580 (10.593476ms[3.484345ms]) HTTP(subType=0) 200 GET /(/) [127.0.0.1 as 127.0.0.1:42242]->[127.0.0.1 as node:1880] contentLen:64B responseLen:1973B svc=[node nodejs] traceparent=[00-7319579283614745adf780db37ba99f1-29d4c315fc751007[0000000000000000]-01]
2025-11-08 14:58:31.11825831 (10.582451ms[3.486613ms]) HTTP(subType=0) 200 GET /(/) [127.0.0.1 as 127.0.0.1:49414]->[127.0.0.1 as node:1880] contentLen:64B responseLen:1973B svc=[node nodejs] traceparent=[00-015ae3414e3b05054f4b9a5f6a98d58a-a816fa3baeb03b61[0000000000000000]-01]
またGrafanaにアクセスした際には、Dashboardの表示に必要なデータをInfluxDBから取得するため並行してリクエストされている様子が見えます。
2025-11-08 15:18:58.11831858 (15.402728ms[15.402728ms]) HTTP(subType=0) 200 POST /api/v2/query(/api/v2/query) [172.18.0.4 as 172.18.0.4:60684]->[172.18.0.3 as influxd:8086] contentLen:1333B responseLen:562B svc=[influxd generic] traceparent=[00-2d806ac404e2b46df3054ac485d39f68-663efcd614216a63[0000000000000000]-01]
2025-11-08 15:18:58.11831858 (14.532911ms[14.532911ms]) HTTP(subType=0) 200 POST /api/v2/query(/api/v2/query) [172.18.0.4 as 172.18.0.4:60672]->[172.18.0.3 as influxd:8086] contentLen:1327B responseLen:564B svc=[influxd generic] traceparent=[00-86cf02876dc1198ee0b25be74480e1f9-4be25a147d046361[0000000000000000]-01]
2025-11-08 15:18:58.11831858 (15.958275ms[15.958275ms]) HTTP(subType=0) 200 POST /api/v2/query(/api/v2/query) [172.18.0.4 as 172.18.0.4:60672]->[172.18.0.3 as influxd:8086] contentLen:1429B responseLen:533B svc=[influxd generic] traceparent=[00-09489041b9febe95fc266c5213dffd52-c66b7215155e9314[0000000000000000]-01]
2025-11-08 15:18:58.11831858 (17.554469ms[17.554469ms]) HTTP(subType=0) 200 POST /api/v2/query(/api/v2/query) [172.18.0.4 as 172.18.0.4:60684]->[172.18.0.3 as influxd:8086] contentLen:1429B responseLen:533B svc=[influxd generic] traceparent=[00-9194b7f1ef872b3f4a9b6cae1bfc1906-f2fd0a26331a4d60[0000000000000000]-01]
2025-11-08 15:18:58.11831858 (22.237979ms[22.237979ms]) HTTP(subType=0) 200 POST /api/v2/query(/api/v2/query) [172.18.0.4 as 172.18.0.4:60672]->[172.18.0.3 as influxd:8086] contentLen:1605B responseLen:1609B svc=[influxd generic] traceparent=[00-a52fcd38a28923991e828bdf3269e1ca-5ef9c7b013e71ed6[0000000000000000]-01]
2025-11-08 15:18:58.11831858 (16.952146ms[16.952146ms]) HTTP(subType=0) 200 POST /api/v2/query(/api/v2/query) [172.18.0.4 as 172.18.0.4:60684]->[172.18.0.3 as influxd:8086] contentLen:1423B responseLen:561B svc=[influxd generic] traceparent=[00-e611f995fb8d2fa17465a4e7fcc8b942-ff3d6f9f467da36f[0000000000000000]-01]
2025-11-08 15:18:58.11831858 (18.524488ms[18.524488ms]) HTTP(subType=0) 200 POST /api/v2/query(/api/v2/query) [172.18.0.4 as 172.18.0.4:60696]->[172.18.0.3 as influxd:8086] contentLen:1423B responseLen:547B svc=[influxd generic] traceparent=[00-6e8ed768ed7359f0e8fd5fac7cd44a7b-8666c88b70b3fb4b[0000000000000000]-01]
このように何も手を加えなくても、ホスト上で動作しているアプリケーションがHTTP通信を行っていれば可観測性を担保することが出来るのはとても楽しいですね。
## もう少し手を加えてみる
ここまででOBIがRaspi4上でも動作することが分かりましたが、せっかくなので取得されたトレースの可視化を行ってみます。
OBIではOTEL_EXPORTER_OTLP_TRACES_ENDPOINTによりトレースを送信するEndpointを指定できます。今回はJaegerを選択したため以下のような設定にしています。
otel-ebpf:
image: docker.io/otel/ebpf-instrument:main
container_name: otel-ebpf
restart: always
environment:
- OTEL_EBPF_OPEN_PORT=1883,1880,3000,8086
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4317
- OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=grpc
- OTEL_EBPF_TRACES_INSTRUMENTATIONS=http,grpc
- OTEL_SERVICE_NAME=iot-monitoring-system
pid: host
privileged: true
network_mode: host
Jaeger上では取得されたトレース結果を可視化することが出来ています。30秒間隔でESP32から送信されてくるデータの処理には約9.5ms程度要していることがここから分かります。
またSpanも作成されており、Node-REDにおける処理に要している時間の内訳も分かります。
## まとめ
Raspi4でもOBIを動作させることが出来ました。またOBI自体はeBPFを利用していることで、言語や実装を問わずトレーシングを実現することも出来ていました。Raspi4ではなく一般的なLinux環境であればより容易に試すことが可能だと思うので、可観測性の第一歩にOBIを使ってみるのはかなり良いかなと思いました。
また今回は単一のRaspi4を用いたトレーシングの実現を行いましたが、複数台で構成されているRaspi環境においては各マシンごとに導入を行うのは骨が折れる作業です。そのためOBIによって楽をしつつ分散トレーシングが可能になるのではないかと思いました。
色々とOBIが楽な一方で必要な権限がそれなりにある点には留意すべきです。これはOBIやeBPFに限った話ではないですが、本番環境等での運用を考える際には整理しておくべきポイントかと思います。
## 参考文献









