Pyranid Logo

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.

Configuring Metrics

Use MetricsCollector::inMemoryInstance() for in-process counters in tests or ad-hoc inspection:

MetricsCollector metricsCollector = MetricsCollector.inMemoryInstance();

Database database = Database.with(dataSource, DatabaseType.POSTGRESQL)
  .metricsCollector(metricsCollector)
  .build();

MetricsCollector.Snapshot snapshot = metricsCollector.snapshot().orElseThrow();

Passing null or omitting Database.Builder::metricsCollector(...) disables metrics collection:

Database database = Database.with(dataSource, DatabaseType.POSTGRESQL)
  .metricsCollector(null)
  .build();

The collector is fixed at Database.Builder::build() time. To reconfigure metrics, build another Database.

Active Phase Time and Elapsed Time

StatementLog.getElapsedDuration() reports independently measured time from executor entry through resource cleanup, including transaction lock waits and time spent by a stream consumer while the cursor remains open. It excludes named SQL compilation and terminal statement metrics/logger callbacks. At EOF or explicit stream closure, the elapsed clock stops; application work after that point is excluded. Manually constructed logs leave this optional value absent unless the builder explicitly supplies it.

The getTotalDuration() method returns the sum of acquisition, preparation, execution, and mapping time. That phase sum excludes stream consumer idle time. The OpenTelemetry adapter uses this phase sum for db.client.operation.duration. Failed phases include the time spent before the failure.

Transaction Outcomes

Physical commit failures and logical transaction outcomes measure different events. MetricsCollector::didFailToCommitPhysicalTransaction(...) reports a failed physical commit attempt, including a failure in the success handling immediately after JDBC commit. It does not establish whether the database committed. Use transaction completion observation when you need the final database outcome together with the caller-visible failure.

A savepoint ownership conflict that blocks commit emits no commit-failure callback and does not increment physicalTransactionsCommitFailed(). The didExitTransactionClosure(...) callback and snapshot reflect the rollback outcome:

Rollback outcomeClosure outcomeSnapshot counter
SucceedsROLLED_BACKtransactionClosuresRolledBack()
Fails; transaction result is IN_DOUBTFAILEDtransactionClosuresFailed()

Notification Metrics

See Notifications for the public API, reconciliation model, and listener lifecycle.

Pyranid supports low-cardinality lifecycle callbacks for callback-scoped notification sessions:

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.

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 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.4.0</version>
</dependency>

This adapter version requires Pyranid 4.8.0. Add the com.pyranid:pyranid dependency explicitly because the adapter declares it as provided.

Wire it in like this:

OpenTelemetryMetricsCollector metricsCollector = OpenTelemetryMetricsCollector
  .withOpenTelemetry(openTelemetry)
  .poolName("primary")
  .namespace("orders")
  .recordCollectionName(false)
  .build();

Database database = Database.with(dataSource, 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.

The db.system.name attribute uses the database type required by Database.with(dataSource, databaseType) from the first transaction or statement event. DatabaseType.GENERIC maps to other_sql.

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

MetricStabilityTypeUnitNotes
db.client.operation.durationStableHistogramsStatement operation duration
db.client.response.returned_rowsDevelopmentHistogram{row}Emitted when Pyranid observes returned rows
db.client.connection.wait_timeDevelopmentHistogramsEmitted only when poolName(...) is configured
db.client.connection.use_timeDevelopmentHistogramsConnection 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

MetricTypeUnit
pyranid.statement.preparation.durationHistograms
pyranid.statement.execution.durationHistograms
pyranid.statement.mapping.durationHistograms
pyranid.statement.errorsCounter{statement}
pyranid.statement.batch.sizeHistogram{statement}
pyranid.statement.rows_affectedHistogram{row}
pyranid.transaction.closure.durationHistograms
pyranid.transaction.commit.durationHistograms
pyranid.transaction.rollback.durationHistograms
pyranid.transaction.physical.begin_failuresCounter{transaction}
pyranid.transaction.countCounter{transaction}
pyranid.transaction.activeUp-down counter{transaction}
pyranid.savepoint.operationsCounter{operation}
pyranid.fetchstream.durationHistograms
pyranid.fetchstream.rows_consumedHistogram{row}
pyranid.post_transaction.operationsCounter{operation}
pyranid.post_transaction.durationHistograms
pyranid.notification.session.open.durationHistograms
pyranid.notification.session.durationHistograms
pyranid.notification.session.activeUp-down counter{session}
pyranid.notification.batch.sizeHistogram{notification}
pyranid.notification.connection.lossesCounter{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.

Previous
Logging and Diagnostics