
テレメトリにも互換性の問題がある
何気ない変更が思わぬトラブルに
例えば、あるサービスで request.duration というメトリクスを出していたとします。
開発者が実装を整理する中で、他のメトリクスとの命名を揃えるため、この名前を request.latency に変更しました。
コードの変更自体は小さなものです。アプリケーションも正常に動いていますし、新しい名前でメトリクスも送られています。
ところが、そのメトリクスを使って作られていたチーム内のダッシュボードは値を表示できなくなりました。
同じメトリクス名を参照していたアラートも、期待どおりに動かなくなりました。
問題は利用側で起きる
変更した実装だけを見れば、これはテレメトリの名前を変えただけに見えます。 しかし、そのテレメトリを利用している側から見ると、依存していたものが突然変わったことになります。
名前だけではありません。属性の名前や型、メトリクスの単位、計測方法などを変更した場合も、同じようなことが起こり得ます。
アプリケーションが動いていて、テレメトリも送られているから問題ない、とは言えません。
利用者が依存しているテレメトリを互換性への配慮なく変更すれば、利用者にとってはbreaking changeになります。
テレメトリにも互換性を考える
APIであれば、利用者が依存しているインターフェースを不用意に変更しないのは当然です。
テレメトリも誰かが継続して利用しているのであれば、同じように互換性を考えるべきです。
ただ、互換性を守ると言っても、何を守るのかが曖昧なままでは判断できません。
メトリクス名なのか、属性なのか、型や単位なのか。まずは、提供する側と利用する側の間で「何を提供し、何を維持するのか」を明らかにする必要があります。
「約束」をTelemetry Contractとして考える
この「何を提供し、何を維持するのか」という約束を、この記事では Telemetry Contract という言葉で表現します。
なお、Telemetry Contractという言葉は私の造語ではありません。
OpenTelemetry周辺でも、OBIのissue #1759やSIG End Userのissue #332などで、この言葉や近い考え方が使われています。
また、OpenTelemetryのVersioning and Stabilityにも、producerとconsumerの間で互換性を扱う考え方があります。
一方で、Telemetry ContractがOpenTelemetryで統一された正式な用語として定義されているわけではなく、その意味も一つに定まっているわけではありません。
そこでこの記事では、
producerがconsumerに何を提供し、何を維持すると約束するか
という意味でTelemetry Contractを使います。
決めた約束をどうやって維持するか
ここでいうContractは、YAMLなどの定義ファイルそのものではありません。定義ファイルは、約束のうち機械で表現できる部分を記述する手段です。
ただ、約束を決めるだけでは十分ではありません。
名前や型、単位、属性について「これは維持する」と決めても、それが人向けのドキュメントにしか書かれていなければ、実装の変更とずれていく可能性があります。
変更のたびに人が確認するだけでは、見落としも起こります。
そこで、約束のうち機械で表現できる部分だけでも、共通の定義として持てないかと考えました。
その定義を、作るときの検証だけでなく、利用者向けのドキュメント生成、変更前後の比較、実装から送出されたテレメトリとの照合にも使えるのであれば、Telemetry Contractを維持するための仕組みとしてかなり使いやすくなりそうです。
このようなことができないか調べていて見つけたのがOpenTelemetry Weaverでした。
OpenTelemetry Weaverとは
OpenTelemetry Weaverは、機械可読なテレメトリの定義を扱うためのツール群です。
テレメトリを公開APIのように扱う
WeaverのREADMEには “Treat your telemetry like a public API”(テレメトリを、公開APIのように扱う) とあります。
ここまで考えてきた「利用者が依存するテレメトリにも互換性がある」という話と、かなり近い発想です。
OpenTelemetryの公式ブログでも、この考え方は「Observability by Design」として説明されています。
Observabilityをデプロイ後に考えるのではなく、ソフトウェア開発の初期から設計対象にし、テレメトリを公開APIのように扱うという考え方です。
メトリクス名の変更によって既存のアラートやダッシュボードが壊れることも、その背景にある問題の一つとして挙げられています。
Observability by Design: Unlocking Consistency with OpenTelemetry Weaver | OpenTelemetry
Weaverでは、テレメトリの定義を検証したり、独自のルールを適用したり、定義からドキュメントなどを生成したりできます。
定義の変更を比較する機能や、実際に送られてきたテレメトリを定義と照合する live-check もあります。
Telemetry Contractを維持する仕組みとして使えるか
私が気に入ったのは、これらを個別の機能として使えることだけではありませんでした。
テレメトリについて「何を提供し、何を維持するのか」を機械可読な定義として表現し、その定義を作成時の検証、変更時の確認、送出されたテレメトリとの照合など、複数の場面で共通の基準として使えるのであれば、
Telemetry Contractを維持する仕組みとして使えるのではないか、と考えました。
一方で、Weaverが「何を約束すべきか」まで決めてくれるわけではありません。
何をTelemetry Contractに含めるのか、どの変更を互換性のない変更として扱うのかは、提供する側と利用する側で決める必要があります。
そこで今回は、WeaverをTelemetry Contractそのものとして見るのではなく、Telemetry Contractのうち機械で表現できる部分を、実際に複数の工程で使えるのかという観点から試してみました。
Weaverでどこまで扱えるか
ここまで、Telemetry Contractのうち機械で表現できる部分を、Weaverを使って複数の工程で利用できるのではないか、と考えてきました。
実際にどこまでできるのか、いくつかの小さな定義を使って試してみます。
今回試した環境と結果
今回の検証では、すべて OpenTelemetry Weaver v0.26.1 を使っています。
先に結果をまとめると、次のようになりました。
| # | 試したこと | 確認できたこと | 今回見えた境界 |
|---|---|---|---|
| 1 | 機械で読める定義を用意する | metricやattributeなどを構造化して定義できた | definition/2 はv0.26.1では not yet stable |
| 2 | 定義を検証する | schema errorや壊れた参照を検出できた | diagnosticsだけで常に修正箇所まで特定できるわけではない |
| 3 | 独自ルールを適用する | Regoで自分たちのルールを評価できた | 何を違反とするかは利用する側で決める |
| 4 | 同じ定義から成果物を生成する | MarkdownやJSONを生成できた | 利用者向けの見せ方はtemplate側で考える必要がある |
| 5 | 変更を確認する | 定義の追加・削除を registry diff で確認できた |
今回試した一部のフィールド変更は registry diff に現れなかった |
| 6 | 互換性のルールを評価する | 独自policyで型、単位、instrumentなどの変更を違反にできた | 何を互換性のない変更とするかは自分たちで決める |
| 7 | 送出されたテレメトリと照合する | live-check で型の不一致や一部のrequired attribute欠落などを検出できた |
signalによって確認できる範囲が違った |
なお、以下の検証は、一つの定義を最初から最後まで通したものではありません。それぞれ目的に合わせた小さなfixtureを使って確認しています。
以下では、表の順に実際に試した内容を見ていきます。
1. 機械で読める定義を用意する
最初に、テレメトリをWeaverで扱える形にしてみます。
小さな決済サービスを定義する
ここでは架空の決済サービスを用意しました。
実際に使った定義から、今回の話に必要なところだけ抜き出すと次のようになります。
attributes: - key: example.payment.method type: members: - id: card - id: bank_transfer stability: development metrics: - name: example.payment.processed instrument: counter unit: "{payment}" stability: development attributes: - ref: example.payment.method
この定義には、メトリクスの名前だけでなく、計測方法、単位、安定性、利用する属性などが記述されています。
例えば example.payment.processed は counter であり、example.payment.method という属性を利用する、といった情報です。
前に話していたTelemetry Contractのうち、機械で表現できる部分を、このような形で記述していくことになります。
なお、今回使った definition/2 は、Weaver v0.26.1では正常に利用できましたが、実行時に次のwarningが表示されました。
File format `definition/2` is not yet stable
そのため、今回の検証では利用できたものの、安定した形式として扱える段階ではない点には注意が必要です。
2. 定義を検証する
定義を機械で読める形にしたら、まず期待したのは、おかしな定義をそのまま使わせないことでした。
壊れた参照を入れてみる
例えば、メトリクスから存在しない属性を参照するように定義を変更します。
attributes: - ref: example.payment.nonexistent
Weaver v0.26.1で、存在しない属性を参照する定義に対して registry check を実行すると、次のようになりました。
$ weaver registry check -r bad-reference --v2 The following attribute reference is not resolved for the group 'metric.example.payment.processed'. Attribute reference: example.payment.nonexistent Provenance: .../model/payment.yaml
このときexit codeは1です。
対象となるmetric、存在しない属性名、定義ファイルまで確認できます。
このほか、必須の type を削除した場合には、
Missing required property: "type".
としてschema errorも検出されました。
少なくとも今回試した範囲では、定義の構造や参照関係がおかしくなっていないかを、利用者へ届く前に確認できます。
一方、すべてのdiagnosticがYAML上の正確な行番号まで示してくれるわけではありませんでした。
小さな定義では十分追えましたが、registryが大きくなった場合の使い勝手は別に確認した方がよさそうです。
3. 独自ルールを適用する
schemaとして正しいだけでは、利用者との約束を守れているとは限りません。
次に、自分たちで決めたルールをWeaverで評価できるかを試しました。
Regoでルールを書いてみる
検証用に、
example.payment.*の属性の説明にはpaymentという言葉を含める
という単純なルールを用意しました。
Weaverでは、このような独自ルールをRegoで記述できます。今回使ったルールは次のようなものです。
package after_resolution
import rego.v1
deny contains finding if {
# 定義されている属性を一つずつ確認する
attr := input.registry.attributes[_]
# example.payment.* の属性だけを対象にする
startswith(attr.key, "example.payment.")
# 説明文に "payment" が含まれていなければ違反とする
not contains(lower(attr.brief), "payment")
finding := {
"id": "example_payment_brief_must_identify_payment",
"message": sprintf(
"Attribute %s must mention payment in its brief.",
[attr.key]
),
"level": "violation"
}
}
after_resolution では、Weaverが読み込んで解決したregistryを input.registry から参照できます。
このルールでは、その中にあるattributeを順に見て、example.payment.* で始まるattributeの説明に payment が含まれていなければ violation を返しています。
試しに example.payment.amount の説明から payment を取り除き、Weaver v0.26.1で registry check を実行しました。
$ weaver registry check -r bad-description --v2 -p bad-description/policies Attribute example.payment.amount must mention payment in its brief.
このときexit codeは1になりました。
ルールを決めるのはWeaverではない
ここで重要なのは、「説明には payment を含めるべきだ」というルールそのものをWeaverが決めたわけではないことです。
このルールを決めたのは私たちです。Weaverは、決めたルールと実際の定義を照らし合わせ、違反しているかを評価しています。
Telemetry Contractについても同じです。
何を利用者との約束として維持するのか。その約束を破る変更を何とするのか。そこは自分たちで決める必要があります。
Weaverが手伝ってくれるのは、一度決めたルールを機械的に評価し、変更のたびに確認できるようにするところです。
4. 同じ定義から成果物を生成する
次に試したのが、同じ定義を人が読むドキュメントなどの生成にも使えるかです。
templateからMarkdownを生成する
WeaverのForgeでは、registryの内容をtemplateへ渡して成果物を生成できます。
今回の検証では、まず次のようなMarkdown用のtemplateを用意しました。
# Example Payment Service telemetry
## Attributes
{% for attr in ctx.registry.attributes %}
- `{{ attr.key }}` ({{ attr.type }}): {{ attr.brief }}
{% endfor %}
## Spans
{% for span in ctx.registry.spans %}
- `{{ span.type }}` — {{ span.brief }}
{% endfor %}
## Metrics
{% for metric in ctx.registry.metrics %}
- `{{ metric.name }}` ({{ metric.instrument }}, `{{ metric.unit }}`) — {{ metric.brief }}
{% endfor %}
## Events
{% for event in ctx.registry.events %}
- `{{ event.name }}` — {{ event.brief }}
{% endfor %}
このtemplateを使って、Weaver v0.26.1でMarkdownを生成します。
$ weaver registry generate markdown generated/markdown \
-r registry --v2 \
-t templates \
-p policies
生成されたMarkdownには、例えば次のような内容が含まれました。
# Example Payment Service telemetry ## Attributes - `example.payment.amount` (double): The payment amount in the currency's major unit. - `example.payment.currency` (string): The payment currency expressed as an ISO 4217 code. - `example.payment.id` (string): A payment identifier. - `example.payment.method` ({"members": [{"brief": "Payment by card.", "id": "card", "value": "card"}, {"brief": "Payment by bank transfer.", "id": "bank_transfer", "value": "bank_transfer"}]}): The payment method used. ## Metrics - `example.payment.processed` (counter, `{payment}`) — The number of processed payments.
同じregistryに書いたattributeやmetricの情報を、人向けのMarkdownへ展開できています。
JSONの成果物も生成する
別のtemplateを用意すれば、同じregistryからJSONのような機械向けの成果物も生成できます。
今回の検証では、attribute、span、metricの名前だけを取り出すtemplateも用意しました。
{
"attribute_keys": [{% for attr in ctx.attributes %}"{{ attr.key }}"{% if not loop.last %}, {% endif %}{% endfor %}],
"span_types": [{% for span in ctx.spans %}"{{ span.type }}"{% if not loop.last %}, {% endif %}{% endfor %}],
"metric_names": [{% for metric in ctx.metrics %}"{{ metric.name }}"{% if not loop.last %}, {% endif %}{% endfor %}]
}
これもWeaver v0.26.1で生成すると、
$ weaver registry generate jsonmeta generated/jsonmeta \
-r registry --v2 \
-t templates \
-p policies
次のようなJSONになりました。
{ "attribute_keys": ["example.payment.amount", "example.payment.currency", "example.payment.id", "example.payment.method"], "span_types": ["example.payment.process"], "metric_names": ["example.payment.processed"] }
用途に応じてtemplateを用意すれば、同じテレメトリ定義から異なる形式の成果物を作れます。
今回の小さなfixtureでは、MarkdownとJSONをそれぞれ同じ入力から繰り返し生成したところ、各形式の生成結果はbyte単位で一致しました。
また、attributeの宣言順だけを入れ替えて再生成した場合も、今回の2つの成果物には差分がありませんでした。
生成物をGitで管理したり、CIで差分を確認したりする場合には使いやすそうです。
生成できることと、必要な意味が伝わることは別
一方で、先ほどのMarkdownを見ると気になるところがあります。
example.payment.method の型は、本来 card と bank_transfer を持つenumです。しかし、単純に {{ attr.type }} と出力すると、
{"members": [{"brief": "Payment by card.", "id": "card", "value": "card"}, ...]}
のように、内部構造に近い表現がそのまま出てきました。
これはWeaverが誤った情報を生成したという話ではありません。今回のtemplateで、enumを利用者向けにどう表現するかを何も指定していないためです。
同じ定義を使っていても、そこから何を取り出し、どう見せるかはtemplate側で決まります。
つまり、同じ定義から生成できることと、利用者に必要な意味が適切に伝わることは別です。
機械可読な定義を基準にすることで、定義とドキュメントを別々に手作業で管理する量は減らせそうです。一方で、「定義を一つにすれば、あとは自動生成ですべて解決する」とまでは言えません。
5. 変更を確認する
Telemetry Contractとして扱うなら、定義を変更したときに何が変わったのかも確認したいところです。
ここでは、先ほどの決済サービスとは別に、小さなstorage service相当のfixtureを使いました。
このfixtureには my.attr、attr2、my.span、my.metric などを定義しています。
追加と削除は registry diff で見えた
Weaver v0.26.1でattributeを追加した定義を比較すると、次のようになりました。
$ weaver registry diff --v2 \
--registry candidate-additive \
--baseline-registry baseline \
--format json
{
"registry_attributes": [
{
"type": "added",
"name": "my.optional.region"
}
],
...
}
attributeを削除した場合も、同じように変更を確認できます。
$ weaver registry diff --v2 \
--registry candidate-remove \
--baseline-registry baseline \
--format json
{
"registry_attributes": [
{
"type": "removed",
"name": "attr2"
}
],
...
}
attr2 を my.attr2 へ名前変更したケースでは、renameとして一つの変更になるのではなく、追加と削除として観測されました。
$ weaver registry diff --v2 \
--registry candidate-rename \
--baseline-registry baseline \
--format json
{
"registry_attributes": [
{
"type": "added",
"name": "my.attr2"
},
{
"type": "removed",
"name": "attr2"
}
],
...
}
定義の中身を変えた場合はどうなるか
次に、定義そのものは残したまま、中のフィールドだけを変更してみました。
例えば my.metric の instrument を、
counter → gauge
へ変更します。
Weaver v0.26.1と今回のfixtureでは、この状態で registry diff を実行しても結果は空でした。
同じ検証範囲では、
attribute type: int → string
metric unit: {1} → ms
stability: stable → development
といった変更も、それぞれのフィールドの変更として registry diff には現れませんでした。
ここは注意が必要です。
これは「Weaverの registry diff はフィールド単位の変更を検出できない」という一般的な性質を示したものではありません。Weaver v0.26.1と、今回試したfixture・変更内容では、この結果になったという観測です。
では、こうした変更をTelemetry Contract上の問題として検出したい場合はどうすればよいのでしょうか。
6. 互換性のルールを評価する
前節では、registry diff を使って定義の追加や削除を確認できました。
一方で、何が変わったのかを確認することと、その変更を許してよいか判断することは別の問題です。
そこで、変更前の定義をbaseline、変更後の定義をcandidateとして比較する独自のRego policyを用意しました。
フィールド単位の変更をpolicyで評価する
今回のpolicyでは、baselineとcandidateの同じ定義を比較し、型や単位、instrument などが変わっていたらviolationを返すようにしました。
例えばattributeの型を比較する部分は、概ね次のような形です。
old := data.registry.attributes[_] new := input.registry.attributes[_] old.key == new.key old.type != new.type
data.registry がbaseline、input.registry がcandidateです。
同じ考え方でmetricの unit や instrument も比較します。
Weaver v0.26.1では、変更内容ごとにcandidateを用意してpolicyを適用しました。
例えば、attributeの型を int から string へ変更したケースでは、
Attribute attr2 type changed from int to string.
metricの unit や instrument を変更したケースでは、
Metric my.metric unit changed from {1} to ms.
Metric my.metric instrument changed from counter to gauge.
というfindingを確認できました。
これらは、いずれも今回用意したpolicyによって violation として評価されたものです。violationがある場合、registry check のexit codeは1になりました。
前節では registry diff に現れなかったフィールド単位の変更も、比較するルールを自分たちで定義すれば評価できることが分かります。
何を壊れたとみなすかはWeaverが決めない
ただし、ここは区別しておく必要があります。
counter から gauge への変更をviolationにしたのは、今回私が作ったpolicyです。
Weaver自身が、
counterからgaugeへの変更はTelemetry Contractを壊す
と判断したわけではありません。
単位の変更を許すのか。attributeの追加をどう扱うのか。stableなattributeの型変更を止めるのか。
何を互換性のない変更とするかは、利用者との間で「何を維持する」と約束したのかによって変わります。
Telemetry Contractで守る範囲を決め、その判断基準をpolicyとして表現すれば、Weaverを使って継続的に評価できます。
ContractとCIの運用も別の話
さらに、互換性のルールを決めても、まだ運用上の判断は残ります。
例えば、
- どのviolationでCIを止めるのか
- 一時的な例外を認めるのか
- 例外を誰が承認するのか
といったことです。
これらは「利用者に何を約束するか」というTelemetry Contractそのものとは別に、組織として決めるpolicyや運用の話です。
Weaverは、決めたルールを機械的に評価するところまでを支援できます。
今回試してみて、変更を観測すること、互換性を判断すること、その判断をCIでどう運用するかは、それぞれ分けて考えた方がよいということがかなりはっきりしました。
7. 送出されたテレメトリと照合する
ここまでは、テレメトリの定義を作ったり変更したりするときの話でした。
しかし、定義が正しくても、実装がその定義どおりのテレメトリを送っているとは限りません。
そこで最後に、Weaverの live-check を使って、実験用に送出したテレメトリをregistryと照合してみました。
今回確認するのは、実験用に送出したテレメトリをregistryと照合できるかです。本番環境での常時監視までを検証するものではありません。
live-check では別のregistryを使う
この検証でも Weaver v0.26.1 を使っています。
ここでは、それまでの検証とは別に、live-check 用の小さなregistryを用意しました。このregistryはlegacy v1形式です。
なお、Weaver v0.26.1の live-check 自体はV2 schemaにも対応しています。そのため、以下の結果は「live-check はv1でなければ使えない」という意味ではありません。
架空のcheckout serviceとして、spanの example.checkout.process、eventの example.checkout.process、metricの example.checkout.total などを定義しています。
主なattributeは次のとおりです。
example.checkout.id string / required example.checkout.method enum(card, cash) example.checkout.total double
live-check はOTLP receiverとして起動し、そこへ実験用のスクリプトからtrace、log、metricを送ります。
今回使った起動方法は、概ね次のようなものです。
$ weaver registry live-check \
-r registry \
--input-source otlp \
--otlp-grpc-address 127.0.0.1 \
--otlp-grpc-port 54317 \
--admin-port 54320 \
--format json \
--output http \
--fail-on violation
Weaver v0.26.1では、今回OTLP/gRPCでtrace、log、metricを受信できました。
型が違えば検出された
まず、registryでは、
example.checkout.total: double
と定義しておきます。
そこへ、example.checkout.total の値を文字列の "12.5" にしたspanを送ってみました。
実験用スクリプトから送信すると、live-check では type_mismatch のviolationとして検出されました。
$ python run.py type --signal span Attribute 'example.checkout.total' has type 'string'. Type should be 'double'.
reportには、実際の型が string、期待した型が double であることも含まれていました。
次に、送るtelemetryはまったく変えず、registry側の定義だけを、
double → string
へ変更します。
同じ "12.5" を送ると、今度は type_mismatch が出なくなりました。
$ python run.py type --signal span --registry registry-string 0 findings
少なくともこのケースでは、registryに書いた型が、送出したtelemetryを判定する基準として使われています。
required attributeはsignalによって結果が違った
一方で、何でも同じように確認できたわけではありません。
example.checkout.id をrequiredとして定義したうえで、実際に送るtelemetryからこのattributeを抜いてみました。
結果は次のようになりました。
| 送ったsignal | required attributeを欠落させた結果 |
|---|---|
| metric | required_attribute_not_present |
event_name を持つlog |
required_attribute_not_present |
| span | findingなし |
metricと、registryのevent名に対応する event_name を持つlogでは、required attributeの欠落がviolationとして検出されました。
一方、spanでは同じattributeを欠落させてもfindingは出ませんでした。
未定義のspan名も同じようには検出されなかった
spanについては、さらに名前の対応も確認しました。
registryには、
example.checkout.process example.checkout.refund
という2つのspan definitionを用意しました。
そのうえで、registryに存在しないspan名も送ってみましたが、今回の検証では未定義span名に対するfindingは出ませんでした。
また、process と refund のどちらのdefinitionに対応付けられたのかを、reportから確認することもできませんでした。
Weaver v0.26.1のCLIにも、今回確認した範囲ではspan matcherを指定するoptionはありませんでした。
一方で、metricはmetric名、eventは event_name によってregistry上の定義と対応付けられていることを確認できました。
attributeの検証とsignal全体の検証は同じではない
ここは今回の live-check の検証で、特に注意したいところです。
spanに含まれる example.checkout.total の型が違えば、type_mismatch は検出されました。
しかし、それだけで、
このspanは
example.checkout.processの定義全体に適合している
とまでは言えません。
今回のWeaver v0.26.1では、spanのrequired attribute欠落や未定義span名について、metricやeventと同じような結果にはなりませんでした。
つまり、個々のattributeをregistryと照合できることと、signal全体が特定のdefinitionに適合していることを確認できることは別です。
そのため、live-check を使えば「送出されたテレメトリが定義どおりであることをすべて確認できる」と考えるのは強すぎます。
それでも、registryで定義した型と実際の値の違いを検出でき、registry側の定義を変えると同じtelemetryに対する判定も変わりました。
今回試した範囲では、機械可読な定義を、送出されたテレメトリを確認するための基準として利用できることは確認できました。
今回わかったこと
ここまで実際に試してみて、Telemetry ContractとWeaverの関係がだいぶ整理できました。
WeaverがTelemetry Contractを決めるわけではない
まず、WeaverがTelemetry Contractそのものを決めてくれるわけではありません。
利用者に何を提供し、何を維持すると約束するのか。
メトリクス名を維持するのか。attributeの型や単位まで含めるのか。どの変更を互換性のない変更として扱うのか。
こうしたことは、提供する側と利用する側で決める必要があります。
Weaverは、その判断を代わりにしてくれるものではありません。
一方で、一度決めた約束のうち機械で表現できる部分については、定義の検証、独自ルールの評価、成果物の生成、変更の確認、送出されたテレメトリとの照合と、複数の工程で利用できました。
今回試してみて重要だと感じたのは、一つのYAMLファイルをそのまま使い回せることではありません。テレメトリの定義を、複数の工程で共通の基準として使えることです。
変更を見つけること、判断すること、止めることは別
今回特に印象に残ったのは、変更を見つけること、その変更が互換性のないものか判断すること、実際に変更を止めることは、それぞれ別だという点です。
registry diff で変更を確認できても、それが利用者との約束を壊す変更なのかは別に判断する必要があります。
独自policyでその判断基準を表現し、violationとして検出することもできます。
さらに、そのviolationでCIを止めるのか、例外を認めるのか、誰が承認するのかは、また別の運用上の判断です。
何を互換性のない変更とするのかは、Telemetry Contractで何を維持すると約束したのかをもとに決める必要があります。
その判断基準をpolicyとして表現し、CIや承認をどう運用するかは、その次の話です。
Weaverは、こうして決めたルールを継続的に評価するところを支援してくれます。
なお、今回はTelemetry Contract自体のライフサイクルまでは扱っていません。実際には、一度依存可能とした定義をどうdeprecatedにし、どの程度の移行期間を設け、いつ削除するのか、といった時間軸も設計する必要があります。今回は、Weaverでそのライフサイクルをどう管理するかまでは検証していません。
テレメトリが出ているだけでは十分ではない
最初に挙げた例では、メトリクス名を変更してもアプリケーションは動き続け、テレメトリも送られていました。
それでも、その名前に依存していたダッシュボードやアラートは壊れました。
テレメトリが出ていることと、利用者との約束を守れていることは同じではありません。
まず、誰に何を約束するのかを決める。
そのうち機械で表現できる部分を定義にする。
そして、その定義を作るときだけでなく、変更するときや、送出されたテレメトリを確認するときにも使う。
今回Weaverを試してみて、Telemetry Contractのうち機械で扱える部分を、複数の工程で共通の基準として利用できることが見えてきました。
まとめ
テレメトリは、出力できていればそれでよいわけではありません。利用者がその名前や型、単位、属性に依存しているなら、それらを不用意に変えることでダッシュボードやアラートを壊してしまいます。
そこでこの記事では、producerがconsumerに「何を提供し、何を維持すると約束するか」をTelemetry Contractとして考えました。
その約束のうち機械で表現できる部分を定義にし、OpenTelemetry Weaver v0.26.1で実際に試してみると、定義の検証、独自ルールの評価、成果物の生成、変更の確認、送出されたテレメトリとの照合と、複数の工程で利用できました。
一方で、何をTelemetry Contractに含めるのか、どの変更を互換性のない変更とするのかはWeaverが決めてくれるわけではありません。変更を見つけること、互換性を判断すること、その判断をCIなどでどう運用するかも、それぞれ分けて考える必要があります。
実際に取り入れるなら、最初からすべてのテレメトリを対象にするのではなく、利用者からの依存関係が把握しやすい、少数のメトリクスなどから小さく試してみるのがよさそうです。
Weaverを試してみて見えてきたのは、Telemetry Contractそのものを自動化することではなく、自分たちで決めた約束のうち機械で扱える部分を、テレメトリの定義を基準にして複数の工程で継続的に扱えることでした。
参考資料
- OpenTelemetry Weaver
- Observability by Design: Unlocking Consistency with OpenTelemetry Weaver
- Weaver v0.26.1 release
- Weaver v0.26.1 CLI documentation
- Semantic Convention Definition Language v2
- OpenTelemetry Versioning and Stability
- OBI: Define the OBI v1 telemetry contract #1759
- SIG End User: Blueprint Proposal: SemConv Governance, Validation and Telemetry Contracts #332










![ファシリテーション・グラフィック[新版] 議論を「見える化」する技法 (日本経済新聞出版) ファシリテーション・グラフィック[新版] 議論を「見える化」する技法 (日本経済新聞出版)](https://m.media-amazon.com/images/I/41U6WwnGlzL._SL500_.jpg)

