Telemetry (Tracing and Metrics)
ldbc supports distributed tracing and metrics collection compliant with OpenTelemetry Semantic Conventions. Internally it uses otel4s, and works on all platforms: JVM, JS, and Native.
When no OpenTelemetry backend is connected, tracing and metrics automatically become no-ops with zero performance impact.
Dependencies
ldbc-connector depends only on otel4s-core (the abstract API). To actually collect and export telemetry data, you need to add a backend implementation.
libraryDependencies ++= Seq(
// ldbc connector (includes otel4s-core)
"io.github.takapi327" %% "ldbc-connector" % "0.6.0",
// OpenTelemetry Java SDK backend
"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,
)
Setup
Set Tracer[F] and Meter[F] on 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
}
When using connection pooling, pass meter to the pooling method.
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)
)
Tracing
Overview
ldbc generates an OpenTelemetry span for each database operation. Spans are created through the exchange mechanism, which works in conjunction with a Mutex to guarantee serialization of MySQL protocol operations.
Span Names
Span names are generated following the OpenTelemetry Semantic Conventions priority:
db.query.summaryif available (e.g.,SELECT users){db.operation.name} {target}(e.g.,SELECT users){target}only- Fallback:
mysql
When extractMetadataFromQueryText in TelemetryConfig is true (default), operation names and table names are automatically extracted from SQL to generate dynamic span names. When false, fixed span names like Execute Statement are used.
The following is a list of the main spans generated by ldbc:
| Span Name | Description |
|---|---|
{operation} {table} |
Dynamically generated from SQL (e.g., SELECT users) |
Execute Statement |
Statement execution in fixed span name mode |
Execute Prepared Statement |
PreparedStatement execution in fixed span name mode |
Execute Statement Batch |
Statement batch execution |
Execute Prepared Statement Batch |
PreparedStatement batch execution |
Callable Statement |
CallableStatement execution |
Create Connection |
Connection establishment |
Close Connection |
Connection close |
Deallocate Prepared Statement |
Server-side PreparedStatement deallocation |
Commit |
Transaction commit |
Rollback |
Transaction rollback |
Span Attributes
Each span is annotated with the following attributes:
| Attribute | Requirement Level | Description | Example |
|---|---|---|---|
db.system.name |
Required |
DBMS name |
mysql |
server.address |
Recommended |
Hostname |
127.0.0.1 |
server.port |
Conditionally Required |
Port number |
3306 |
db.namespace |
Conditionally Required |
Database name |
mydb |
db.query.text |
Opt-In |
Executed SQL |
SELECT * FROM users WHERE id = ? |
db.collection.name |
Conditionally Required |
Table name |
users |
db.operation.name |
Conditionally Required |
Operation name |
SELECT |
db.operation.batch.size |
Conditionally Required |
Batch size (2 or more) |
10 |
db.mysql.thread_id |
— | MySQL thread ID |
42 |
db.mysql.version |
— | MySQL server version |
8.0.32 |
error.type |
Conditionally Required |
Error type (on failure only) |
SQLException |
Error Recording
When a database operation fails, the following is recorded on the span:
- The span status is set to
Error - The
error.typeattribute records the error class name recordExceptionrecords the stack trace as a span event- For MySQL
ERRPacketresponses, the error code and SQLState are also recorded as attributes
TelemetryConfig
Use TelemetryConfig to customize telemetry behavior.
import ldbc.connector.telemetry.TelemetryConfig
// Default settings (all enabled)
val default = TelemetryConfig.default
// Disable metadata extraction from SQL (use fixed span names)
val fixed = TelemetryConfig.withoutQueryTextExtraction
// Individual settings
val custom = TelemetryConfig.default
.withoutQueryTextExtraction // Use fixed span names
.withoutSanitization // Disable query text sanitization
.withoutInClauseCollapsing // Disable IN clause collapsing
| Setting | Default | Description |
|---|---|---|
extractMetadataFromQueryText |
true |
Extract operation name and table name from SQL to dynamically generate span names |
sanitizeNonParameterizedQueries |
true |
Replace literal values in non-parameterized queries with ? |
collapseInClauses |
true |
Collapse IN (?, ?, ?) to IN (?) |
Query Sanitization
When sanitizeNonParameterizedQueries is enabled, string literals, numbers, NULL values, etc. in the SQL recorded in the db.query.text attribute are replaced with ?. This prevents sensitive data from being included in traces.
-- Original SQL
SELECT * FROM users WHERE name = 'Alice' AND age > 25
-- After sanitization (value recorded in db.query.text)
SELECT * FROM users WHERE name = ? AND age > ?
IN Clause Collapsing
When collapseInClauses is enabled, span cardinality is kept low even when the number of parameters in IN clauses varies.
-- Original SQL
SELECT * FROM users WHERE id IN (?, ?, ?, ?)
-- After collapsing
SELECT * FROM users WHERE id IN (?)
Metrics
Overview
ldbc collects metrics compliant with the OpenTelemetry Database Metrics Semantic Conventions. Metrics are divided into two categories: operation metrics and connection pool metrics.
Operation Metrics
Automatically recorded for all Statement, PreparedStatement, and CallableStatement executions.
db.client.operation.duration
| Item | Details |
|---|---|
| Instrument Type | Histogram |
| Unit | s (seconds) |
| Stability | Stable |
| Description | Duration of database client operations |
| Bucket Boundaries | [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] |
Recorded for all of executeQuery, executeUpdate, and executeBatch. Cancelled operations are not recorded.
db.client.response.returned_rows
| Item | Details |
|---|---|
| Instrument Type | Histogram |
| Unit | {row} |
| Stability | Development |
| Description | Number of rows returned by the query |
| Bucket Boundaries | [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000] |
Recorded only for executeQuery-type methods. Not recorded for executeUpdate or executeBatch.
Limitation with Cursor Fetch
When useCursorFetch=true, a StreamingResultSet is used and rows are fetched incrementally in units of fetchSize. In this case, db.client.response.returned_rows records only the number of rows from the initial fetch. Rows fetched subsequently via next() calls are not included.
This is because the OpenTelemetry Semantic Conventions do not provide clear guidance for streaming/cursor-based ResultSets. Neither MySQL Connector/J nor the official OpenTelemetry JDBC instrumentation implement db.client.response.returned_rows at all, making this an industry-wide unresolved issue.
When useCursorFetch=false (the default), all rows are fetched at once and an accurate value is recorded.
Metrics Attributes
Operation metrics are annotated with the following low-cardinality attributes. Unlike trace attributes, high-cardinality attributes such as db.query.text and db.mysql.thread_id are excluded to keep metrics aggregation costs manageable.
| Attribute | Description | Example |
|---|---|---|
db.system.name |
DBMS name |
mysql |
server.address |
Hostname |
127.0.0.1 |
server.port |
Port number |
3306 |
db.namespace |
Database name |
mydb |
Connection Pool Metrics
Recorded only when using connection pooling (PooledDataSource). All metrics carry the db.client.connection.pool.name attribute.
Histogram Metrics
| Metric Name | Unit | Description |
|---|---|---|
db.client.connection.create_time |
s |
Time taken to create a new physical connection |
db.client.connection.wait_time |
s |
Time waiting to acquire a connection from the pool |
db.client.connection.use_time |
s |
Time between borrowing a connection and returning it |
Counter Metrics
| Metric Name | Unit | Description |
|---|---|---|
db.client.connection.timeouts |
{timeout} |
Number of connection acquisition timeouts |
Gauge Metrics (Observable)
The following metrics are reported at export time via OTel's BatchCallback.
| Metric Name | Unit | Description |
|---|---|---|
db.client.connection.count |
{connection} |
Current number of connections (state attribute: idle/used) |
db.client.connection.idle.max |
{connection} |
Maximum allowed number of idle connections |
db.client.connection.idle.min |
{connection} |
Minimum number of idle connections maintained |
db.client.connection.max |
{connection} |
Maximum allowed number of connections |
db.client.connection.pending_requests |
{request} |
Number of requests waiting for a connection |
Differences Between Traces and Metrics
ldbc uses different attributes for traces and metrics. This follows OpenTelemetry best practices to properly manage metrics cardinality.
| Attribute | Traces | Metrics | Reason |
|---|---|---|---|
db.system.name |
✅ | ✅ | Low cardinality |
server.address |
✅ | ✅ | Low cardinality |
server.port |
✅ | ✅ | Low cardinality |
db.namespace |
✅ | ✅ | Low cardinality |
db.query.text |
✅ | ❌ | High cardinality, differs per query |
db.collection.name |
✅ | ❌ | Sufficient as a span attribute |
db.mysql.thread_id |
✅ | ❌ | Varies per connection |
db.mysql.version |
✅ | ❌ | Version info is sufficient in spans |
error.type |
✅ | ❌ | Error details are tracked in spans |
Selective Use of Tracer/Meter
Tracer and Meter can be used independently.
// Tracing only (no metrics)
val ds = MySQLDataSource.build[IO]("localhost", 3306, "user")
.setTracer(tracer)
// Metrics only (no tracing)
val ds = MySQLDataSource.build[IO]("localhost", 3306, "user")
.setMeter(meter)
// Both
val ds = MySQLDataSource.build[IO]("localhost", 3306, "user")
.setTracer(tracer)
.setMeter(meter)
// Neither (no-op, zero performance impact)
val ds = MySQLDataSource.build[IO]("localhost", 3306, "user")