From 0019b5987d727753687d459d26eaee7adb140c6d Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 8 Apr 2026 15:58:55 +0200 Subject: [PATCH] feat : add DatabaseService for PostgreSQL metrics Co-Authored-By: Claude Sonnet 4.6 --- src/Service/DatabaseService.php | 124 ++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/Service/DatabaseService.php diff --git a/src/Service/DatabaseService.php b/src/Service/DatabaseService.php new file mode 100644 index 0000000..2beb7b4 --- /dev/null +++ b/src/Service/DatabaseService.php @@ -0,0 +1,124 @@ + false, + 'name' => $databaseName, + 'size' => '', + 'tableCount' => 0, + 'activeConnections' => 0, + 'cacheHitRatio' => 0.0, + 'largestTable' => '', + ]; + + try { + // Check database exists + $exists = $this->connection->fetchOne( + 'SELECT 1 FROM pg_database WHERE datname = :dbname', + ['dbname' => $databaseName] + ); + + if (!$exists) { + return $fallback; + } + + // Database size + $sizeBytes = (int) $this->connection->fetchOne( + 'SELECT pg_database_size(:dbname)', + ['dbname' => $databaseName] + ); + $size = $this->formatBytes($sizeBytes); + + // Table count + $tableCount = (int) $this->connection->fetchOne( + "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_catalog = :dbname", + ['dbname' => $databaseName] + ); + + // Active connections + $activeConnections = (int) $this->connection->fetchOne( + 'SELECT count(*) FROM pg_stat_activity WHERE datname = :dbname', + ['dbname' => $databaseName] + ); + + // Cache hit ratio + $cacheHitRatio = (float) ($this->connection->fetchOne( + 'SELECT round(100.0 * sum(blks_hit) / nullif(sum(blks_hit + blks_read), 0), 2) FROM pg_stat_database WHERE datname = :dbname', + ['dbname' => $databaseName] + ) ?? 0); + + // Largest table — requires querying the target database catalog + $largestTable = $this->fetchLargestTable($databaseName); + + return [ + 'connected' => true, + 'name' => $databaseName, + 'size' => $size, + 'tableCount' => $tableCount, + 'activeConnections' => $activeConnections, + 'cacheHitRatio' => $cacheHitRatio, + 'largestTable' => $largestTable, + ]; + } catch (\Throwable) { + return $fallback; + } + } + + private function fetchLargestTable(string $databaseName): string + { + try { + $row = $this->connection->fetchAssociative( + "SELECT relname, pg_total_relation_size(c.oid) as total_size + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' AND c.relkind = 'r' + AND c.relowner = (SELECT oid FROM pg_roles WHERE rolname = current_user) + ORDER BY pg_total_relation_size(c.oid) DESC + LIMIT 1" + ); + + if (!$row) { + return '-'; + } + + return $row['relname'] . ' (' . $this->formatBytes((int) $row['total_size']) . ')'; + } catch (\Throwable) { + return '-'; + } + } + + private function formatBytes(int $bytes): string + { + if ($bytes < 1024) { + return $bytes . ' B'; + } + + $units = ['KB', 'MB', 'GB', 'TB']; + $value = (float) $bytes; + + foreach ($units as $unit) { + $value /= 1024; + if ($value < 1024) { + return round($value, 1) . ' ' . $unit; + } + } + + return round($value, 1) . ' TB'; + } +}