テレメトリ(トレースとメトリクス)
ldbcはOpenTelemetry Semantic Conventionsに準拠した分散トレーシングとメトリクス収集をサポートしています。内部ではotel4sを使用しており、JVM・JS・Native のすべてのプラットフォームで動作します。
OpenTelemetryバックエンドを接続しない場合、トレースとメトリクスは自動的にno-op(何もしない)となり、パフォーマンスへの影響はありません。
依存関係
ldbc-connectorはotel4s-core(抽象API)のみに依存しています。実際にテレメトリデータを収集・送信するには、バックエンド実装を追加する必要があります。
libraryDependencies ++= Seq(
// ldbcコネクタ(otel4s-coreを含む)
"io.github.takapi327" %% "ldbc-connector" % "0.6.0",
// OpenTelemetry Java SDKバックエンド
"org.typelevel" %% "otel4s-oteljava" % "0.15.1",
"io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.59.0" % Runtime,
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % "1.59.0" % Runtime,
)
セットアップ
Tracer[F]とMeter[F]をMySQLDataSourceに設定します。
import cats.effect.*
import io.opentelemetry.api.GlobalOpenTelemetry
import org.typelevel.otel4s.oteljava.OtelJava
import ldbc.connector.*
object Main extends IOApp.Simple:
override def run: IO[Unit] =
val resource = for
otel <- Resource
.eval(IO.delay(GlobalOpenTelemetry.get))
.evalMap(OtelJava.forAsync[IO])
tracer <- Resource.eval(otel.tracerProvider.get("my-app"))
meter <- Resource.eval(otel.meterProvider.get("my-app"))
datasource = MySQLDataSource
.build[IO]("127.0.0.1", 3306, "user")
.setPassword("password")
.setDatabase("mydb")
.setTracer(tracer)
.setMeter(meter)
connection <- datasource.getConnection
yield connection
resource.use { conn =>
conn.createStatement().flatMap(_.executeQuery("SELECT 1")).void
}
コネクションプーリング使用時はmeterをpoolingメソッドに渡します。
val pool = MySQLDataSource.pooling[IO](
config = MySQLConfig.default
.setHost("127.0.0.1")
.setPort(3306)
.setUser("user")
.setPassword("password")
.setDatabase("mydb"),
meter = Some(meter),
tracer = Some(tracer)
)
トレーシング
概要
ldbcは、データベース操作ごとにOpenTelemetryスパンを生成します。スパンはexchange機構を通じて作成され、MySQL プロトコルの直列性を保証するMutexと連携しています。
スパン名
スパン名はOpenTelemetry Semantic Conventionsの優先順位に従って生成されます:
db.query.summaryが利用可能な場合(例:SELECT users){db.operation.name} {target}(例:SELECT users){target}のみ- フォールバック:
mysql
TelemetryConfigのextractMetadataFromQueryTextがtrue(デフォルト)の場合、SQLからオペレーション名とテーブル名を自動抽出し、動的なスパン名を生成します。falseの場合はExecute Statementのような固定のスパン名を使用します。
以下はldbcが生成する主なスパンの一覧です:
| スパン名 | 説明 |
|---|---|
{operation} {table} |
SQL文から動的に生成(例:SELECT users) |
Execute Statement |
固定スパン名モード時のStatement実行 |
Execute Prepared Statement |
固定スパン名モード時のPreparedStatement実行 |
Execute Statement Batch |
Statementのバッチ実行 |
Execute Prepared Statement Batch |
PreparedStatementのバッチ実行 |
Callable Statement |
CallableStatementの実行 |
Create Connection |
コネクション確立 |
Close Connection |
コネクション切断 |
Deallocate Prepared Statement |
サーバー側PreparedStatementの解放 |
Commit |
トランザクションコミット |
Rollback |
トランザクションロールバック |
スパン属性
各スパンには以下の属性が付与されます:
| 属性 | 要求レベル | 説明 | 値の例 |
|---|---|---|---|
db.system.name |
Required |
DBMS名 |
mysql |
server.address |
Recommended |
ホスト名 |
127.0.0.1 |
server.port |
Conditionally Required |
ポート番号 |
3306 |
db.namespace |
Conditionally Required |
データベース名 |
mydb |
db.query.text |
Opt-In |
実行されたSQL |
SELECT * FROM users WHERE id = ? |
db.collection.name |
Conditionally Required |
テーブル名 |
users |
db.operation.name |
Conditionally Required |
操作名 |
SELECT |
db.operation.batch.size |
Conditionally Required |
バッチサイズ(2以上) |
10 |
db.mysql.thread_id |
— | MySQLスレッドID |
42 |
db.mysql.version |
— | MySQLサーバーバージョン |
8.0.32 |
error.type |
Conditionally Required |
エラー種別(失敗時のみ) |
SQLException |
エラー記録
データベース操作でエラーが発生した場合、スパンには以下が記録されます:
- スパンのステータスが
Errorに設定される error.type属性にエラーの型名が記録されるrecordExceptionによりスタックトレースがスパンイベントとして記録される- MySQLからの
ERRPacketの場合、エラーコードやSQLStateも属性として記録される
TelemetryConfig
TelemetryConfigを使用して、テレメトリの動作をカスタマイズできます。
import ldbc.connector.telemetry.TelemetryConfig
// デフォルト設定(すべて有効)
val default = TelemetryConfig.default
// SQLからのメタデータ抽出を無効にする(固定スパン名を使用)
val fixed = TelemetryConfig.withoutQueryTextExtraction
// 個別設定
val custom = TelemetryConfig.default
.withoutQueryTextExtraction // 固定スパン名を使用
.withoutSanitization // クエリテキストのサニタイズを無効化
.withoutInClauseCollapsing // IN句の折りたたみを無効化
| 設定 | デフォルト | 説明 |
|---|---|---|
extractMetadataFromQueryText |
true |
SQLからオペレーション名・テーブル名を抽出してスパン名を動的に生成する |
sanitizeNonParameterizedQueries |
true |
パラメータ化されていないクエリのリテラル値を?に置換する |
collapseInClauses |
true |
IN (?, ?, ?)をIN (?)に折りたたむ |
クエリのサニタイズ
sanitizeNonParameterizedQueriesが有効な場合、db.query.text属性に記録されるSQLから文字列リテラル、数値、NULL等が?に置換されます。これにより、センシティブなデータがトレースに含まれることを防ぎます。
-- 元のSQL
SELECT * FROM users WHERE name = 'Alice' AND age > 25
-- サニタイズ後(db.query.text に記録される値)
SELECT * FROM users WHERE name = ? AND age > ?
IN句の折りたたみ
collapseInClausesが有効な場合、IN句のパラメータ数が可変でもスパンのカーディナリティを抑えます。
-- 元のSQL
SELECT * FROM users WHERE id IN (?, ?, ?, ?)
-- 折りたたみ後
SELECT * FROM users WHERE id IN (?)
メトリクス
概要
ldbcはOpenTelemetry Database Metrics Semantic Conventionsに準拠したメトリクスを収集します。メトリクスはオペレーションメトリクスとコネクションプールメトリクスの2種類に分かれます。
オペレーションメトリクス
全てのStatement、PreparedStatement、CallableStatementの実行で自動的に記録されます。
db.client.operation.duration
| 項目 | 内容 |
|---|---|
| Instrument型 | Histogram |
| 単位 | s(秒) |
| 安定性 | Stable |
| 説明 | データベースクライアント操作の所要時間 |
| バケット境界 | [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] |
executeQuery、executeUpdate、executeBatchのすべてで記録されます。キャンセルされた操作は記録されません。
db.client.response.returned_rows
| 項目 | 内容 |
|---|---|
| Instrument型 | Histogram |
| 単位 | {row} |
| 安定性 | Development |
| 説明 | クエリによって返された行数 |
| バケット境界 | [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000] |
executeQuery系のメソッドでのみ記録されます。executeUpdateやexecuteBatchでは記録されません。
カーソルフェッチ使用時の制限事項
useCursorFetch=trueの場合、StreamingResultSetが使用され行はfetchSize単位で逐次取得されます。この場合、db.client.response.returned_rowsには初回フェッチ分の行数のみが記録されます。next()呼び出しで後続フェッチされた行は含まれません。
これはOpenTelemetry Semantic Conventionsがストリーミング/カーソルベースのResultSetに対する明確なガイダンスを提供していないことに起因します。MySQL Connector/JおよびOpenTelemetry公式JDBCインストルメンテーションはいずれもdb.client.response.returned_rows自体を未実装であり、業界全体で未解決の課題です。
useCursorFetch=false(デフォルト)の場合は全行が一度に取得されるため、正確な値が記録されます。
メトリクス属性
オペレーションメトリクスには以下の低カーディナリティ属性が付与されます。トレース属性とは異なり、メトリクスの集約コストを抑えるため高カーディナリティな属性(db.query.text、db.mysql.thread_id等)は除外されています。
| 属性 | 説明 | 値の例 |
|---|---|---|
db.system.name |
DBMS名 |
mysql |
server.address |
ホスト名 |
127.0.0.1 |
server.port |
ポート番号 |
3306 |
db.namespace |
データベース名 |
mydb |
コネクションプールメトリクス
コネクションプーリング(PooledDataSource)使用時にのみ記録されます。すべてのメトリクスはdb.client.connection.pool.name属性を持ちます。
Histogram メトリクス
| メトリクス名 | 単位 | 説明 |
|---|---|---|
db.client.connection.create_time |
s |
新しい物理コネクションの作成にかかった時間 |
db.client.connection.wait_time |
s |
プールからコネクションを取得するまでの待ち時間 |
db.client.connection.use_time |
s |
コネクションが借りられてから返却されるまでの時間 |
Counter メトリクス
| メトリクス名 | 単位 | 説明 |
|---|---|---|
db.client.connection.timeouts |
{timeout} |
コネクション取得のタイムアウト発生回数 |
ゲージメトリクス(Observable)
以下のメトリクスはOTelのBatchCallbackを通じてエクスポート時に報告されます。
| メトリクス名 | 単位 | 説明 |
|---|---|---|
db.client.connection.count |
{connection} |
現在のコネクション数(state属性:idle/used) |
db.client.connection.idle.max |
{connection} |
アイドルコネクションの最大許容数 |
db.client.connection.idle.min |
{connection} |
アイドルコネクションの最小維持数 |
db.client.connection.max |
{connection} |
コネクションの最大許容数 |
db.client.connection.pending_requests |
{request} |
コネクション取得待ちのリクエスト数 |
トレースとメトリクスの違い
ldbcではトレースとメトリクスで異なる属性を使用しています。これはOpenTelemetryのベストプラクティスに従い、メトリクスのカーディナリティを適切に管理するためです。
| 属性 | トレース | メトリクス | 理由 |
|---|---|---|---|
db.system.name |
✅ | ✅ | 低カーディナリティ |
server.address |
✅ | ✅ | 低カーディナリティ |
server.port |
✅ | ✅ | 低カーディナリティ |
db.namespace |
✅ | ✅ | 低カーディナリティ |
db.query.text |
✅ | ❌ | クエリごとに異なる高カーディナリティ |
db.collection.name |
✅ | ❌ | テーブル名はスパン属性として十分 |
db.mysql.thread_id |
✅ | ❌ | コネクションごとに変動する |
db.mysql.version |
✅ | ❌ | バージョン情報はスパンで十分 |
error.type |
✅ | ❌ | エラーの詳細はスパンで追跡 |
Tracer/Meterの選択的使用
TracerとMeterは独立して使用できます。
// トレースのみ(メトリクスなし)
val ds = MySQLDataSource.build[IO]("localhost", 3306, "user")
.setTracer(tracer)
// メトリクスのみ(トレースなし)
val ds = MySQLDataSource.build[IO]("localhost", 3306, "user")
.setMeter(meter)
// 両方
val ds = MySQLDataSource.build[IO]("localhost", 3306, "user")
.setTracer(tracer)
.setMeter(meter)
// どちらも設定しない(no-op、パフォーマンス影響なし)
val ds = MySQLDataSource.build[IO]("localhost", 3306, "user")