Skip to content

Commit

Permalink
feat(metrics): Add Summary metric type support (#9)
Browse files Browse the repository at this point in the history
Closes #5
  • Loading branch information
zlodes committed Sep 3, 2023
1 parent 0e7b0ac commit b26bc3e
Show file tree
Hide file tree
Showing 15 changed files with 947 additions and 21 deletions.
8 changes: 0 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ This package provides you an ability to collect and export [Prometheus](https://
* Won't break your business logic even if something is wrong with Metrics Storage
* Ready to use with static analysis tools (PHPStan, Psalm)

## Supported metric types

1. [Counter](https://prometheus.io/docs/concepts/metric_types/#counter)
2. [Gauge](https://prometheus.io/docs/concepts/metric_types/#gauge)
3. [Histogram](https://prometheus.io/docs/concepts/metric_types/#histogram)

Summary is still in development. [What can I do if my client library does not support the metric type I need?](https://prometheus.io/docs/practices/histograms/#what-can-i-do-if-my-client-library-does-not-support-the-metric-type-i-need)

## Adapters
* For Laravel: [zlodes/prometheus-client-laravel](https://github.com/zlodes/php-prometheus-client-laravel)

Expand Down
60 changes: 60 additions & 0 deletions src/Collector/ByType/SummaryCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Zlodes\PrometheusClient\Collector\ByType;

use Psr\Log\LoggerInterface;
use Zlodes\PrometheusClient\Collector\Collector;
use Zlodes\PrometheusClient\Exception\StorageWriteException;
use Zlodes\PrometheusClient\Metric\Summary;
use Zlodes\PrometheusClient\StopWatch\HRTimeStopWatch;
use Zlodes\PrometheusClient\StopWatch\StopWatch;
use Zlodes\PrometheusClient\Storage\DTO\MetricNameWithLabels;
use Zlodes\PrometheusClient\Storage\DTO\MetricValue;
use Zlodes\PrometheusClient\Storage\Storage;

/**
* @final
*/
final class SummaryCollector extends Collector
{
/**
* @internal Zlodes\PrometheusClient\Collector
*
* @param class-string<StopWatch> $stopWatchClass
*/
public function __construct(
private readonly Summary $summary,
private readonly Storage $storage,
private readonly LoggerInterface $logger,
private readonly string $stopWatchClass = HRTimeStopWatch::class,
) {
}

public function update(float|int $value): void
{
$summary = $this->summary;
$labels = $this->getLabels();

try {
$this->storage->persistSummary(
new MetricValue(
new MetricNameWithLabels($summary->getName(), $labels),
$value,
)
);
} catch (StorageWriteException $e) {
$this->logger->error("Cannot persist Summary {$summary->getName()}: $e");
}
}

public function startTimer(): StopWatch
{
$stopWatchClass = $this->stopWatchClass;

return new $stopWatchClass(function (float $elapsed): void {
$this->update($elapsed);
});
}
}
21 changes: 21 additions & 0 deletions src/Collector/CollectorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
use Zlodes\PrometheusClient\Collector\ByType\CounterCollector;
use Zlodes\PrometheusClient\Collector\ByType\GaugeCollector;
use Zlodes\PrometheusClient\Collector\ByType\HistogramCollector;
use Zlodes\PrometheusClient\Collector\ByType\SummaryCollector;
use Zlodes\PrometheusClient\Exception\MetricHasWrongTypeException;
use Zlodes\PrometheusClient\Exception\MetricNotFoundException;
use Zlodes\PrometheusClient\Metric\Counter;
use Zlodes\PrometheusClient\Metric\Gauge;
use Zlodes\PrometheusClient\Metric\Histogram;
use Zlodes\PrometheusClient\Metric\Summary;
use Zlodes\PrometheusClient\Registry\Registry;
use Zlodes\PrometheusClient\Storage\Storage;

Expand Down Expand Up @@ -90,4 +92,23 @@ public function histogram(string $histogramName): HistogramCollector
$this->logger
);
}

/**
* @param non-empty-string $summaryName
*
* @return SummaryCollector
*
* @throws MetricNotFoundException
* @throws MetricHasWrongTypeException
*/
final public function summary(string $summaryName): SummaryCollector
{
$summary = $this->registry->getMetric($summaryName, Summary::class);

return new SummaryCollector(
$summary,
$this->storage,
$this->logger
);
}
}
68 changes: 68 additions & 0 deletions src/Metric/Summary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Zlodes\PrometheusClient\Metric;

use InvalidArgumentException;
use Webmozart\Assert\Assert;

final class Summary extends Metric
{
/** @var non-empty-list<float> */
private array $quantiles = [0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999];

public function getPrometheusType(): string
{
return 'summary';
}

public function getDependentMetrics(): array
{
$selfName = $this->getName();

return [
"{$selfName}_sum",
"{$selfName}_count",
];
}

/**
* @return non-empty-list<float>
*/
public function getQuantiles(): array
{
return $this->quantiles;
}

/**
* @param non-empty-list<float> $quantiles
*
* @return $this
*/
public function withQuantiles(array $quantiles): self
{
$this->validateQuantiles($quantiles);

$summary = clone $this;
$summary->quantiles = $quantiles;

return $summary;
}

/**
* @param non-empty-list<float> $quantiles
*
* @throws InvalidArgumentException
*/
private function validateQuantiles(array $quantiles): void
{
Assert::notEmpty($quantiles);
Assert::allNumeric($quantiles);
Assert::uniqueValues($quantiles);

foreach ($quantiles as $quantile) {
Assert::range($quantile, 0.0, 1.0, 'Quantile MUST be in range [0.0, 1.0]');
}
}
}
86 changes: 83 additions & 3 deletions src/Storage/InMemory/InMemoryStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
namespace Zlodes\PrometheusClient\Storage\InMemory;

use Generator;
use Webmozart\Assert\Assert;
use Zlodes\PrometheusClient\Exception\MetricKeySerializationException;
use Zlodes\PrometheusClient\Exception\MetricKeyUnserializationException;
use Zlodes\PrometheusClient\Exception\StorageReadException;
use Zlodes\PrometheusClient\Exception\StorageWriteException;
use Zlodes\PrometheusClient\KeySerialization\JsonSerializer;
use Zlodes\PrometheusClient\KeySerialization\Serializer;
use Zlodes\PrometheusClient\Metric\Summary;
use Zlodes\PrometheusClient\Registry\Registry;
use Zlodes\PrometheusClient\Storage\DTO\MetricNameWithLabels;
use Zlodes\PrometheusClient\Storage\DTO\MetricValue;
use Zlodes\PrometheusClient\Storage\Storage;
Expand All @@ -23,7 +26,11 @@ final class InMemoryStorage implements Storage
/** @var array<non-empty-string, InMemoryHistogram> */
private array $histogramStorage = [];

/** @var array<non-empty-string, InMemorySummary> */
private array $summaryStorage = [];

public function __construct(
private readonly Registry $registry,
private readonly Serializer $metricKeySerializer = new JsonSerializer(),
) {
}
Expand All @@ -33,12 +40,15 @@ public function fetch(): Generator
yield from $this->fetchGaugeAndCounterMetrics();

yield from $this->fetchHistogramMetrics();

yield from $this->fetchSummaryMetrics();
}

public function clear(): void
{
$this->simpleMetricsStorage = [];
$this->histogramStorage = [];
$this->summaryStorage = [];
}

public function setValue(MetricValue $value): void
Expand Down Expand Up @@ -88,6 +98,18 @@ public function persistHistogram(MetricValue $value, array $buckets): void
$histogram->registerValue($value->value);
}

public function persistSummary(MetricValue $value): void
{
try {
$key = $this->metricKeySerializer->serialize($value->metricNameWithLabels);
} catch (MetricKeySerializationException $e) {
throw new StorageWriteException('Cannot serialize metric key', previous: $e);
}

$summary = $this->summaryStorage[$key] ??= new InMemorySummary();
$summary->push($value->value);
}

/**
* @return Generator<int, MetricValue>
*/
Expand All @@ -110,6 +132,8 @@ private function fetchGaugeAndCounterMetrics(): Generator

/**
* @return Generator<int, MetricValue>
*
* @throws StorageReadException
*/
private function fetchHistogramMetrics(): Generator
{
Expand All @@ -123,10 +147,12 @@ private function fetchHistogramMetrics(): Generator
);
}

$metricName = $keyWithLabels->metricName;

foreach ($histogram->getBuckets() as $bucket => $value) {
yield new MetricValue(
new MetricNameWithLabels(
$keyWithLabels->metricName,
$metricName,
[
...$keyWithLabels->labels,
'le' => (string) $bucket,
Expand All @@ -138,19 +164,73 @@ private function fetchHistogramMetrics(): Generator

yield new MetricValue(
new MetricNameWithLabels(
$keyWithLabels->metricName . '_sum',
$metricName . '_sum',
$keyWithLabels->labels
),
$histogram->getSum()
);

yield new MetricValue(
new MetricNameWithLabels(
$keyWithLabels->metricName . '_count',
$metricName . '_count',
$keyWithLabels->labels
),
$histogram->getCount()
);
}
}

/**
* @return Generator<int, MetricValue>
*
* @throws StorageReadException
*/
private function fetchSummaryMetrics(): Generator
{
foreach ($this->summaryStorage as $serializedKey => $inMemorySummary) {
try {
$keyWithLabels = $this->metricKeySerializer->unserialize($serializedKey);
} catch (MetricKeyUnserializationException $e) {
throw new StorageReadException(
"Fetch error. Cannot unserialize metrics key for key: $serializedKey",
previous: $e
);
}

$metricName = $keyWithLabels->metricName;
$summary = $this->registry->getMetric($metricName, Summary::class);

foreach ($summary->getQuantiles() as $quantile) {
$quantileValue = $inMemorySummary->getQuantile($quantile);
Assert::notNull($quantileValue);

yield new MetricValue(
new MetricNameWithLabels(
$metricName,
[
...$keyWithLabels->labels,
'quantile' => (string) $quantile,
]
),
$quantileValue
);
}

yield new MetricValue(
new MetricNameWithLabels(
$metricName . '_sum',
$keyWithLabels->labels
),
$inMemorySummary->getSum()
);

yield new MetricValue(
new MetricNameWithLabels(
$metricName . '_count',
$keyWithLabels->labels
),
$inMemorySummary->getCount()
);
}
}
}
Loading

0 comments on commit b26bc3e

Please sign in to comment.