Skip to main content

OpenTelemetry

The foundations-jdbc-otel module wires OpenTelemetry into the existing QueryListener mechanism, producing spans and metrics for every query. When no OTel SDK is configured, the listener does nothing.

Setup​

Add the foundations-jdbc-otel module to your project alongside the core library. Then create an OtelQueryListener and attach it to your transactor:

val config =
PgConfig.builder("localhost", 5432, "mydb", "user", "pass").build()

val telemetryConfig = TelemetryConfig.builder(config).build()

val otelListener: QueryListener =
OtelQueryListener.create(GlobalOpenTelemetry.get(), telemetryConfig)

val tx = Transactor.create(config).withListener(otelListener)

TelemetryConfig.builder(config) extracts the database system, host, port, and database name from the DatabaseConfig. You can also build it manually:

var telemetryConfig = TelemetryConfig.builder(DatabaseSystem.POSTGRESQL)
.serverAddress("db.example.com")
.serverPort(5432)
.dbNamespace("orders")
.build();

Span attributes​

Each query produces a span with attributes following OTel Semantic Conventions v1.39.0:

AttributeSource
db.system.nameFrom TelemetryConfig
db.query.textRaw SQL text (when recordQueryText is enabled)
server.address / server.portFrom config
db.namespaceDatabase name from config
error.typeException class on failure

The span name is the operation's .named() value if set, otherwise "DB query".

TelemetryConfig options​

OptionDefaultDescription
recordQueryTexttrueInclude db.query.text attribute

No ThreadLocal​

The listener uses backdated spans rather than ThreadLocal state. beforeQuery is a no-op. afterQuery and failedQuery each create a complete span by computing Instant.now() - elapsed. This is safe for virtual threads, nested queries, and composed listeners.

Pool metrics​

If you use HikariCP, register connection pool metrics:

val poolMetrics: PoolMetrics =
PoolMetrics.register(GlobalOpenTelemetry.get(), dataSource, Optional.of("main-pool"))

fun onShutdown() {
poolMetrics.close()
}

Metrics follow the OTel database pool semantic conventions:

MetricDescription
db.client.connection.countActive and idle connections (by state)
db.client.connection.pending_requestsThreads waiting for a connection
db.client.connection.maxMaximum pool size

Grafana dashboard​

A pre-built Grafana dashboard is included at foundations-jdbc-otel/grafana/foundations-jdbc-dashboard.json. Import it into Grafana and configure:

  • Tempo / Jaeger datasource for trace panels (latency percentiles, slow queries)
  • Prometheus datasource for metric panels (throughput, pool status)

Panels include:

  • Query latency p50/p95/p99 by operation
  • Error rate
  • Throughput by database system
  • Connection pool active/idle/pending
  • Pool usage percentage gauge
  • Slow queries table