Skip to content

Commit 4b9f0d0

Browse files
Feat/add summary support (#53)
* add summary support - tests and structure Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - doc Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * summary in memory adapter Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - apcu Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - redis Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - refacto Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - handle apc atomicity Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - handle redis atomicity Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - fix redis prefix tests Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - cleaning Signed-off-by: Matthias Perret <moussadedijon@gmail.com> * add summary support - clean2 Signed-off-by: Matthias Perret <moussadedijon@gmail.com> Co-authored-by: Lukas Kämmerling <lukas.kaemmerling@hetzner-cloud.de>
1 parent 35d5ad5 commit 4b9f0d0

18 files changed

+1512
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ $gauge->set(2.5, ['blue']);
4141

4242
$histogram = $registry->getOrRegisterHistogram('test', 'some_histogram', 'it observes', ['type'], [0.1, 1, 2, 3.5, 4, 5, 6, 7, 8, 9]);
4343
$histogram->observe(3.5, ['blue']);
44+
45+
$summary = $registry->getOrRegisterSummary('test', 'some_summary', 'it observes a sliding window', ['type'], 84600, [0.01, 0.05, 0.5, 0.95, 0.99]);
46+
$histogram->observe(5, ['blue']);
4447
```
4548

4649
Manually register and retrieve metrics (these steps are combined in the `getOrRegister...` methods):

examples/some_summary.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
require __DIR__ . '/../vendor/autoload.php';
4+
5+
use Prometheus\CollectorRegistry;
6+
use Prometheus\Storage\Redis;
7+
8+
error_log('c=' . $_GET['c']);
9+
10+
$adapter = $_GET['adapter'];
11+
12+
if ($adapter === 'redis') {
13+
Redis::setDefaultOptions(['host' => $_SERVER['REDIS_HOST'] ?? '127.0.0.1']);
14+
$adapter = new Prometheus\Storage\Redis();
15+
} elseif ($adapter === 'apc') {
16+
$adapter = new Prometheus\Storage\APC();
17+
} elseif ($adapter === 'in-memory') {
18+
$adapter = new Prometheus\Storage\InMemory();
19+
}
20+
$registry = new CollectorRegistry($adapter);
21+
22+
$summary = $registry->registerSummary('test', 'some_summary', 'it observes', ['type'], 600, [0.01, 0.05, 0.5, 0.95, 0.99]);
23+
$summary->observe($_GET['c'], ['blue']);
24+
25+
echo "OK\n";

src/Prometheus/CollectorRegistry.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ class CollectorRegistry implements RegistryInterface
3636
*/
3737
private $histograms = [];
3838

39+
/**
40+
* @var Summary[]
41+
*/
42+
private $summaries = [];
43+
3944
/**
4045
* @var Gauge[]
4146
*/
@@ -264,6 +269,85 @@ public function getOrRegisterHistogram(
264269
return $histogram;
265270
}
266271

272+
273+
/**
274+
* @param string $namespace e.g. cms
275+
* @param string $name e.g. duration_seconds
276+
* @param string $help e.g. A summary of the duration in seconds.
277+
* @param string[] $labels e.g. ['controller', 'action']
278+
* @param int $maxAgeSeconds e.g. 604800
279+
* @param float[]|null $quantiles e.g. [0.01, 0.5, 0.99]
280+
*
281+
* @return Summary
282+
* @throws MetricsRegistrationException
283+
*/
284+
public function registerSummary(
285+
string $namespace,
286+
string $name,
287+
string $help,
288+
array $labels = [],
289+
int $maxAgeSeconds = 600,
290+
array $quantiles = null
291+
): Summary {
292+
$metricIdentifier = self::metricIdentifier($namespace, $name);
293+
if (isset($this->summaries[$metricIdentifier])) {
294+
throw new MetricsRegistrationException("Metric already registered");
295+
}
296+
$this->summaries[$metricIdentifier] = new Summary(
297+
$this->storageAdapter,
298+
$namespace,
299+
$name,
300+
$help,
301+
$labels,
302+
$maxAgeSeconds,
303+
$quantiles
304+
);
305+
return $this->summaries[$metricIdentifier];
306+
}
307+
308+
/**
309+
* @param string $namespace
310+
* @param string $name
311+
*
312+
* @return Summary
313+
* @throws MetricNotFoundException
314+
*/
315+
public function getSummary(string $namespace, string $name): Summary
316+
{
317+
$metricIdentifier = self::metricIdentifier($namespace, $name);
318+
if (!isset($this->summaries[$metricIdentifier])) {
319+
throw new MetricNotFoundException("Metric not found:" . $metricIdentifier);
320+
}
321+
return $this->summaries[self::metricIdentifier($namespace, $name)];
322+
}
323+
324+
/**
325+
* @param string $namespace e.g. cms
326+
* @param string $name e.g. duration_seconds
327+
* @param string $help e.g. A summary of the duration in seconds.
328+
* @param string[] $labels e.g. ['controller', 'action']
329+
* @param int $maxAgeSeconds e.g. 604800
330+
* @param float[]|null $quantiles e.g. [0.01, 0.5, 0.99]
331+
*
332+
* @return Summary
333+
* @throws MetricsRegistrationException
334+
*/
335+
public function getOrRegisterSummary(
336+
string $namespace,
337+
string $name,
338+
string $help,
339+
array $labels = [],
340+
int $maxAgeSeconds = 600,
341+
array $quantiles = null
342+
): Summary {
343+
try {
344+
$summary = $this->getSummary($namespace, $name);
345+
} catch (MetricNotFoundException $e) {
346+
$summary = $this->registerSummary($namespace, $name, $help, $labels, $maxAgeSeconds, $quantiles);
347+
}
348+
return $summary;
349+
}
350+
267351
/**
268352
* @param string $namespace
269353
* @param string $name

src/Prometheus/Math.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Prometheus;
6+
7+
class Math
8+
{
9+
10+
/**
11+
* taken from https://www.php.net/manual/fr/function.stats-stat-percentile.php#79752
12+
* @param float[] $arr must be sorted
13+
* @param float $q
14+
*
15+
* @return float
16+
*/
17+
public function quantile(array $arr, float $q): float
18+
{
19+
$count = count($arr);
20+
if ($count === 0) {
21+
return 0;
22+
}
23+
24+
$allindex = ($count - 1) * $q;
25+
$intvalindex = (int) $allindex;
26+
$floatval = $allindex - $intvalindex;
27+
if ($count > $intvalindex + 1) {
28+
$result = $floatval * ($arr[$intvalindex + 1] - $arr[$intvalindex]) + $arr[$intvalindex];
29+
} else {
30+
$result = $arr[$intvalindex];
31+
}
32+
return $result;
33+
}
34+
}

src/Prometheus/RegistryInterface.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,46 @@ public function getHistogram(string $namespace, string $name): Histogram;
112112
* @throws MetricsRegistrationException
113113
*/
114114
public function getOrRegisterHistogram(string $namespace, string $name, string $help, array $labels = [], array $buckets = null): Histogram;
115+
116+
/**
117+
* @param string $namespace e.g. cms
118+
* @param string $name e.g. duration_seconds
119+
* @param string $help e.g. A histogram of the duration in seconds.
120+
* @param string[] $labels e.g. ['controller', 'action']
121+
* @param int $maxAgeSeconds e.g. 604800
122+
* @param float[]|null $quantiles e.g. [0.01, 0.5, 0.99]
123+
*
124+
* @return Summary
125+
* @throws MetricsRegistrationException
126+
*/
127+
public function registerSummary(
128+
string $namespace,
129+
string $name,
130+
string $help,
131+
array $labels = [],
132+
int $maxAgeSeconds = 86400,
133+
array $quantiles = null
134+
): Summary;
135+
136+
/**
137+
* @param string $namespace
138+
* @param string $name
139+
*
140+
* @return Summary
141+
* @throws MetricNotFoundException
142+
*/
143+
public function getSummary(string $namespace, string $name): Summary;
144+
145+
/**
146+
* @param string $namespace e.g. cms
147+
* @param string $name e.g. duration_seconds
148+
* @param string $help e.g. A histogram of the duration in seconds.
149+
* @param string[] $labels e.g. ['controller', 'action']
150+
* @param int $maxAgeSeconds e.g. 604800
151+
* @param float[]|null $quantiles e.g. [0.01, 0.5, 0.99]
152+
*
153+
* @return Summary
154+
* @throws MetricsRegistrationException
155+
*/
156+
public function getOrRegisterSummary(string $namespace, string $name, string $help, array $labels = [], int $maxAgeSeconds = 86400, array $quantiles = null): Summary;
115157
}

src/Prometheus/Storage/APC.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use APCUIterator;
88
use Prometheus\Exception\StorageException;
9+
use Prometheus\Math;
910
use Prometheus\MetricFamilySamples;
1011
use RuntimeException;
1112

@@ -44,6 +45,7 @@ public function collect(): array
4445
$metrics = $this->collectHistograms();
4546
$metrics = array_merge($metrics, $this->collectGauges());
4647
$metrics = array_merge($metrics, $this->collectCounters());
48+
$metrics = array_merge($metrics, $this->collectSummaries());
4749
return $metrics;
4850
}
4951

@@ -85,6 +87,27 @@ public function updateHistogram(array $data): void
8587
apcu_inc($this->histogramBucketValueKey($data, $bucketToIncrease));
8688
}
8789

90+
/**
91+
* @param mixed[] $data
92+
*/
93+
public function updateSummary(array $data): void
94+
{
95+
// store meta
96+
$metaKey = $this->metaKey($data);
97+
apcu_add($metaKey, $this->metaData($data));
98+
99+
// store value key
100+
$valueKey = $this->valueKey($data);
101+
apcu_add($valueKey, $this->encodeLabelValues($data['labelValues']));
102+
103+
// trick to handle uniqid collision
104+
$done = false;
105+
while (!$done) {
106+
$sampleKey = $valueKey . ':' . uniqid('', true);
107+
$done = apcu_add($sampleKey, $data['value'], $data['maxAgeSeconds']);
108+
}
109+
}
110+
88111
/**
89112
* @param mixed[] $data
90113
*/
@@ -350,6 +373,75 @@ private function collectHistograms(): array
350373
return $histograms;
351374
}
352375

376+
/**
377+
* @return MetricFamilySamples[]
378+
*/
379+
private function collectSummaries(): array
380+
{
381+
$math = new Math();
382+
$summaries = [];
383+
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':summary:.*:meta/') as $summary) {
384+
$metaData = $summary['value'];
385+
$data = [
386+
'name' => $metaData['name'],
387+
'help' => $metaData['help'],
388+
'type' => $metaData['type'],
389+
'labelNames' => $metaData['labelNames'],
390+
'maxAgeSeconds' => $metaData['maxAgeSeconds'],
391+
'quantiles' => $metaData['quantiles'],
392+
'samples' => [],
393+
];
394+
395+
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':summary:' . $metaData['name'] . ':.*:value$/') as $value) {
396+
$encodedLabelValues = $value['value'];
397+
$decodedLabelValues = $this->decodeLabelValues($encodedLabelValues);
398+
$samples = [];
399+
foreach (new APCUIterator('/^' . $this->prometheusPrefix . ':summary:' . $metaData['name'] . ':' . str_replace('/', '\\/', preg_quote($encodedLabelValues)) . ':value:.*/') as $sample) {
400+
$samples[] = $sample['value'];
401+
}
402+
403+
if (count($samples) === 0) {
404+
apcu_delete($value['key']);
405+
continue;
406+
}
407+
408+
// Compute quantiles
409+
sort($samples);
410+
foreach ($data['quantiles'] as $quantile) {
411+
$data['samples'][] = [
412+
'name' => $metaData['name'],
413+
'labelNames' => ['quantile'],
414+
'labelValues' => array_merge($decodedLabelValues, [$quantile]),
415+
'value' => $math->quantile($samples, $quantile),
416+
];
417+
}
418+
419+
// Add the count
420+
$data['samples'][] = [
421+
'name' => $metaData['name'] . '_count',
422+
'labelNames' => [],
423+
'labelValues' => $decodedLabelValues,
424+
'value' => count($samples),
425+
];
426+
427+
// Add the sum
428+
$data['samples'][] = [
429+
'name' => $metaData['name'] . '_sum',
430+
'labelNames' => [],
431+
'labelValues' => $decodedLabelValues,
432+
'value' => array_sum($samples),
433+
];
434+
}
435+
436+
if (count($data['samples']) > 0) {
437+
$summaries[] = new MetricFamilySamples($data);
438+
} else {
439+
apcu_delete($summary['key']);
440+
}
441+
}
442+
return $summaries;
443+
}
444+
353445
/**
354446
* @param mixed $val
355447
* @return int

src/Prometheus/Storage/Adapter.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ interface Adapter
1818
*/
1919
public function collect(): array;
2020

21+
/**
22+
* @param mixed[] $data
23+
* @return void
24+
*/
25+
public function updateSummary(array $data): void;
26+
2127
/**
2228
* @param mixed[] $data
2329
* @return void

0 commit comments

Comments
 (0)