From c6a53bd7711bb17637a6614d616d91dc24b45531 Mon Sep 17 00:00:00 2001 From: Janez Urevc Date: Tue, 23 Jul 2024 15:16:18 +0200 Subject: [PATCH] PDO storage adapter - MySQL support. --- src/Prometheus/Storage/PDO.php | 158 ++++++++++++------ .../Prometheus/PDO/CollectorRegistryTest.php | 18 +- tests/Test/Prometheus/PDO/CounterTest.php | 18 +- tests/Test/Prometheus/PDO/GaugeTest.php | 18 +- tests/Test/Prometheus/PDO/HistogramTest.php | 18 +- tests/Test/Prometheus/PDO/SummaryTest.php | 19 ++- 6 files changed, 195 insertions(+), 54 deletions(-) diff --git a/src/Prometheus/Storage/PDO.php b/src/Prometheus/Storage/PDO.php index adf61ea..177f4cc 100644 --- a/src/Prometheus/Storage/PDO.php +++ b/src/Prometheus/Storage/PDO.php @@ -33,14 +33,15 @@ class PDO implements Adapter * PDO database connection. * @param string $prefix * Database table prefix (default: "prometheus_"). - * @param array{0: int, 1: int} $precision - * Precision of the 'value' DECIMAL column in the database table (default: 16, 2). */ - public function __construct(\PDO $database, string $prefix = 'prometheus_', array $precision = [16, 2]) + public function __construct(\PDO $database, string $prefix = 'prometheus_') { + if (!in_array($database->getAttribute(\PDO::ATTR_DRIVER_NAME), ['mysql', 'sqlite'], true)) { + throw new \RuntimeException('Only MySQL and SQLite are supported.'); + } + $this->database = $database; $this->prefix = $prefix; - $this->precision = $precision; $this->createTables(); } @@ -67,6 +68,14 @@ public function wipeStorage(): void $this->database->query("DELETE FROM `{$this->prefix}_histograms`"); } + public function deleteTables(): void + { + $this->database->query("DROP TABLE `{$this->prefix}_metadata`"); + $this->database->query("DROP TABLE `{$this->prefix}_values`"); + $this->database->query("DROP TABLE `{$this->prefix}_summaries`"); + $this->database->query("DROP TABLE `{$this->prefix}_histograms`"); + } + /** * @return MetricFamilySamples[] */ @@ -283,26 +292,31 @@ protected function collectGauges(): array */ public function updateHistogram(array $data): void { - // TODO do we update metadata at all? If metadata changes then the old labels might not be correct any more? - $metadata_sql = <<prefix}_metadata` - VALUES(:name, :type, :metadata) - ON CONFLICT(name, type) DO UPDATE SET - metadata=excluded.metadata; -SQL; - $statement = $this->database->prepare($metadata_sql); - $statement->execute([ - ':name' => $data['name'], - ':type' => Histogram::TYPE, - ':metadata' => $this->encodeMetadata($data), - ]); + $this->updateMetadata($data, Histogram::TYPE); - $values_sql = <<database->getAttribute(\PDO::ATTR_DRIVER_NAME)) { + case 'sqlite': + $values_sql = <<prefix}_histograms`(`name`, `labels_hash`, `labels`, `value`, `bucket`) VALUES(:name,:hash,:labels,:value,:bucket) ON CONFLICT(name, labels_hash, bucket) DO UPDATE SET `value` = `value` + excluded.value; SQL; + break; + + case 'mysql': + $values_sql = <<prefix}_histograms`(`name`, `labels_hash`, `labels`, `value`, `bucket`) + VALUES(:name,:hash,:labels,:value,:bucket) + ON DUPLICATE KEY UPDATE + `value` = `value` + VALUES(`value`); +SQL; + break; + + default: + throw new \RuntimeException('Unsupported database type'); + } + $statement = $this->database->prepare($values_sql); $label_values = $this->encodeLabelValues($data); @@ -337,22 +351,9 @@ public function updateHistogram(array $data): void */ public function updateSummary(array $data): void { - // TODO do we update metadata at all? If metadata changes then the old labels might not be correct any more? - $metadata_sql = <<prefix}_metadata` - VALUES(:name, :type, :metadata) - ON CONFLICT(name, type) DO UPDATE SET - metadata=excluded.metadata; -SQL; - - $statement = $this->database->prepare($metadata_sql); - $statement->execute([ - ':name' => $data['name'], - ':type' => Summary::TYPE, - ':metadata' => $this->encodeMetadata($data), - ]); + $this->updateMetadata($data, Summary::TYPE); - $values_sql = <<prefix}_summaries`(`name`, `labels_hash`, `labels`, `value`, `time`) VALUES(:name,:hash,:labels,:value,:time) SQL; @@ -387,37 +388,85 @@ public function updateCounter(array $data): void /** * @param mixed[] $data */ - protected function updateStandard(array $data, string $type): void + protected function updateMetadata(array $data, string $type): void { // TODO do we update metadata at all? If metadata changes then the old labels might not be correct any more? - $metadata_sql = <<database->getAttribute(\PDO::ATTR_DRIVER_NAME)) { + case 'sqlite': + $metadata_sql = <<prefix}_metadata` VALUES(:name, :type, :metadata) ON CONFLICT(name, type) DO UPDATE SET - metadata=excluded.metadata; + `metadata` = excluded.metadata; SQL; + break; + + case 'mysql': + $metadata_sql = <<prefix}_metadata` + VALUES(:name, :type, :metadata) + ON DUPLICATE KEY UPDATE + `metadata` = VALUES(`metadata`); +SQL; + break; + default: + throw new \RuntimeException('Unsupported database type'); + } $statement = $this->database->prepare($metadata_sql); $statement->execute([ ':name' => $data['name'], ':type' => $type, ':metadata' => $this->encodeMetadata($data), ]); + } - if ($data['command'] === Adapter::COMMAND_SET) { - $values_sql = <<updateMetadata($data, $type); + + switch ($this->database->getAttribute(\PDO::ATTR_DRIVER_NAME)) { + case 'sqlite': + if ($data['command'] === Adapter::COMMAND_SET) { + $values_sql = <<prefix}_values`(`name`, `type`, `labels_hash`, `labels`, `value`) VALUES(:name,:type,:hash,:labels,:value) ON CONFLICT(name, type, labels_hash) DO UPDATE SET `value` = excluded.value; SQL; - } else { - $values_sql = <<prefix}_values`(`name`, `type`, `labels_hash`, `labels`, `value`) VALUES(:name,:type,:hash,:labels,:value) ON CONFLICT(name, type, labels_hash) DO UPDATE SET `value` = `value` + excluded.value; SQL; + } + break; + + case 'mysql': + if ($data['command'] === Adapter::COMMAND_SET) { + $values_sql = <<prefix}_values`(`name`, `type`, `labels_hash`, `labels`, `value`) + VALUES(:name,:type,:hash,:labels,:value) + ON DUPLICATE KEY UPDATE + `value` = VALUES(`value`); +SQL; + } else { + $values_sql = <<prefix}_values`(`name`, `type`, `labels_hash`, `labels`, `value`) + VALUES(:name,:type,:hash,:labels,:value) + ON DUPLICATE KEY UPDATE + `value` = `value` + VALUES(`value`); +SQL; + } + break; + + default: + throw new \RuntimeException('Unsupported database type'); } $statement = $this->database->prepare($values_sql); @@ -433,6 +482,7 @@ protected function updateStandard(array $data, string $type): void protected function createTables(): void { + $driver = $this->database->getAttribute(\PDO::ATTR_DRIVER_NAME); $sql = <<prefix}_metadata` ( `name` varchar(255) NOT NULL, @@ -443,36 +493,46 @@ protected function createTables(): void SQL; $this->database->query($sql); + $hash_size = $driver == 'sqlite' ? 32 : 64; $sql = <<prefix}_values` ( `name` varchar(255) NOT NULL, `type` varchar(9) NOT NULL, - `labels_hash` varchar(32) NOT NULL, + `labels_hash` varchar({$hash_size}) NOT NULL, `labels` TEXT NOT NULL, - `value` DECIMAL({$this->precision[0]},{$this->precision[1]}) DEFAULT 0.0, + `value` double DEFAULT 0.0, PRIMARY KEY (`name`, `type`, `labels_hash`) ) SQL; $this->database->query($sql); + $timestamp_type = $driver == 'sqlite' ? 'timestamp' : 'int'; $sql = <<prefix}_summaries` ( `name` varchar(255) NOT NULL, - `labels_hash` varchar(32) NOT NULL, + `labels_hash` varchar({$hash_size}) NOT NULL, `labels` TEXT NOT NULL, - `value` DECIMAL({$this->precision[0]},{$this->precision[1]}) DEFAULT 0.0, - `time` timestamp NOT NULL -); -CREATE INDEX `name` ON `{$this->prefix}_summaries`(`name`); + `value` double DEFAULT 0.0, + `time` {$timestamp_type} NOT NULL SQL; + switch ($driver) { + case 'sqlite': + $sql .= "); CREATE INDEX `name` ON `{$this->prefix}_summaries`(`name`);"; + break; + + case 'mysql': + $sql .= ", KEY `name` (`name`));"; + break; + } + $this->database->query($sql); $sql = <<prefix}_histograms` ( `name` varchar(255) NOT NULL, - `labels_hash` varchar(32) NOT NULL, + `labels_hash` varchar({$hash_size}) NOT NULL, `labels` TEXT NOT NULL, - `value` DECIMAL({$this->precision[0]},{$this->precision[1]}) DEFAULT 0.0, + `value` double DEFAULT 0.0, `bucket` varchar(255) NOT NULL, PRIMARY KEY (`name`, `labels_hash`, `bucket`) ); diff --git a/tests/Test/Prometheus/PDO/CollectorRegistryTest.php b/tests/Test/Prometheus/PDO/CollectorRegistryTest.php index b8eef34..20c8232 100644 --- a/tests/Test/Prometheus/PDO/CollectorRegistryTest.php +++ b/tests/Test/Prometheus/PDO/CollectorRegistryTest.php @@ -9,9 +9,25 @@ class CollectorRegistryTest extends AbstractCollectorRegistryTest { + /** + * @var \PDO|null + */ + private $pdo; + public function configureAdapter(): void { - $this->adapter = new PDO(new \PDO('sqlite::memory:')); + $this->pdo = new \PDO('sqlite::memory:'); + //$this->pdo = new \PDO('mysql:host=db;dbname=db', 'db', 'db'); + $prefix = 'test' . substr(hash('sha256', uniqid()), 0, 6) . '_'; + $this->adapter = new PDO($this->pdo, $prefix); $this->adapter->wipeStorage(); } + + protected function tearDown(): void + { + parent::tearDown(); + $this->adapter->deleteTables(); /** @phpstan-ignore-line */ + $this->adapter = null; /** @phpstan-ignore-line */ + $this->pdo = null; + } } diff --git a/tests/Test/Prometheus/PDO/CounterTest.php b/tests/Test/Prometheus/PDO/CounterTest.php index 1688d7a..2e9a543 100644 --- a/tests/Test/Prometheus/PDO/CounterTest.php +++ b/tests/Test/Prometheus/PDO/CounterTest.php @@ -12,9 +12,25 @@ */ class CounterTest extends AbstractCounterTest { + /** + * @var \PDO|null + */ + private $pdo; + public function configureAdapter(): void { - $this->adapter = new PDO(new \PDO('sqlite::memory:')); + $this->pdo = new \PDO('sqlite::memory:'); + //$this->pdo = new \PDO('mysql:host=db;dbname=db', 'db', 'db'); + $prefix = 'test' . substr(hash('sha256', uniqid()), 0, 6) . '_'; + $this->adapter = new PDO($this->pdo, $prefix); $this->adapter->wipeStorage(); } + + protected function tearDown(): void + { + parent::tearDown(); + $this->adapter->deleteTables(); /** @phpstan-ignore-line */ + $this->adapter = null; /** @phpstan-ignore-line */ + $this->pdo = null; + } } diff --git a/tests/Test/Prometheus/PDO/GaugeTest.php b/tests/Test/Prometheus/PDO/GaugeTest.php index 09318e3..b46335c 100644 --- a/tests/Test/Prometheus/PDO/GaugeTest.php +++ b/tests/Test/Prometheus/PDO/GaugeTest.php @@ -12,9 +12,25 @@ */ class GaugeTest extends AbstractGaugeTest { + /** + * @var \PDO|null + */ + private $pdo; + public function configureAdapter(): void { - $this->adapter = new PDO(new \PDO('sqlite::memory:')); + $this->pdo = new \PDO('sqlite::memory:'); + //$this->pdo = new \PDO('mysql:host=db;dbname=db', 'db', 'db'); + $prefix = 'test' . substr(hash('sha256', uniqid()), 0, 6) . '_'; + $this->adapter = new PDO($this->pdo, $prefix); $this->adapter->wipeStorage(); } + + protected function tearDown(): void + { + parent::tearDown(); + $this->adapter->deleteTables(); /** @phpstan-ignore-line */ + $this->adapter = null; /** @phpstan-ignore-line */ + $this->pdo = null; + } } diff --git a/tests/Test/Prometheus/PDO/HistogramTest.php b/tests/Test/Prometheus/PDO/HistogramTest.php index 3c2940d..a91163a 100644 --- a/tests/Test/Prometheus/PDO/HistogramTest.php +++ b/tests/Test/Prometheus/PDO/HistogramTest.php @@ -12,9 +12,25 @@ */ class HistogramTest extends AbstractHistogramTest { + /** + * @var \PDO|null + */ + private $pdo; + public function configureAdapter(): void { - $this->adapter = new PDO(new \PDO('sqlite::memory:')); + $this->pdo = new \PDO('sqlite::memory:'); + //$this->pdo = new \PDO('mysql:host=db;dbname=db', 'db', 'db'); + $prefix = 'test' . substr(hash('sha256', uniqid()), 0, 6) . '_'; + $this->adapter = new PDO($this->pdo, $prefix); $this->adapter->wipeStorage(); } + + public function tearDown(): void + { + parent::tearDown(); + $this->adapter->deleteTables(); /** @phpstan-ignore-line */ + $this->adapter = null; /** @phpstan-ignore-line */ + $this->pdo = null; + } } diff --git a/tests/Test/Prometheus/PDO/SummaryTest.php b/tests/Test/Prometheus/PDO/SummaryTest.php index c1429d1..adc6ab5 100644 --- a/tests/Test/Prometheus/PDO/SummaryTest.php +++ b/tests/Test/Prometheus/PDO/SummaryTest.php @@ -12,9 +12,26 @@ */ class SummaryTest extends AbstractSummaryTest { + /** + * @var \PDO|null + */ + private $pdo; + public function configureAdapter(): void { - $this->adapter = new PDO(new \PDO('sqlite::memory:')); + $this->pdo = new \PDO('sqlite::memory:'); + //$this->pdo = new \PDO('mysql:host=db;dbname=db', 'db', 'db'); + $prefix = 'test' . substr(hash('sha256', uniqid()), 0, 6) . '_'; + $this->adapter = new PDO($this->pdo, $prefix); $this->adapter->wipeStorage(); } + + public function tearDown(): void + { + parent::tearDown(); + $this->adapter->deleteTables(); /** @phpstan-ignore-line */ + $this->adapter = null; /** @phpstan-ignore-line */ + $this->pdo = null; + } + }