Core Concepts
Metrics Collection
Pyranid exposes a MetricsCollector hook for low-cardinality database metrics. It is separate from StatementLogger: use the logger for diagnostics and SQL detail, and use metrics collectors for counters, gauges, and histograms.
Metrics collection is disabled by default. Collector failures are swallowed by Pyranid so metrics cannot change database behavior.
Core Configuration
Use MetricsCollector::inMemoryInstance() for in-process counters in tests or ad-hoc inspection:
MetricsCollector metricsCollector = MetricsCollector.inMemoryInstance();
Database database = Database.withDataSource(dataSource)
.metricsCollector(metricsCollector)
.build();
MetricsCollector.Snapshot snapshot = metricsCollector.snapshot().orElseThrow();
MetricsCollector metricsCollector = MetricsCollector.inMemoryInstance();
Database database = Database.withDataSource(dataSource)
.metricsCollector(metricsCollector)
.build();
MetricsCollector.Snapshot snapshot = metricsCollector.snapshot().orElseThrow();
Passing null or omitting Database.Builder::metricsCollector(...) disables metrics collection:
Database database = Database.withDataSource(dataSource)
.metricsCollector(null)
.build();
Database database = Database.withDataSource(dataSource)
.metricsCollector(null)
.build();
The collector is fixed at Database.Builder::build() time. To reconfigure metrics, build another Database.
Notification Metrics
See Notifications for the public API, reconciliation model, and listener lifecycle.
Pyranid supports low-cardinality lifecycle callbacks for callback-scoped notification sessions:
willOpenNotificationSession(...)didOpenNotificationSession(...)didFailToOpenNotificationSession(...)didDeliverNotificationBatch(...)didLoseNotificationConnection(...)didCloseNotificationSession(...)
An invocation that reaches willOpenNotificationSession(...) produces exactly one open or failed-open event. An opened session produces exactly one close event after cleanup, with an outcome of CALLBACK_RETURNED, INTERRUPTED, or FAILED. Delivered-batch metrics report only a count; Pyranid never puts channel names or payload contents into notification metrics.
The in-memory collector exposes a separate MetricsCollector.NotificationSnapshot through notificationSnapshot(). Its seven counters cover sessions started, opened, callback-returned, interrupted, and failed, plus delivered batches and delivered notifications. Notification counters deliberately do not change the existing MetricsCollector.Snapshot record shape.
These are cumulative counters, not current-state gauges: sessionsOpened() does not mean that a session is still open. The CALLBACK_RETURNED outcome means the callback and cleanup completed normally, INTERRUPTED means cooperative interruption ended an opened session cleanly, and FAILED includes real callback, transport, or cleanup failure. A sustained zero delivery count alongside session starts/opens is a reason to inspect your application's traffic and database topology; it is not itself proof of a broken or healthy quiet connection.
Metrics belong to the Database instance that performs the work. Session lifecycle metrics go to the collector configured on the instance whose withNotificationSession(...) method is invoked; send and ordinary statement metrics go to the executing instance. If your application uses separate listener and ordinary-work Database instances, pass the same thread-safe collector to both if their metrics should be aggregated.
OpenTelemetry
Use on the optional pyranid-otel artifact if you'd like to export metrics to OpenTelemetry:
<dependency>
<groupId>com.pyranid</groupId>
<artifactId>pyranid-otel</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.pyranid</groupId>
<artifactId>pyranid-otel</artifactId>
<version>1.3.0</version>
</dependency>
...and wire it in like this:
OpenTelemetryMetricsCollector metricsCollector = OpenTelemetryMetricsCollector
.withOpenTelemetry(openTelemetry)
.poolName("primary")
.namespace("orders")
.recordCollectionName(false)
.build();
Database database = Database.withDataSource(dataSource)
.databaseType(DatabaseType.POSTGRESQL)
.metricsCollector(metricsCollector)
.build();
OpenTelemetryMetricsCollector metricsCollector = OpenTelemetryMetricsCollector
.withOpenTelemetry(openTelemetry)
.poolName("primary")
.namespace("orders")
.recordCollectionName(false)
.build();
Database database = Database.withDataSource(dataSource)
.databaseType(DatabaseType.POSTGRESQL)
.metricsCollector(metricsCollector)
.build();
Specifying poolName(null) or leaving it unset skips pool-name-gated connection metrics. Setting namespace(null) omits db.namespace; Pyranid does not look up a namespace from JDBC metadata. Using recordCollectionName(false) is the default because collection/table names can be too high-cardinality for some deployments.
If first-use transaction metrics need a precise db.system.name, configure Database.Builder::databaseType(...). Otherwise lazy detection may report other_sql for transaction-only events before a statement has detected the database type.
The pyranid-otel adapter exports notification-session opening, lifetime, active-session, delivery-batch, and connection-loss metrics. The batch-size histogram's aggregation count is the number of delivered batches and its sum is the number of delivered notifications. Notification sends continue through Pyranid's ordinary statement metrics because sending is an ordinary database operation.
OpenTelemetry Metrics
| Metric | Stability | Type | Unit | Notes |
|---|---|---|---|---|
db.client.operation.duration | Stable | Histogram | s | Statement operation duration |
db.client.response.returned_rows | Development | Histogram | {row} | Emitted when Pyranid observes returned rows |
db.client.connection.wait_time | Development | Histogram | s | Emitted only when poolName(...) is configured |
db.client.connection.use_time | Development | Histogram | s | Connection hold time; not exact physical transaction lifetime |
Connection pool internals such as idle count, max count, pending requests, timeouts, and create time are intentionally not emitted by Pyranid. Use your pool or proxy exporter for those.
Pyranid Metrics
| Metric | Type | Unit |
|---|---|---|
pyranid.statement.preparation.duration | Histogram | s |
pyranid.statement.execution.duration | Histogram | s |
pyranid.statement.mapping.duration | Histogram | s |
pyranid.statement.errors | Counter | {statement} |
pyranid.statement.batch.size | Histogram | {statement} |
pyranid.statement.rows_affected | Histogram | {row} |
pyranid.transaction.closure.duration | Histogram | s |
pyranid.transaction.commit.duration | Histogram | s |
pyranid.transaction.rollback.duration | Histogram | s |
pyranid.transaction.physical.begin_failures | Counter | {transaction} |
pyranid.transaction.count | Counter | {transaction} |
pyranid.transaction.active | Up-down counter | {transaction} |
pyranid.savepoint.operations | Counter | {operation} |
pyranid.fetchstream.duration | Histogram | s |
pyranid.fetchstream.rows_consumed | Histogram | {row} |
pyranid.post_transaction.operations | Counter | {operation} |
pyranid.post_transaction.duration | Histogram | s |
pyranid.notification.session.open.duration | Histogram | s |
pyranid.notification.session.duration | Histogram | s |
pyranid.notification.session.active | Up-down counter | {session} |
pyranid.notification.batch.size | Histogram | {notification} |
pyranid.notification.connection.losses | Counter | {connection} |
Attribute Policy
OpenTelemetryMetricsCollector keeps default attributes bounded. It emits db.system.name, db.operation.name, configured db.namespace, the full SQLSTATE as db.response.status_code on failures by default, and error.type using an underlying SQLException or otherwise the deepest available application cause. Configure recordFullSqlState(false) to emit only the two-character SQLSTATE class.
Every notification instrument includes db.system.name and includes configured db.namespace and db.client.connection.pool.name when present. Session-open duration adds pyranid.notification.session.open_outcome with success or failure; session duration adds pyranid.notification.session.outcome with callback_returned, interrupted, or failed. The error.type attribute is included for failed session opens, connection losses, and failed session closes when a throwable is available.
Notification metrics never include channel names, payloads, session UUIDs, or backend process identifiers. This keeps the default attribute set bounded and avoids exporting application data.
Statement::getId() from Query::id(...) is not emitted by the OTel collector. It remains available to custom collectors and StatementLogger.
Snapshot Semantics
The default MetricsCollector::disabledInstance() collector collects nothing, so snapshot() and notificationSnapshot() return Optional::empty().
The opt-in in-memory collector returned by MetricsCollector::inMemoryInstance() maintains local counters and returns nonempty optionals from both methods: MetricsCollector.Snapshot from snapshot() and MetricsCollector.NotificationSnapshot from notificationSnapshot(). These snapshots are intended for tests or local inspection. MetricsCollector::reset() is best-effort; concurrent updates may race with reset operations.
OpenTelemetryMetricsCollector does not maintain a parallel set of in-process counters. Its snapshot() and notificationSnapshot() methods therefore return Optional::empty(); read its metrics through the configured OpenTelemetry SDK or exporter instead.

