001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.ThreadSafe; 023import java.time.Duration; 024import java.util.Optional; 025 026/** 027 * Contract for collecting operational metrics from Pyranid. 028 * <p> 029 * The default collector is {@link #disabledInstance()}, which performs no work. {@link #inMemoryInstance()} returns a 030 * fresh counter-only collector useful for tests and ad-hoc inspection through {@link #snapshot()}. 031 * <p> 032 * Implementations must be thread-safe, non-blocking, and failure-tolerant. Pyranid catches and discards collector 033 * exceptions so metrics collection cannot affect database behavior. 034 * 035 * @author <a href="https://www.revetkn.com">Mark Allen</a> 036 * @since 4.2.0 037 */ 038@ThreadSafe 039public interface MetricsCollector { 040 /** 041 * Called immediately before Pyranid attempts to acquire a statement-scoped {@link java.sql.Connection}. 042 * <p> 043 * Statement-scoped connections are used for standalone statement execution and stream opening outside a physical 044 * {@link Transaction}. 045 * 046 * @param ctx statement context for the operation that needs a connection 047 */ 048 default void willAcquireStatementConnection(@NonNull StatementContext<?> ctx) { 049 // No-op by default 050 } 051 052 /** 053 * Called after Pyranid successfully acquires a statement-scoped {@link java.sql.Connection}. 054 * 055 * @param ctx statement context for the operation that acquired a connection 056 * @param acquisitionDuration elapsed time spent acquiring the connection 057 */ 058 default void didAcquireStatementConnection(@NonNull StatementContext<?> ctx, 059 @NonNull Duration acquisitionDuration) { 060 // No-op by default 061 } 062 063 /** 064 * Called after Pyranid fails to acquire a statement-scoped {@link java.sql.Connection}. 065 * 066 * @param ctx statement context for the operation that needed a connection 067 * @param databaseType database type known at the time of failure 068 * @param acquisitionDuration elapsed time spent attempting to acquire the connection 069 * @param throwable failure that prevented connection acquisition 070 */ 071 default void didFailToAcquireStatementConnection(@NonNull StatementContext<?> ctx, 072 @NonNull DatabaseType databaseType, 073 @NonNull Duration acquisitionDuration, 074 @NonNull Throwable throwable) { 075 // No-op by default 076 } 077 078 /** 079 * Called after Pyranid successfully releases a statement-scoped {@link java.sql.Connection}. 080 * 081 * @param ctx statement context for the operation that used the connection 082 * @param heldDuration elapsed time between successful acquisition and release 083 */ 084 default void didReleaseStatementConnection(@NonNull StatementContext<?> ctx, 085 @NonNull Duration heldDuration) { 086 // No-op by default 087 } 088 089 /** 090 * Called after Pyranid fails to release a statement-scoped {@link java.sql.Connection}. 091 * 092 * @param ctx statement context for the operation that used the connection 093 * @param heldDuration elapsed time between successful acquisition and the failed release attempt 094 * @param throwable failure that prevented connection release 095 */ 096 default void didFailToReleaseStatementConnection(@NonNull StatementContext<?> ctx, 097 @NonNull Duration heldDuration, 098 @NonNull Throwable throwable) { 099 // No-op by default 100 } 101 102 /** 103 * Called immediately before Pyranid attempts to acquire the JDBC connection backing a physical transaction. 104 * <p> 105 * Pyranid transactions acquire their JDBC connection lazily, so this callback is emitted only when transaction work 106 * first needs database access. 107 * 108 * @param transaction transaction that needs a physical connection 109 * @param databaseType database type known at the time of acquisition 110 */ 111 default void willAcquireTransactionConnection(@NonNull Transaction transaction, 112 @NonNull DatabaseType databaseType) { 113 // No-op by default 114 } 115 116 /** 117 * Called after Pyranid successfully acquires the JDBC connection backing a physical transaction. 118 * 119 * @param transaction transaction that acquired the connection 120 * @param databaseType database type known at the time of acquisition 121 * @param acquisitionDuration elapsed time spent acquiring the connection 122 */ 123 default void didAcquireTransactionConnection(@NonNull Transaction transaction, 124 @NonNull DatabaseType databaseType, 125 @NonNull Duration acquisitionDuration) { 126 // No-op by default 127 } 128 129 /** 130 * Called after Pyranid fails to acquire the JDBC connection backing a physical transaction. 131 * 132 * @param transaction transaction that needed a connection 133 * @param databaseType database type known at the time of failure 134 * @param acquisitionDuration elapsed time spent attempting to acquire the connection 135 * @param throwable failure that prevented connection acquisition 136 */ 137 default void didFailToAcquireTransactionConnection(@NonNull Transaction transaction, 138 @NonNull DatabaseType databaseType, 139 @NonNull Duration acquisitionDuration, 140 @NonNull Throwable throwable) { 141 // No-op by default 142 } 143 144 /** 145 * Called after Pyranid successfully releases the JDBC connection backing a physical transaction. 146 * 147 * @param transaction transaction that owned the connection 148 * @param databaseType database type known at the time of release 149 * @param heldDuration elapsed time between successful acquisition and release 150 */ 151 default void didReleaseTransactionConnection(@NonNull Transaction transaction, 152 @NonNull DatabaseType databaseType, 153 @NonNull Duration heldDuration) { 154 // No-op by default 155 } 156 157 /** 158 * Called after Pyranid fails to release the JDBC connection backing a physical transaction. 159 * 160 * @param transaction transaction that owned the connection 161 * @param databaseType database type known at the time of failure 162 * @param heldDuration elapsed time between successful acquisition and the failed release attempt 163 * @param throwable failure that prevented connection release 164 */ 165 default void didFailToReleaseTransactionConnection(@NonNull Transaction transaction, 166 @NonNull DatabaseType databaseType, 167 @NonNull Duration heldDuration, 168 @NonNull Throwable throwable) { 169 // No-op by default 170 } 171 172 /** 173 * Called when Pyranid enters a closure-based transaction. 174 * <p> 175 * This is a logical transaction event and may occur even if user code never performs database work and no physical JDBC 176 * transaction is begun. 177 * 178 * @param transaction transaction passed to the closure through Pyranid APIs 179 * @param isolation requested transaction isolation 180 * @param databaseType database type known at transaction entry 181 */ 182 default void didEnterTransactionClosure(@NonNull Transaction transaction, 183 @NonNull TransactionIsolation isolation, 184 @NonNull DatabaseType databaseType) { 185 // No-op by default 186 } 187 188 /** 189 * Called when Pyranid exits a closure-based transaction. 190 * 191 * @param transaction transaction that is exiting 192 * @param outcome logical transaction outcome 193 * @param databaseType database type known at transaction exit 194 * @param logicalDuration elapsed time from transaction entry to exit, including user code, physical transaction work, 195 * cleanup, and post-transaction operations 196 * @param thrown exception or error that caused the transaction to exit abnormally, or {@code null} when the 197 * transaction completed without a reported failure 198 */ 199 default void didExitTransactionClosure(@NonNull Transaction transaction, 200 @NonNull TransactionClosureOutcome outcome, 201 @NonNull DatabaseType databaseType, 202 @NonNull Duration logicalDuration, 203 @Nullable Throwable thrown) { 204 // No-op by default 205 } 206 207 /** 208 * Called after Pyranid successfully begins a physical JDBC transaction. 209 * 210 * @param transaction transaction whose physical JDBC transaction began 211 * @param isolation requested transaction isolation 212 * @param databaseType database type known at transaction begin 213 */ 214 default void didBeginPhysicalTransaction(@NonNull Transaction transaction, 215 @NonNull TransactionIsolation isolation, 216 @NonNull DatabaseType databaseType) { 217 // No-op by default 218 } 219 220 /** 221 * Called after Pyranid fails while beginning a physical JDBC transaction. 222 * 223 * @param transaction transaction whose physical JDBC transaction failed to begin 224 * @param isolation requested transaction isolation 225 * @param phase begin phase that failed 226 * @param databaseType database type known at the time of failure 227 * @param throwable failure that prevented the physical transaction from beginning 228 */ 229 default void didFailToBeginPhysicalTransaction(@NonNull Transaction transaction, 230 @NonNull TransactionIsolation isolation, 231 @NonNull PhysicalTransactionBeginFailurePhase phase, 232 @NonNull DatabaseType databaseType, 233 @NonNull Throwable throwable) { 234 // No-op by default 235 } 236 237 /** 238 * Called after Pyranid successfully commits a physical JDBC transaction. 239 * 240 * @param transaction transaction that committed 241 * @param databaseType database type known at commit time 242 * @param physicalDuration elapsed time between physical transaction begin and commit 243 */ 244 default void didCommitPhysicalTransaction(@NonNull Transaction transaction, 245 @NonNull DatabaseType databaseType, 246 @NonNull Duration physicalDuration) { 247 // No-op by default 248 } 249 250 /** 251 * Called after Pyranid fails to commit a physical JDBC transaction. 252 * 253 * @param transaction transaction whose commit failed 254 * @param databaseType database type known at commit time 255 * @param physicalDuration elapsed time between physical transaction begin and the failed commit attempt 256 * @param throwable failure that prevented commit from completing normally 257 */ 258 default void didFailToCommitPhysicalTransaction(@NonNull Transaction transaction, 259 @NonNull DatabaseType databaseType, 260 @NonNull Duration physicalDuration, 261 @NonNull Throwable throwable) { 262 // No-op by default 263 } 264 265 /** 266 * Called after Pyranid successfully rolls back a physical JDBC transaction. 267 * 268 * @param transaction transaction that rolled back 269 * @param databaseType database type known at rollback time 270 * @param physicalDuration elapsed time between physical transaction begin and rollback 271 */ 272 default void didRollbackPhysicalTransaction(@NonNull Transaction transaction, 273 @NonNull DatabaseType databaseType, 274 @NonNull Duration physicalDuration) { 275 // No-op by default 276 } 277 278 /** 279 * Called after Pyranid fails to roll back a physical JDBC transaction. 280 * 281 * @param transaction transaction whose rollback failed 282 * @param databaseType database type known at rollback time 283 * @param physicalDuration elapsed time between physical transaction begin and the failed rollback attempt 284 * @param throwable failure that prevented rollback from completing normally 285 */ 286 default void didFailToRollbackPhysicalTransaction(@NonNull Transaction transaction, 287 @NonNull DatabaseType databaseType, 288 @NonNull Duration physicalDuration, 289 @NonNull Throwable throwable) { 290 // No-op by default 291 } 292 293 /** 294 * Called after a post-transaction operation runs. 295 * 296 * @param transaction transaction whose post-transaction operation ran 297 * @param result transaction result visible to the post-transaction operation 298 * @param databaseType database type known at post-transaction execution time 299 * @param duration elapsed time spent running the post-transaction operation 300 * @param throwable failure thrown by the post-transaction operation, or {@code null} when it completed normally 301 */ 302 default void didRunPostTransactionOperation(@NonNull Transaction transaction, 303 @NonNull TransactionResult result, 304 @NonNull DatabaseType databaseType, 305 @NonNull Duration duration, 306 @Nullable Throwable throwable) { 307 // No-op by default 308 } 309 310 /** 311 * Called after Pyranid successfully creates a transaction savepoint. 312 * 313 * @param transaction transaction that created the savepoint 314 * @param databaseType database type known at savepoint creation time 315 */ 316 default void didCreateSavepoint(@NonNull Transaction transaction, 317 @NonNull DatabaseType databaseType) { 318 // No-op by default 319 } 320 321 /** 322 * Called after Pyranid successfully rolls back to a transaction savepoint. 323 * 324 * @param transaction transaction that rolled back to the savepoint 325 * @param databaseType database type known at savepoint rollback time 326 */ 327 default void didRollbackToSavepoint(@NonNull Transaction transaction, 328 @NonNull DatabaseType databaseType) { 329 // No-op by default 330 } 331 332 /** 333 * Called after Pyranid successfully releases a transaction savepoint. 334 * 335 * @param transaction transaction that released the savepoint 336 * @param databaseType database type known at savepoint release time 337 */ 338 default void didReleaseSavepoint(@NonNull Transaction transaction, 339 @NonNull DatabaseType databaseType) { 340 // No-op by default 341 } 342 343 /** 344 * Called immediately before Pyranid executes a statement. 345 * 346 * @param ctx statement context for the statement about to execute 347 */ 348 default void willExecuteStatement(@NonNull StatementContext<?> ctx) { 349 // No-op by default 350 } 351 352 /** 353 * Called after Pyranid successfully executes a statement. 354 * 355 * @param ctx statement context for the executed statement 356 * @param statementLog diagnostic statement log for the execution 357 * @param result statement execution result 358 */ 359 default void didExecuteStatement(@NonNull StatementContext<?> ctx, 360 @NonNull StatementLog<?> statementLog, 361 @NonNull StatementResult result) { 362 // No-op by default 363 } 364 365 /** 366 * Called after Pyranid fails to execute a statement. 367 * 368 * @param ctx statement context for the failed statement 369 * @param statementLog diagnostic statement log for the failed execution 370 * @param databaseType database type known at statement failure time 371 * @param throwable failure thrown while executing the statement 372 */ 373 default void didFailToExecuteStatement(@NonNull StatementContext<?> ctx, 374 @NonNull StatementLog<?> statementLog, 375 @NonNull DatabaseType databaseType, 376 @NonNull Throwable throwable) { 377 // No-op by default 378 } 379 380 /** 381 * Called immediately before Pyranid opens a streaming statement. 382 * 383 * @param ctx statement context for the stream about to open 384 */ 385 default void willOpenStream(@NonNull StatementContext<?> ctx) { 386 // No-op by default 387 } 388 389 /** 390 * Called after Pyranid successfully opens a streaming statement. 391 * 392 * @param ctx statement context for the opened stream 393 * @param openDuration elapsed time spent opening the stream 394 */ 395 default void didOpenStream(@NonNull StatementContext<?> ctx, 396 @NonNull Duration openDuration) { 397 // No-op by default 398 } 399 400 /** 401 * Called after Pyranid fails to open a streaming statement. 402 * 403 * @param ctx statement context for the stream that failed to open 404 * @param databaseType database type known at stream-open failure time 405 * @param openDuration elapsed time spent attempting to open the stream 406 * @param throwable failure that prevented the stream from opening 407 */ 408 default void didFailToOpenStream(@NonNull StatementContext<?> ctx, 409 @NonNull DatabaseType databaseType, 410 @NonNull Duration openDuration, 411 @NonNull Throwable throwable) { 412 // No-op by default 413 } 414 415 /** 416 * Called after an opened stream reaches a terminal state. 417 * <p> 418 * Pyranid does not emit this callback for streams that fail before they open; those failures are reported through 419 * {@link #didFailToOpenStream(StatementContext, DatabaseType, Duration, Throwable)}. 420 * 421 * @param ctx statement context for the stream 422 * @param outcome terminal stream outcome 423 * @param rowsConsumed number of rows consumed from the stream 424 * @param streamDuration elapsed time between stream-open start and terminal close handling 425 * @param throwable failure associated with the terminal outcome, or {@code null} when no failure is available 426 */ 427 default void didCloseStream(@NonNull StatementContext<?> ctx, 428 @NonNull StreamTerminalOutcome outcome, 429 @NonNull Long rowsConsumed, 430 @NonNull Duration streamDuration, 431 @Nullable Throwable throwable) { 432 // No-op by default 433 } 434 435 /** 436 * Returns a counter snapshot if this collector supports in-process inspection. 437 * <p> 438 * Implementations may read counters independently without cross-counter atomicity. Callers that need invariant-consistent 439 * snapshots should drain in-flight work before reading. 440 * 441 * @return a metrics snapshot, if supported 442 */ 443 @NonNull 444 default Optional<Snapshot> snapshot() { 445 return Optional.empty(); 446 } 447 448 /** 449 * Best-effort reset for test and ad-hoc use. 450 * <p> 451 * Concurrent updates may race with reset operations. Production rolling-window semantics should be implemented outside 452 * this interface. 453 */ 454 default void reset() { 455 // No-op by default 456 } 457 458 /** 459 * Returns the shared no-op metrics collector. 460 * 461 * @return no-op metrics collector used when metrics collection is disabled 462 */ 463 @NonNull 464 static MetricsCollector disabledInstance() { 465 return DisabledMetricsCollector.defaultInstance(); 466 } 467 468 /** 469 * Creates a fresh in-memory counter collector. 470 * <p> 471 * This collector is intended for tests and lightweight local inspection through {@link #snapshot()}. 472 * 473 * @return new in-memory metrics collector 474 */ 475 @NonNull 476 static MetricsCollector inMemoryInstance() { 477 return InMemoryMetricsCollector.defaultInstance(); 478 } 479 480 /** 481 * Logical outcome for a closure-based transaction. 482 */ 483 enum TransactionClosureOutcome { 484 /** 485 * The transaction used a physical JDBC transaction and commit completed normally. 486 */ 487 COMMITTED, 488 489 /** 490 * The transaction used a physical JDBC transaction and rollback completed normally. 491 */ 492 ROLLED_BACK, 493 494 /** 495 * The transaction closure exited without ever acquiring a JDBC connection. 496 */ 497 NO_PHYSICAL_TX, 498 499 /** 500 * The transaction failed before Pyranid could report a normal commit or rollback outcome. 501 */ 502 FAILED 503 } 504 505 /** 506 * Physical transaction begin phase that failed. 507 */ 508 enum PhysicalTransactionBeginFailurePhase { 509 /** 510 * Connection acquisition from the configured {@link javax.sql.DataSource} failed. 511 */ 512 ACQUIRE_CONNECTION, 513 514 /** 515 * Reading the connection's initial autocommit setting failed. 516 */ 517 READ_INITIAL_AUTOCOMMIT, 518 519 /** 520 * Reading the connection's initial transaction isolation failed. 521 */ 522 READ_INITIAL_ISOLATION, 523 524 /** 525 * Reading the connection's initial read-only setting failed. 526 */ 527 READ_INITIAL_READ_ONLY, 528 529 /** 530 * Disabling autocommit for the transaction failed. 531 */ 532 SET_AUTOCOMMIT_FALSE, 533 534 /** 535 * Applying the requested transaction isolation failed. 536 */ 537 SET_ISOLATION, 538 539 /** 540 * Applying the requested read-only setting failed. 541 */ 542 SET_READ_ONLY 543 } 544 545 /** 546 * Terminal outcome for an opened stream. 547 */ 548 enum StreamTerminalOutcome { 549 /** 550 * Stream iteration reached the end of the result set and cleanup completed without an iteration or callback failure. 551 */ 552 COMPLETED_NORMALLY, 553 554 /** 555 * The stream was closed before all rows were consumed. 556 */ 557 EARLY_CLOSE, 558 559 /** 560 * The caller-provided stream callback failed. 561 */ 562 CALLBACK_FAILURE, 563 564 /** 565 * Result-set iteration failed while the stream was open. 566 */ 567 ITERATION_FAILURE, 568 569 /** 570 * The stream failed before it opened. 571 * <p> 572 * Pyranid normally reports this through {@link #didFailToOpenStream(StatementContext, DatabaseType, Duration, 573 * Throwable)} instead of {@link #didCloseStream(StatementContext, StreamTerminalOutcome, Long, Duration, Throwable)}. 574 */ 575 OPEN_FAILURE 576 } 577 578 /** 579 * Counter snapshot for collectors that support in-process inspection. 580 * <p> 581 * Implementations may read counters independently without cross-counter atomicity. Callers that need invariant-consistent 582 * snapshots should drain in-flight work before reading. 583 * 584 * @param connectionsAcquiredStatementScope statement-scoped connection acquisitions that completed normally 585 * @param connectionsAcquiredTransactionScope transaction-scoped connection acquisitions that completed normally 586 * @param connectionsFailedStatementScope statement-scoped connection acquisitions that failed 587 * @param connectionsFailedTransactionScope transaction-scoped connection acquisitions that failed 588 * @param connectionReleaseFailuresStatementScope statement-scoped connection releases that failed 589 * @param connectionReleaseFailuresTransactionScope transaction-scoped connection releases that failed 590 * @param transactionClosuresEntered closure-based transactions entered 591 * @param transactionClosuresExited closure-based transactions exited 592 * @param transactionClosuresCommitted closure-based transactions that committed 593 * @param transactionClosuresRolledBack closure-based transactions that rolled back 594 * @param transactionClosuresNoPhysical closure-based transactions that exited without a physical JDBC transaction 595 * @param transactionClosuresFailed closure-based transactions that failed before a normal commit or rollback outcome 596 * @param physicalTransactionsBegun physical JDBC transactions begun 597 * @param physicalTransactionsBeginFailed physical JDBC transactions that failed while beginning 598 * @param physicalTransactionsCommitted physical JDBC transactions committed 599 * @param physicalTransactionsCommitFailed physical JDBC transactions whose commit failed 600 * @param physicalTransactionsRolledBack physical JDBC transactions rolled back 601 * @param physicalTransactionsRollbackFailed physical JDBC transactions whose rollback failed 602 * @param savepointsCreated transaction savepoints created 603 * @param savepointsRolledBack transaction savepoints rolled back to 604 * @param savepointsReleased transaction savepoints released 605 * @param statementsExecuted statements that executed successfully 606 * @param statementsFailed statements that failed during execution 607 * @param streamsOpened streams that opened successfully 608 * @param streamsOpenFailures streams that failed before opening 609 * @param streamsClosedNormally opened streams that reached the end of the result set 610 * @param streamsEarlyClosed opened streams that closed before all rows were consumed 611 * @param streamsCallbackFailed opened streams whose caller-provided callback failed 612 * @param streamsIterationFailed opened streams whose result-set iteration failed 613 * @param postTransactionOperationsRun post-transaction operations that ran 614 * @param postTransactionOperationsFailed post-transaction operations that failed 615 */ 616 @ThreadSafe 617 record Snapshot(@NonNull Long connectionsAcquiredStatementScope, 618 @NonNull Long connectionsAcquiredTransactionScope, 619 @NonNull Long connectionsFailedStatementScope, 620 @NonNull Long connectionsFailedTransactionScope, 621 @NonNull Long connectionReleaseFailuresStatementScope, 622 @NonNull Long connectionReleaseFailuresTransactionScope, 623 @NonNull Long transactionClosuresEntered, 624 @NonNull Long transactionClosuresExited, 625 @NonNull Long transactionClosuresCommitted, 626 @NonNull Long transactionClosuresRolledBack, 627 @NonNull Long transactionClosuresNoPhysical, 628 @NonNull Long transactionClosuresFailed, 629 @NonNull Long physicalTransactionsBegun, 630 @NonNull Long physicalTransactionsBeginFailed, 631 @NonNull Long physicalTransactionsCommitted, 632 @NonNull Long physicalTransactionsCommitFailed, 633 @NonNull Long physicalTransactionsRolledBack, 634 @NonNull Long physicalTransactionsRollbackFailed, 635 @NonNull Long savepointsCreated, 636 @NonNull Long savepointsRolledBack, 637 @NonNull Long savepointsReleased, 638 @NonNull Long statementsExecuted, 639 @NonNull Long statementsFailed, 640 @NonNull Long streamsOpened, 641 @NonNull Long streamsOpenFailures, 642 @NonNull Long streamsClosedNormally, 643 @NonNull Long streamsEarlyClosed, 644 @NonNull Long streamsCallbackFailed, 645 @NonNull Long streamsIterationFailed, 646 @NonNull Long postTransactionOperationsRun, 647 @NonNull Long postTransactionOperationsFailed) { 648 } 649}