src/Prometheus/Storage/APC.php
<?php
namespace Prometheus\Storage;
use Prometheus\MetricFamilySamples;
use RuntimeException;
class APC implements Adapter
{
const PROMETHEUS_PREFIX = 'prom';
/**
* @return MetricFamilySamples[]
*/
public function collect()
{
$metrics = $this->collectHistograms();
$metrics = array_merge($metrics, $this->collectGauges());
$metrics = array_merge($metrics, $this->collectCounters());
return $metrics;
}
public function updateHistogram(array $data)
{
// Initialize the sum
$sumKey = $this->histogramBucketValueKey($data, 'sum');
$new = apcu_add($sumKey, $this->toInteger(0));
// If sum does not exist, assume a new histogram and store the metadata
if ($new) {
apcu_store($this->metaKey($data), json_encode($this->metaData($data)));
}
// Atomically increment the sum
// Taken from https://github.com/prometheus/client_golang/blob/66058aac3a83021948e5fb12f1f408ff556b9037/prometheus/value.go#L91
$done = false;
while (!$done) {
$old = apcu_fetch($sumKey);
$done = apcu_cas($sumKey, $old, $this->toInteger($this->fromInteger($old) + $data['value']));
}
// Figure out in which bucket the observation belongs
$bucketToIncrease = '+Inf';
foreach ($data['buckets'] as $bucket) {
if ($data['value'] <= $bucket) {
$bucketToIncrease = $bucket;
break;
}
}
// Initialize and increment the bucket
apcu_add($this->histogramBucketValueKey($data, $bucketToIncrease), 0);
apcu_inc($this->histogramBucketValueKey($data, $bucketToIncrease));
}
public function updateGauge(array $data)
{
$valueKey = $this->valueKey($data);
if ($data['command'] == Adapter::COMMAND_SET) {
apcu_store($valueKey, $this->toInteger($data['value']));
apcu_store($this->metaKey($data), json_encode($this->metaData($data)));
} else {
$new = apcu_add($valueKey, $this->toInteger(0));
if ($new) {
apcu_store($this->metaKey($data), json_encode($this->metaData($data)));
}
// Taken from https://github.com/prometheus/client_golang/blob/66058aac3a83021948e5fb12f1f408ff556b9037/prometheus/value.go#L91
$done = false;
while (!$done) {
$old = apcu_fetch($valueKey);
$done = apcu_cas($valueKey, $old, $this->toInteger($this->fromInteger($old) + $data['value']));
}
}
}
public function updateCounter(array $data)
{
$new = apcu_add($this->valueKey($data), 0);
if ($new) {
apcu_store($this->metaKey($data), json_encode($this->metaData($data)));
}
apcu_inc($this->valueKey($data), $data['value']);
}
public function flushAPC()
{
apcu_clear_cache();
}
/**
* @param array $data
* @return string
*/
private function metaKey(array $data)
{
return implode(':', array(self::PROMETHEUS_PREFIX, $data['type'], $data['name'], 'meta'));
}
/**
* @param array $data
* @return string
*/
private function valueKey(array $data)
{
return implode(':', array(
self::PROMETHEUS_PREFIX,
$data['type'],
$data['name'],
$this->encodeLabelValues($data['labelValues']),
'value'
));
}
/**
* @param array $data
* @return string
*/
private function histogramBucketValueKey(array $data, $bucket)
{
return implode(':', array(
self::PROMETHEUS_PREFIX,
$data['type'],
$data['name'],
$this->encodeLabelValues($data['labelValues']),
$bucket,
'value'
));
}
/**
* @param array $data
* @return array
*/
private function metaData(array $data)
{
$metricsMetaData = $data;
unset($metricsMetaData['value']);
unset($metricsMetaData['command']);
unset($metricsMetaData['labelValues']);
return $metricsMetaData;
}
/**
* @return array
*/
private function collectCounters()
{
$counters = array();
foreach (new \APCUIterator('/^prom:counter:.*:meta/') as $counter) {
$metaData = json_decode($counter['value'], true);
$data = array(
'name' => $metaData['name'],
'help' => $metaData['help'],
'type' => $metaData['type'],
'labelNames' => $metaData['labelNames'],
);
foreach (new \APCUIterator('/^prom:counter:' . $metaData['name'] . ':.*:value/') as $value) {
$parts = explode(':', $value['key']);
$labelValues = $parts[3];
$data['samples'][] = array(
'name' => $metaData['name'],
'labelNames' => array(),
'labelValues' => $this->decodeLabelValues($labelValues),
'value' => $value['value']
);
}
$this->sortSamples($data['samples']);
$counters[] = new MetricFamilySamples($data);
}
return $counters;
}
/**
* @return array
*/
private function collectGauges()
{
$gauges = array();
foreach (new \APCUIterator('/^prom:gauge:.*:meta/') as $gauge) {
$metaData = json_decode($gauge['value'], true);
$data = array(
'name' => $metaData['name'],
'help' => $metaData['help'],
'type' => $metaData['type'],
'labelNames' => $metaData['labelNames'],
);
foreach (new \APCUIterator('/^prom:gauge:' . $metaData['name'] . ':.*:value/') as $value) {
$parts = explode(':', $value['key']);
$labelValues = $parts[3];
$data['samples'][] = array(
'name' => $metaData['name'],
'labelNames' => array(),
'labelValues' => $this->decodeLabelValues($labelValues),
'value' => $this->fromInteger($value['value'])
);
}
$this->sortSamples($data['samples']);
$gauges[] = new MetricFamilySamples($data);
}
return $gauges;
}
/**
* @return array
*/
private function collectHistograms()
{
$histograms = array();
foreach (new \APCUIterator('/^prom:histogram:.*:meta/') as $histogram) {
$metaData = json_decode($histogram['value'], true);
$data = array(
'name' => $metaData['name'],
'help' => $metaData['help'],
'type' => $metaData['type'],
'labelNames' => $metaData['labelNames'],
'buckets' => $metaData['buckets']
);
// Add the Inf bucket so we can compute it later on
$data['buckets'][] = '+Inf';
$histogramBuckets = array();
foreach (new \APCUIterator('/^prom:histogram:' . $metaData['name'] . ':.*:value/') as $value) {
$parts = explode(':', $value['key']);
$labelValues = $parts[3];
$bucket = $parts[4];
// Key by labelValues
$histogramBuckets[$labelValues][$bucket] = $value['value'];
}
// Compute all buckets
$labels = array_keys($histogramBuckets);
sort($labels);
foreach ($labels as $labelValues) {
$acc = 0;
$decodedLabelValues = $this->decodeLabelValues($labelValues);
foreach ($data['buckets'] as $bucket) {
$bucket = (string) $bucket;
if (!isset($histogramBuckets[$labelValues][$bucket])) {
$data['samples'][] = array(
'name' => $metaData['name'] . '_bucket',
'labelNames' => array('le'),
'labelValues' => array_merge($decodedLabelValues, array($bucket)),
'value' => $acc
);
} else {
$acc += $histogramBuckets[$labelValues][$bucket];
$data['samples'][] = array(
'name' => $metaData['name'] . '_' . 'bucket',
'labelNames' => array('le'),
'labelValues' => array_merge($decodedLabelValues, array($bucket)),
'value' => $acc
);
}
}
// Add the count
$data['samples'][] = array(
'name' => $metaData['name'] . '_count',
'labelNames' => array(),
'labelValues' => $decodedLabelValues,
'value' => $acc
);
// Add the sum
$data['samples'][] = array(
'name' => $metaData['name'] . '_sum',
'labelNames' => array(),
'labelValues' => $decodedLabelValues,
'value' => $this->fromInteger($histogramBuckets[$labelValues]['sum'])
);
}
$histograms[] = new MetricFamilySamples($data);
}
return $histograms;
}
/**
* @param mixed $val
* @return int
*/
private function toInteger($val)
{
return unpack('Q', pack('d', $val))[1];
}
/**
* @param mixed $val
* @return int
*/
private function fromInteger($val)
{
return unpack('d', pack('Q', $val))[1];
}
private function sortSamples(array &$samples)
{
usort($samples, function($a, $b){
return strcmp(implode("", $a['labelValues']), implode("", $b['labelValues']));
});
}
/**
* @param array $values
* @return string
* @throws RuntimeException
*/
private function encodeLabelValues(array $values)
{
$json = json_encode($values);
if (false === $json) {
throw new RuntimeException(json_last_error_msg());
}
return base64_encode($json);
}
/**
* @param string $values
* @return array
* @throws RuntimeException
*/
private function decodeLabelValues($values)
{
$json = base64_decode($values, true);
if (false === $json) {
throw new RuntimeException('Cannot base64 decode label values');
}
$decodedValues = json_decode($json, true);
if (false === $decodedValues) {
throw new RuntimeException(json_last_error_msg());
}
return $decodedValues;
}
}