Skip to content

Commit

Permalink
Metrics plugin implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
luzrain committed Nov 12, 2024
1 parent 1d3116e commit d7be7ae
Show file tree
Hide file tree
Showing 25 changed files with 1,034 additions and 12 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"amphp/http-server": "^3.3.1",
"amphp/socket": "^2.3.1",
"luzrain/polyfill-inotify": "^1.0",
"promphp/prometheus_client_php": "^2.12",
"psr/container": "^2.0",
"psr/http-message": "^2.0",
"psr/log": "^3.0",
Expand Down
2 changes: 1 addition & 1 deletion src/BundledPlugin/HttpServer/HttpServerProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public function handleRequest(Request $request): Response
/**
* @return list<Listen>
*/
private static function normalizeListenList(self|string|array $listen): array
private static function normalizeListenList(Listen|string|array $listen): array
{
$listen = \is_array($listen) ? $listen : [$listen];
$ret = [];
Expand Down
22 changes: 13 additions & 9 deletions src/BundledPlugin/HttpServer/Internal/HttpServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public function start(): void
);

foreach ($this->listen as $listen) {
$socketHttpServer->expose(...$this->createInternetAddressAndContext($listen));
$socketHttpServer->expose(...self::createInternetAddressAndContext($listen, true, self::DEFAULT_TCP_BACKLOG));
}

$socketHttpServer->start($this->requestHandler, $errorHandler);
Expand All @@ -122,20 +122,24 @@ public function start(): void
/**
* @return array{0: InternetAddress, 1: BindContext}
*/
private function createInternetAddressAndContext(Listen $listen): array
public static function createInternetAddressAndContext(Listen $listen, bool $reusePort = false, int $backlog = 0): array
{
$internetAddress = new InternetAddress($listen->host, $listen->port);
$context = new BindContext();

$context = (new BindContext())
->withReusePort()
->withBacklog(self::DEFAULT_TCP_BACKLOG)
;
if ($reusePort) {
$context = $context->withReusePort();
}

if ($backlog > 0) {
$context = $context->withBacklog($backlog);
}

if ($listen->tls) {
\assert($listen->tlsCertificate !== null);
$context = $context->withTlsContext(
(new ServerTlsContext())->withDefaultCertificate(new Certificate($listen->tlsCertificate, $listen->tlsCertificateKey)),
);
$cert = new Certificate($listen->tlsCertificate, $listen->tlsCertificateKey);
$tlsContext = (new ServerTlsContext())->withDefaultCertificate($cert);
$context = $context->withTlsContext($tlsContext);
}

return [$internetAddress, $context];
Expand Down
6 changes: 6 additions & 0 deletions src/BundledPlugin/HttpServer/Listen.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ public function __construct(
throw new \InvalidArgumentException('Certificate file must be provided');
}
}

public function getAddress(): string
{
return ($this->tls ? 'https://' : 'http://') . $this->host .
(($this->tls && $this->port === 443) || (!$this->tls && $this->port === 80) ? '' : ':' . $this->port);
}
}
49 changes: 49 additions & 0 deletions src/BundledPlugin/Metrics/Counter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Luzrain\PHPStreamServer\BundledPlugin\Metrics;

use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Exception\LabelsNotMatchException;
use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Internal\Message\IncreaseCounterMessage;
use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Internal\Metric;
use Revolt\EventLoop;

final class Counter extends Metric
{
private array $buffer = [];

/**
* @param array<string, string> $labels
* @throws LabelsNotMatchException
*/
public function inc(array $labels = []): void
{
$this->add(1, $labels);
}

/**
* @param array<string, string> $labels
* @throws LabelsNotMatchException
*/
public function add(int $value, array $labels = []): void
{
$this->checkLabels($labels);

$key = \hash('xxh128', \json_encode($labels));
$this->buffer[$key] ??= [0, ''];
$buffer = &$this->buffer[$key][0];
$callbackId = &$this->buffer[$key][1];
$buffer += $value;

if ($callbackId !== '') {
return;
}

$callbackId = EventLoop::delay(self::FLUSH_TIMEOUT, function() use($labels, &$buffer, $key) {
$value = $buffer;
unset($this->buffer[$key]);
$this->messageBus->dispatch(new IncreaseCounterMessage($this->namespace, $this->name, $labels, $value));
});
}
}
21 changes: 21 additions & 0 deletions src/BundledPlugin/Metrics/Exception/LabelsNotMatchException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Luzrain\PHPStreamServer\BundledPlugin\Metrics\Exception;

final class LabelsNotMatchException extends \InvalidArgumentException
{
public function __construct(array $labels, array $givenLabels)
{
if ($labels === [] && $givenLabels !== []) {
$text = \sprintf('Labels do not match. Should not contain labels, %s assigned', \json_encode($givenLabels));
} else if($labels !== [] && $givenLabels === []) {
$text = \sprintf('Labels do not match. Should contain %s labels, no labels assigned', \json_encode($labels));
} else {
$text = \sprintf('Labels do not match. Should contain %s labels, %s assigned', \json_encode($labels), \json_encode($givenLabels));
}

parent::__construct($text);
}
}
13 changes: 13 additions & 0 deletions src/BundledPlugin/Metrics/Exception/MetricNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Luzrain\PHPStreamServer\BundledPlugin\Metrics\Exception;

final class MetricNotFoundException extends \InvalidArgumentException
{
public function __construct(string $type, string $namespace, string $name)
{
parent::__construct(\sprintf('%s metric "%s_%s" not found', \ucfirst($type), $namespace, $name));
}
}
92 changes: 92 additions & 0 deletions src/BundledPlugin/Metrics/Gauge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Luzrain\PHPStreamServer\BundledPlugin\Metrics;

use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Exception\LabelsNotMatchException;
use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Internal\Message\SetGaugeMessage;
use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Internal\Metric;
use Revolt\EventLoop;

final class Gauge extends Metric
{
private array $buffer = [];

/**
* @param array<string, string> $labels
* @throws LabelsNotMatchException
*/
public function set(float $value, array $labels = []): void
{
$this->checkLabels($labels);

$key = \hash('xxh128', \json_encode($labels).'set');
$this->buffer[$key] ??= [0, ''];
$buffer = &$this->buffer[$key][0];
$callbackId = &$this->buffer[$key][1];
$buffer = $value;

if ($callbackId !== '') {
return;
}

$callbackId = EventLoop::delay(self::FLUSH_TIMEOUT, function() use($labels, &$buffer, $key) {
$value = $buffer;
unset($this->buffer[$key]);
$this->messageBus->dispatch(new SetGaugeMessage($this->namespace, $this->name, $labels, $value, false));
});
}

/**
* @param array<string, string> $labels
* @throws LabelsNotMatchException
*/
public function inc(array $labels = []): void
{
$this->add(1, $labels);
}

/**
* @param array<string, string> $labels
* @throws LabelsNotMatchException
*/
public function dec(array $labels = []): void
{
$this->add(-1, $labels);
}

/**
* @param array<string, string> $labels
* @throws LabelsNotMatchException
*/
public function add(float $value, array $labels = []): void
{
$this->checkLabels($labels);

$key = \hash('xxh128', \json_encode($labels).'add');
$this->buffer[$key] ??= [0, ''];
$buffer = &$this->buffer[$key][0];
$callbackId = &$this->buffer[$key][1];
$buffer += $value;

if ($callbackId !== '') {
return;
}

$callbackId = EventLoop::delay(self::FLUSH_TIMEOUT, function() use($labels, &$buffer, $key) {
$value = $buffer;
unset($this->buffer[$key]);
$this->messageBus->dispatch(new SetGaugeMessage($this->namespace, $this->name, $labels, $value, true));
});
}

/**
* @param array<string, string> $labels
* @throws LabelsNotMatchException
*/
public function sub(float $value, array $labels = []): void
{
$this->add(-$value, $labels);
}
}
92 changes: 92 additions & 0 deletions src/BundledPlugin/Metrics/Histogram.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Luzrain\PHPStreamServer\BundledPlugin\Metrics;

use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Exception\LabelsNotMatchException;
use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Internal\Message\ObserveHistorgamMessage;
use Luzrain\PHPStreamServer\BundledPlugin\Metrics\Internal\Metric;
use Revolt\EventLoop;

final class Histogram extends Metric
{
private array $buffer = [];

/**
* @param array<string, string> $labels
* @throws LabelsNotMatchException
*/
public function observe(float $value, array $labels = []): void
{
$this->checkLabels($labels);

$key = \hash('xxh128', \json_encode($labels));
$this->buffer[$key] ??= [[], ''];
$buffer = &$this->buffer[$key][0];
$callbackId = &$this->buffer[$key][1];
$buffer[] = $value;

if ($callbackId !== '') {
return;
}

$callbackId = EventLoop::delay(self::FLUSH_TIMEOUT, function() use($labels, &$buffer, $key) {
$values = $buffer;
unset($this->buffer[$key]);
$this->messageBus->dispatch(new ObserveHistorgamMessage($this->namespace, $this->name, $labels, $values));
});
}

/**
* Creates count buckets, where the lowest bucket has an upper bound of start and each following bucket's upper
* bound is factor times the previous bucket's upper bound.
* The returned slice is meant to be used for the Buckets field of HistogramOpts.
*
* @return list<float>
*/
public static function exponentialBuckets(float $start, float $factor, int $count): array
{
$start > 0 ?: throw new \InvalidArgumentException('$start must be a positive integer');
$factor > 0 ?: throw new \InvalidArgumentException('$factor must greater than 1');
$count >= 1 ?: throw new \InvalidArgumentException('$count must be a positive integer');

$buckets = [];
for ($i = 0; $i < $count; $i++) {
$buckets[] = $start;
$start *= $factor;
}

return $buckets;
}

/**
* Creates count regular buckets, each width wide, where the lowest bucket has an upper bound of start.
* The returned slice is meant to be used for the Buckets field of HistogramOpts.
*
* @return list<float>
*/
public static function linearBuckets(float $start, float $width, int $count): array
{
$width > 0 ?: throw new \InvalidArgumentException('$width must greater than 1');
$count >= 1 ?: throw new \InvalidArgumentException('$count must be a positive integer');

$buckets = [];
for ($i = 0; $i < $count; $i++) {
$buckets[] = $start;
$start += $width;
}

return $buckets;
}

/**
* Creates default buckets.
*
* @return list<float>
*/
public static function defaultBuckets(): array
{
return [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
}
}
45 changes: 45 additions & 0 deletions src/BundledPlugin/Metrics/Internal/Message/GetMetricMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Luzrain\PHPStreamServer\BundledPlugin\Metrics\Internal\Message;

use Luzrain\PHPStreamServer\MessageBus\MessageInterface;

/**
* @implements MessageInterface<GetMetricResponse|false>
*/
final readonly class GetMetricMessage implements MessageInterface
{
public const TYPE_COUNTER = 'counter';
public const TYPE_GAUGE = 'gauge';
public const TYPE_HISTOGRAM = 'histogram';
public const TYPE_SUMMARY = 'summary';

private function __construct(
public string $type,
public string $namespace,
public string $name,
) {
}

public static function counter(string $namespace, string $name): self
{
return new self(self::TYPE_COUNTER, $namespace, $name);
}

public static function gauge(string $namespace, string $name): self
{
return new self(self::TYPE_GAUGE, $namespace, $name);
}

public static function histogram(string $namespace, string $name): self
{
return new self(self::TYPE_HISTOGRAM, $namespace, $name);
}

public static function summary(string $namespace, string $name): self
{
return new self(self::TYPE_SUMMARY, $namespace, $name);
}
}
Loading

0 comments on commit d7be7ae

Please sign in to comment.