<?php
declare(strict_types=1);
namespace Monitoring\Metric\Clients;
use Config\Env;
use InvalidArgumentException;
use ProxyManager\Exception\DisabledMethodException;
use RuntimeException;
class StatsdClient implements ClientInterface
{
public const DEFAULT_EVENT_METRIC = 'event';
public const METRIC_NAME_REGEXP = '|^[a-z]+[a-z0-9_.]{3,}$|iu';
private Connection\ConnectionInterface $connection;
private string $namespace;
private string $eventMetric;
public function __construct(array $config)
{
if (empty($config['dvp-enabled']) && Env::isDvp()) {
throw new DisabledMethodException('Disabled on DVP');
}
if (empty($config['statsd'])) {
throw new DisabledMethodException('No statsd section in config');
}
if (!extension_loaded('sockets')) {
throw new RuntimeException('PHP sockets extension is required');
}
$config = $config['statsd'];
if (empty($config['host'])) {
throw new InvalidArgumentException('Host is required in config');
}
$this->connection = (($config['mode'] ?? null) === 'tcp')
? new Connection\TcpSocket($config['host'], $config['port'] ?? 8125, $config['timeout'] ?? null, !empty($config['persistent']))
: new Connection\UdpSocket($config['host'], $config['port'] ?? 8126, $config['timeout'] ?? null, !empty($config['persistent']));
$this->namespace = $config['namespace'] ?? '';
$this->eventMetric = $config['event_metric'] ?? self::DEFAULT_EVENT_METRIC;
}
public function increment(string $metric, int $value, array $tags = null): void
{
}
public function gauge(string $metric, float $value, array $tags = []): void
{
$this->send($metric, $value, 'g', $tags);
}
public function timing(string $metric, float $value, array $tags = []): void
{
$this->send($metric, $value, 'ms', $tags);
}
public function set(string $metric, int $value, array $tags = []): void
{
$this->send($metric, $value, 's', $tags);
}
public function histogram(string $metric, float $value, array $tags = []): void
{
// Statsd cannot send histogram, we will send using gauge and process it on Grafana side
$this->gauge($metric, $value, $tags);
}
public function event(string $title, string $event, array $tags = []): void
{
// Prometheus cannot send events, emulate via increment with additional tags
$tags['title'] = $title;
$tags['event'] = $event;
$this->count($this->eventMetric, 1, $tags);
}
public function count(string $metric, float $value, array $tags = []): void
{
$this->send($metric, $value, 'c', $tags);
}
private function send(string $metric, $value, string $type, array $tags = []): void
{
$this->validateMetricName($metric);
if (!empty($this->namespace)) {
$metric = $this->namespace . '.' . $metric;
}
$message = $metric . ':' . $value . '|' . $type;
if (!empty($tags)) {
$tagArray = [];
foreach ($tags as $tagName => $tagValue) {
$tagArray[] = ($tagName . ':' . $tagValue);
}
$message .= '|#' . implode(',', $tagArray);
}
$this->connection->send($message);
}
private function validateMetricName(string $metric): void
{
if (substr($metric, -1) === '.') {
return;
}
if (preg_match(self::METRIC_NAME_REGEXP, $metric)) {
return;
}
throw new ValidateException("Invalid metric name: $metric");
}
}