diff --git a/src/Prometheus/CollectorRegistry.php b/src/Prometheus/CollectorRegistry.php index 541e1d8..2742ac8 100644 --- a/src/Prometheus/CollectorRegistry.php +++ b/src/Prometheus/CollectorRegistry.php @@ -36,6 +36,11 @@ class CollectorRegistry */ private $histograms = []; + /** + * @var Summary[] + */ + private $summaries = []; + /** * CollectorRegistry constructor. * @param Adapter $redisAdapter @@ -238,6 +243,66 @@ public function getOrRegisterHistogram($namespace, $name, $help, $labels = [], $ return $histogram; } + /** + * @param string $namespace e.g. cms + * @param string $name e.g. duration_seconds + * @param string $help e.g. A duration in seconds. + * @param array $labels e.g. ['controller', 'action'] + * @param array $quantiles e.g. [0.1, 0.5, 0.9] + * @return Summary + * @throws MetricsRegistrationException + */ + public function registerSummary(string $namespace, string $name, string $help, array $labels = [], ?array $quantiles = null): Summary + { + $metricIdentifier = self::metricIdentifier($namespace, $name); + if (isset($this->summaries[$metricIdentifier])) { + throw new MetricsRegistrationException("Metric already registered"); + } + $this->summaries[$metricIdentifier] = new Summary( + $this->storageAdapter, + $namespace, + $name, + $help, + $labels, + $quantiles + ); + return $this->summaries[$metricIdentifier]; + } + + /** + * @param string $namespace + * @param string $name + * @return Summary + * @throws MetricNotFoundException + */ + public function getSummary(string $namespace, string $name): Summary + { + $metricIdentifier = self::metricIdentifier($namespace, $name); + if (!isset($this->summaries[$metricIdentifier])) { + throw new MetricNotFoundException("Metric not found:" . $metricIdentifier); + } + return $this->summaries[self::metricIdentifier($namespace, $name)]; + } + + /** + * @param string $namespace e.g. cms + * @param string $name e.g. duration_seconds + * @param string $help e.g. A duration in seconds. + * @param array $labels e.g. ['controller', 'action'] + * @param array $quantiles e.g. [0.1, 0.5, 0.9] + * @return Summary + * @throws MetricsRegistrationException + */ + public function getOrRegisterSummary(string $namespace, string $name, string $help, array $labels = [], ?array $quantiles = null): Summary + { + try { + $summary = $this->getSummary($namespace, $name); + } catch (MetricNotFoundException $e) { + $summary = $this->registerSummary($namespace, $name, $help, $labels, $quantiles); + } + return $summary; + } + /** * @param $namespace * @param $name diff --git a/src/Prometheus/Storage/APC.php b/src/Prometheus/Storage/APC.php index 2fe659d..4598a6d 100644 --- a/src/Prometheus/Storage/APC.php +++ b/src/Prometheus/Storage/APC.php @@ -59,6 +59,14 @@ public function updateHistogram(array $data): void apcu_inc($this->histogramBucketValueKey($data, $bucketToIncrease)); } + /** + * @param array $data + */ + public function updateSummary(array $data) + { + // TODO: Implement updateSummary() method. + } + /** * @param array $data */ diff --git a/src/Prometheus/Storage/Adapter.php b/src/Prometheus/Storage/Adapter.php index 068e1d9..ceb4230 100644 --- a/src/Prometheus/Storage/Adapter.php +++ b/src/Prometheus/Storage/Adapter.php @@ -23,6 +23,12 @@ public function collect(); */ public function updateHistogram(array $data): void; + /** + * @param array $data + * @return void + */ + public function updateSummary(array $data): void; + /** * @param array $data * @return void diff --git a/src/Prometheus/Storage/InMemory.php b/src/Prometheus/Storage/InMemory.php index bfd4c70..a64e4b2 100644 --- a/src/Prometheus/Storage/InMemory.php +++ b/src/Prometheus/Storage/InMemory.php @@ -175,6 +175,15 @@ public function updateHistogram(array $data): void $this->histograms[$metaKey]['samples'][$bucketKey] += 1; } + /** + * @param array $data + * @return void + */ + public function updateSummary(array $data): void + { + // TODO: Implement updateSummary() method. + } + /** * @param array $data */ diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 85e3000..0b49aea 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -10,6 +10,7 @@ use Prometheus\Gauge; use Prometheus\Histogram; use Prometheus\MetricFamilySamples; +use Prometheus\Summary; class Redis implements Adapter { @@ -105,6 +106,7 @@ public function collect(): array $metrics = $this->collectHistograms(); $metrics = array_merge($metrics, $this->collectGauges()); $metrics = array_merge($metrics, $this->collectCounters()); + $metrics = array_merge($metrics, $this->collectSummaries()); return array_map( function (array $metric) { return new MetricFamilySamples($metric); @@ -198,6 +200,47 @@ public function updateHistogram(array $data): void ); } + /** + * @param array $data + * @throws StorageException + */ + public function updateSummary(array $data): void + { + $this->openConnection(); + $metaData = $data; + unset($metaData['value']); + unset($metaData['labelValues']); + $this->redis->eval( + <<toMetricKey($data), + json_encode(['b' => 'sum', 'labelValues' => $data['labelValues']]), + json_encode(['b' => 'count', 'labelValues' => $data['labelValues']]), + json_encode(['b' => 'values', 'labelValues' => $data['labelValues']]), + self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX, + $data['value'], + json_encode($metaData), + ], + 5 + ); + } + + /** * @param array $data * @throws StorageException @@ -348,6 +391,66 @@ private function collectHistograms(): array return $histograms; } + /** + * @return array + */ + private function collectSummaries(): array + { + $keys = $this->redis->sMembers(self::$prefix . Summary::TYPE . self::PROMETHEUS_METRIC_KEYS_SUFFIX); + sort($keys); + $summaries = []; + + foreach ($keys as $key) { + $raw = $this->redis->hGetAll($key); + $summary = json_decode($raw['__meta'], true); + unset($raw['__meta']); + $allLabelValues = []; + + foreach (array_keys($raw) as $k) { + $d = json_decode($k, true); + $allLabelValues[] = $d['labelValues']; + } + + // We need set semantics. + // This is the equivalent of array_unique but for arrays of arrays. + $allLabelValues = array_map('unserialize', array_unique(array_map('serialize', $allLabelValues))); + sort($allLabelValues); + + foreach ($allLabelValues as $labelValues) { + $valuesKey = json_encode(['b' => 'values', 'labelValues' => $labelValues]); + $values = !empty($raw[$valuesKey]) ? explode(',', $raw[$valuesKey]) : []; + foreach ($summary['quantiles'] as $quantile) { + $summary['samples'][] = [ + 'name' => $summary['name'], + 'labelNames' => ['quantile'], + 'labelValues' => array_merge($labelValues, ['quantile' => $quantile]), + 'value' => Summary::getQuantile($quantile, $values) + ]; + } + + // Add the count + $countKey = json_encode(['b' => 'count', 'labelValues' => $labelValues]); + $summary['samples'][] = [ + 'name' => $summary['name'] . '_count', + 'labelNames' => [], + 'labelValues' => $labelValues, + 'value' => !empty($raw[$countKey]) ? (int)$raw[$countKey] : 0, + ]; + + // Add the sum + $sumKey = json_encode(['b' => 'sum', 'labelValues' => $labelValues]); + $summary['samples'][] = [ + 'name' => $summary['name'] . '_sum', + 'labelNames' => [], + 'labelValues' => $labelValues, + 'value' => !empty($raw[$sumKey]) ? $raw[$sumKey] : 0, + ]; + } + $summaries[] = $summary; + } + return $summaries; + } + /** * @return array */ diff --git a/src/Prometheus/Summary.php b/src/Prometheus/Summary.php new file mode 100644 index 0000000..7a2166c --- /dev/null +++ b/src/Prometheus/Summary.php @@ -0,0 +1,108 @@ += $quantiles[$i + 1]) { + throw new InvalidArgumentException( + 'Summary quantiles must be in increasing order: ' . + $quantiles[$i] . ' >= ' . $quantiles[$i + 1] + ); + } + } + foreach ($labels as $label) { + if ($label === 'quantile') { + throw new InvalidArgumentException('Summary cannot have a label named "quantile".'); + } + } + $this->quantiles = $quantiles; + } + + /** + * List of default quantiles + * @return array + */ + public static function getDefaultQuantiles(): array + { + return [ + 0.01, 0.05, 0.5, 0.9, 0.99, + ]; + } + + /** + * @param double $value e.g. 123 + * @param array $labels e.g. ['status', 'opcode'] + */ + public function observe(float $value, array $labels = []): void + { + $this->assertLabelsAreDefinedCorrectly($labels); + $this->storageAdapter->updateSummary( + [ + 'value' => $value, + 'name' => $this->getName(), + 'help' => $this->getHelp(), + 'type' => $this->getType(), + 'labelNames' => $this->getLabelNames(), + 'labelValues' => $labels, + 'quantiles' => $this->quantiles, + ] + ); + } + + /** + * @param float $percentile + * @param array $values + * @return float + */ + public static function getQuantile(float $percentile, array $values): float + { + sort($values); + $index = (int)($percentile * count($values)); + return (floor($index) === $index) + ? ($values[$index - 1] + $values[$index]) / 2 + : (float) $values[(int)floor($index)]; + } + + /** + * @return string + */ + public function getType(): string + { + return self::TYPE; + } +}