OpenTelemetry 公式の Instrumentation ライブラリを使っていると、Requestsのspanの名前が "GET" だけだったり、 mysqlclient のspanが "SELECT" だけだったりします。
たとえばGrafanaでトレースを見ていると "SELECT" ばかりが何個も並んでいて、一つ一つクリックして attributes の db.statement を確認していかないとどのクエリを実行しているのか分かりません。非常に面倒です。
Flaskのspanであれば "GET /user/<id:int>" のように表示してくれるのに、なぜRequestsのspanの名前はtable名やpathを書いてくれないのでしょうか。それは OpenTelemetry の Semantic conventions でそう推奨されているからです。
HTTP span の Convention:
HTTP span names SHOULD be {method} {target} if there is a (low-cardinality) target available. If there is no (low-cardinality) {target} available, HTTP span names SHOULD be {method}.
Tracing API の Spec に書かれている Span name についての説明:
The span name concisely identifies the work represented by the Span, for example, an RPC method name, a function name, or the name of a subtask or stage within a larger computation. The span name SHOULD be the most general string that identifies a (statistically) interesting class of Spans, rather than individual Span instances while still being human-readable.
...
Span Name Guidance getToo general get_account/42Too specific get_accountGood, and account_id=42 would make a nice Span attribute get_account/{accountId}Also good (using the "HTTP route")
Flaskのrouteはほぼworkと1対1対応しているので span name に target を含められるのですが、RequestsやWSGIではPATHのどの部分が work に対応している部分なのかがわからないので target が含められていないのです。
また、mysqlclient の場合は次のようなルールにより target が含められていません。
The span name SHOULD be {db.query.summary} if a summary is available.
If no summary is available, the span name SHOULD be {db.operation.name} {target} provided that a (low-cardinality) db.operation.name is available (see below for the exact definition of the {target} placeholder).
...
The {target} SHOULD describe the entity that the operation is performed against and SHOULD adhere to one of the following values, provided they are accessible:
- db.collection.name SHOULD be used for operations on a specific database collection.
...
[3] db.collection.name: It is RECOMMENDED to capture the value as provided by the application without attempting to do any case normalization.
The collection name SHOULD NOT be extracted from db.query.text, when the database system supports query text with multiple collections in non-batch operations.
つまり、 "SELECT user" のようにテーブル名 (db.collection.name) を target として span name に含めることは Convention で指定されているものの、そのテーブル名をSQLから文字列パースして取り出すことは禁止されているのです。
DB DriverやHTTP Clientのようなアプリケーションレイヤーより下のライブラリではこれらのルールを守りながら使いやすい Span name を提供することは難しいです。どちらも "target" を呼び出し元から受け取るように引数を増やして、 opentelemtry-instrumentation-xxx のようなhookライブラリから利用できるところに保存するか、ライブラリ自身がotel対応する必要が出てくるでしょう。
しかし、自身のアプリケーションのトレースを取得するのであれば、別にこれらのConventionを厳密に守る必要はありません。ログと違いトレースは30日やそこらで削除することが多いので、今使っているTrace Viewerで扱いやすいようにカスタマイズしてしまうのがいいでしょう。
span name のカスタマイズは Agent (OTLP Collector) でもできると思いますが、ここでは Python でカスタマイズする例を書いておきます。 カスタマイズ対象は mysqlclient と urllib3 です。(Reuqestsではなくurllib3を使っているのは、今関わっているプロジェクトではNiquestsを選定して、 opentelemetry-instrumentation-requests が使えなくなったからです。)
# opentelemetry-instrumentation-mysqlclient のカスタマイズ例 # span name にコマンドだけでなくテーブル名も表示する import re from opentelemetry.instrumentation.dbapi import CursorTracer from opentelemetry.instrumentation.mysqlclient import MySQLClientInstrumentor def get_operation_name(self, cursor, args): if args and isinstance(args[0], str): # Strip leading comments so we get the operation name. sql = self._leading_comment_remover.sub("", args[0]) command = sql.split(maxsplit=1)[0].upper() pattern = r"\b(FROM|INTO|UPDATE)\s+[`]?([\w]+)[`]?\b" if m := re.search(pattern, sql, re.IGNORECASE): return f"{command} {m.group(2)}" else: return command return "" CursorTracer.get_operation_name = get_operation_name MySQLClientInstrumentor().instrument() # urllib3 (Niquests) のカスタマイズ例 # span 名にpathを含める import urllib.parse from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor def request_hook(span, pool, request_info): url = urllib.parse.urlparse(request_info.url) span.update_name(f"{request_info.method} {url.path}") URLLib3Instrumentor().instrument(request_hook=request_hook)
SQLからtable名を取り出す部分はかなり適当ですが、複雑なクエリを使っていないのでこれでも十分に便利になりました。