OpenTelemetry Swift / iOS Evidence-first

OpenTelemetry Swiftのspan欠損・重複を検証
— iOS配送信頼性ラボの読み方

75回の実行、1,142ファイルの証跡、21件の実験索引。
「届いたはず」を件数と生データで再検証できる形にしました。

← Articles 一覧へ戻る

この記事の役割

iOSアプリが生成したOpenTelemetryのspanは、ネットワーク障害、アプリのバックグラウンド移行、強制終了、再起動をまたいだとき、実際には何件届くのか。この疑問を、送信側の完了通知ではなくCollectorが受け取ったIDとの突合で調べました。

Zennの記事では、E000からE021まで実験を積み上げた経緯を時系列で説明しています。このページはその転載ではありません。公開した結果リポジトリを第三者が読む順序、証跡と主張のつながり、再検証できる範囲に焦点を絞った補助ガイドです。

確認基準: 2026年8月8日に公開したリポジトリのmainブランチ。この記事では結論だけを切り出さず、再現条件と未完了の実験も併記します。

何を測ったリポジトリか

中心にある問いは、「生成したspanのうち、Collectorへ一意に届いたのは何件か」です。アプリ側で100件のspan.end()が終わっても、それだけでは配送完了とは扱いません。生成ID、HTTPリクエスト、永続化ファイル、Collector受信ID、再起動後の回収結果を同じrun IDへ結び付けます。

コミット済みの実行証跡は、iPhone 17 Simulator / iOS 26.4.1、opentelemetry-swift persistence 2.5.0、opentelemetry-swift-core 2.5.1、OpenTelemetry Collector 0.157.0、loopbackのOTLP/HTTP JSONという固定条件で取得しています。

  • 75件の検証済みruntime run
  • 1,142件のdigest検証対象ファイル
  • 21件のclaim index登録: runtime完了17、runtime保留2、model完了2

先に押さえる五つの結果

境界 観測したこと 実装判断
再試行の所有者 失敗後のHTTP bodyが100件相当から200、300件相当へ増え、条件によって同じ100件が3回届いた。 永続化層に再試行を持たせるなら、その下のexporterは状態を持たない構成にする。
span.end()と永続化 100件を終了しても、永続化ファイルができる前の停止では再起動後0/100だった。明示的なflush後は100/100になった。 終了通知と耐久化を同じ完了条件として扱わない。
バックグラウンド遷移 5秒のbatch間隔より前にbackgroundへ移ると未flush条件は0/100。background handlerでflushした条件は100/100だった。 ライフサイクル境界に配送処理を置き、完了時間も測る。
256KiB付近のobject境界 500件は0/500、1,000件は最後の232件だけ回収という無言の欠損を観測。batchを100へ下げると両方を全件回収した。 件数上限だけでなく、エンコード後byte数でも分割する。
main queueの停止時間 同期flush中、先にqueueへ入れたprobeが297〜1,271ms遅れた。配送件数が正しくてもUI応答性は別問題だった。 配送の完全性と、ライフサイクル予算・UI停止時間を別の指標にする。

ここでの数値は、SDK全体に永久に当てはまる仕様値ではありません。固定したバージョンと実験条件で観測した結果です。依存関係やOSを変えた場合は、新しい条件として再実行する必要があります。

「何件届いたか」をどう証明するか

このラボでは、スクリーンショットを結論の根拠にしていません。主張は次の順序で追えるようにしています。

  1. 実験計画で、変更する条件、固定する条件、成功基準、除外条件を先に決める。
  2. raw evidenceへ、生成ledger、HTTP lifecycle、永続化状態、Collectorの受信データをrun単位で保存する。
  3. manifestで対象ファイルを列挙し、digestと取得条件を固定する。
  4. reconciliationで生成IDと受信IDを集合比較し、missing、duplicate、unexpectedを数える。
  5. claim indexから、各主張が参照する結果、証跡、状態を監査する。

つまり「flushが成功を返した」ことではなく、「生成したIDがCollector側に一度ずつ存在する」ことを成功条件にしています。配送APIの戻り値と実際の到達件数がずれる境界を見つけるには、この二つを分ける必要がありました。

リポジトリを読む順序

  1. READMEで、研究質問とE000〜E021の状態を一覧する。
  2. evidence/claim-index.jsonで、主張から結果・証跡へのパスと状態を確認する。
  3. experiments/で、対象実験のplanとresultsを対で読む。
  4. evidence/raw/で、run manifestと列挙された生データを照合する。
  5. docs/reproduction.mdで、固定環境と再実行手順を確認する。

関心のある結論から直接rawデータへ飛ぶより、まずclaim indexを通る方が、runtime実測、model上の検証、未実行の条件を混同しにくくなります。

Simulatorなしでできる検証

リポジトリをcloneした後、次の四つはiOS Simulatorを起動せずに実行できます。

scripts/test-core.sh
scripts/test-evidence-verifier.sh
scripts/verify-all-evidence.sh
scripts/audit-claim-index.sh

test-core.shはID突合などの決定的なcoreロジック、test-evidence-verifier.shは証跡検証器そのものを確認します。残り二つは、75 run / 1,142 filesのinventoryと、21件のclaim state・参照パスを監査します。

ただしmanifestとdigestは、単独の削除、改変、未列挙ファイルを見つける仕組みです。生データとdigestを同時に整合するよう書き換える攻撃まで、ローカルファイルだけで証明するものではありません。Gitのremote historyが外部の基準点になり、署名付きtagや独立保管を追加するとさらに強くなります。

runtimeを再実行するときの注意

実際の配送を再実行する場合は、固定したCollector、Simulator、Swift packageを用意し、各experimentに記録されたmatrixを使います。デバイスやOS、依存バージョンを変えた結果は、既存runへ追記せず別条件として残します。

  • E017とE018は、core/modelの検証はありますが登録済みSimulator matrixが未実行で、状態はruntimePendingです。
  • E019とE020のmodel結果を、iOS runtimeで観測済みの結果として扱わないよう状態を分離しています。
  • flush完了時間だけではmain queueの停止を表せないため、ライフサイクル介入ではprobeの再開時刻も確認します。
  • payloadを大きくした場合、件数ベースの安全値はそのまま使えません。個々のspanが上限を超える場合はbatch 1でも回収できません。
このリポジトリは、特定バージョン・特定条件の実測記録です。OpenTelemetry Swiftの現行版に同じ挙動があると断定する資料ではありません。導入判断では、利用するバージョンで同じmatrixを再実行してください。

この検証から残した設計原則

  • 再試行を行う層は一つにし、再送対象の集合を増幅させない。
  • span.end()、processor flush、永続化、Collector到達を別々の状態として記録する。
  • batchは件数とencoded byteの両方で制限し、分割不能な単一spanは無言で捨てず識別情報を残す。
  • backgroundでの回収率だけでなく、処理時間とmain queueへの影響も同じ実験で測る。
  • 完了した実測、計算モデル、保留中の実験を一つの索引で区別する。

SDKの関数を呼んだ回数ではなく、配送境界の前後に残る観測可能な事実を数える。この方針が、0件、重複、部分回収を同じ物差しで比較するための土台になりました。