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.

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();

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

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:

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>

...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();

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

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