Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Summary Collector #2

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/Prometheus/CollectorRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class CollectorRegistry
*/
private $histograms = [];

/**
* @var Summary[]
*/
private $summaries = [];

/**
* CollectorRegistry constructor.
* @param Adapter $redisAdapter
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/Prometheus/Storage/APC.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
6 changes: 6 additions & 0 deletions src/Prometheus/Storage/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/Prometheus/Storage/InMemory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
103 changes: 103 additions & 0 deletions src/Prometheus/Storage/Redis.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Prometheus\Gauge;
use Prometheus\Histogram;
use Prometheus\MetricFamilySamples;
use Prometheus\Summary;

class Redis implements Adapter
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
<<<LUA
local increment = redis.call('hIncrByFloat', KEYS[1], KEYS[2], ARGV[1])
redis.call('hIncrBy', KEYS[1], KEYS[3], 1)
local values = redis.call('hGet', KEYS[1], KEYS[4])
if values == false then
values = ARGV[1]
else
values = values .. "," .. ARGV[1]
end
redis.call('hSet', KEYS[1], KEYS[4], values)
if increment == ARGV[1] then
redis.call('hSet', KEYS[1], '__meta', ARGV[2])
redis.call('sAdd', KEYS[5], KEYS[1])
end
LUA
,
[
$this->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
Expand Down Expand Up @@ -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;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a LOT of nested loops in here, can we reduce these to improve performance?


/**
* @return array
*/
Expand Down
108 changes: 108 additions & 0 deletions src/Prometheus/Summary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

declare(strict_types=1);

namespace Prometheus;

use InvalidArgumentException;
use Prometheus\Storage\Adapter;

class Summary extends Collector
{
const TYPE = 'summary';

/**
* @var array|null
*/
private $quantiles;

/**
* @param Adapter $adapter
* @param string $namespace
* @param string $name
* @param string $help
* @param array $labels
* @param array $quantiles
* @throws InvalidArgumentException
*/
public function __construct(Adapter $adapter, string $namespace, string $name, string $help, array $labels = [], ?array $quantiles = null)
{
parent::__construct($adapter, $namespace, $name, $help, $labels);

if (null === $quantiles) {
$quantiles = self::getDefaultQuantiles();
}

if (0 === count($quantiles)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to flip these yoda statements to be consistent with the existing code base

throw new InvalidArgumentException('Summary must have at least one quantile.');
}

for ($i = 0; $i < count($quantiles) - 1; $i++) {
if ($quantiles[$i] >= $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;
}
}