|
- <?php
- namespace VPSManager;
-
- require_once __DIR__ . '/Config.php';
-
- final class Graphite
- {
- public const RANGES = ['1h' => '-1h', '6h' => '-6h', '24h' => '-24h', '7d' => '-7d', '30d' => '-30d'];
- private const SUFFIXES = [
- 'load1' => 'load.load.shortterm',
- 'load5' => 'load.load.midterm',
- 'load15' => 'load.load.longterm',
- 'used' => 'memory.memory-used',
- 'free' => 'memory.memory-free',
- 'rx' => 'interface-eth0.if_octets.rx',
- 'tx' => 'interface-eth0.if_octets.tx',
- ];
- private $baseUrl;
- private $transport;
-
- public function __construct(array $config, ?callable $transport = null)
- {
- $this->baseUrl = Config::url($config, 'graphite_url');
- $this->transport = $transport ?? [self::class, 'request'];
- }
-
- public static function range($value): string
- {
- return is_string($value) && isset(self::RANGES[$value]) ? $value : '24h';
- }
-
- public static function validUuid($uuid): bool
- {
- return is_string($uuid) && preg_match('/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i', $uuid) === 1;
- }
-
- // Only WHMCS-authorized module parameters may be passed here; never request UUID/type.
- public function getForService(array $params, $range = '24h'): array
- {
- $fields = $params['customfields'] ?? [];
- if (($fields['vtype'] ?? null) !== 'lx' || !self::validUuid($fields['uuid'] ?? null)) {
- throw new \InvalidArgumentException('Servicio no compatible con las métricas LX.');
- }
- $range = self::range($range);
- $targets = [];
- foreach (self::SUFFIXES as $key => $suffix) {
- $targets[$key] = 'lx.' . $fields['uuid'] . '.' . $suffix;
- }
- $query = 'format=json&from=' . rawurlencode(self::RANGES[$range]) . '&until=now&maxDataPoints=1000';
- foreach ($targets as $target) {
- $query .= '&target=' . rawurlencode($target);
- }
- $response = call_user_func($this->transport, $this->baseUrl . '/render/?' . $query);
- if (!is_array($response) || ($response['status'] ?? 0) !== 200 || !is_string($response['body'] ?? null)) {
- throw new \RuntimeException('Graphite HTTP error.');
- }
- $raw = json_decode($response['body'], true, 32);
- if (substr(ltrim($response['body']), 0, 1) !== '[' || json_last_error() !== JSON_ERROR_NONE || !is_array($raw) || !array_is_list($raw)) {
- throw new \RuntimeException('Graphite JSON error.');
- }
- $series = array_fill_keys(array_keys($targets), []);
- $seen = [];
- foreach ($raw as $row) {
- if (!is_array($row) || !is_string($row['target'] ?? null)) {
- throw new \RuntimeException('Graphite series error.');
- }
- $key = array_search($row['target'], $targets, true);
- if ($key === false) {
- continue; // Never expose unrequested targets or metadata.
- }
- if (isset($seen[$key]) || !is_array($row['datapoints'] ?? null) || count($row['datapoints']) > 10000) {
- throw new \RuntimeException('Graphite series error.');
- }
- $seen[$key] = true;
- $points = [];
- foreach ($row['datapoints'] as $point) {
- if (!is_array($point) || count($point) !== 2 || !array_is_list($point)
- || !is_int($point[1]) || $point[1] < 0 || $point[1] > 253402300799) {
- throw new \RuntimeException('Graphite datapoint error.');
- }
- $value = $point[0];
- if ($value !== null && ((!is_int($value) && !is_float($value)) || !is_finite((float) $value) || $value < 0)) {
- throw new \RuntimeException('Graphite value error.');
- }
- if (array_key_exists($point[1], $points)) {
- throw new \RuntimeException('Graphite duplicate timestamp.');
- }
- $points[$point[1]] = $value;
- }
- ksort($points, SORT_NUMERIC);
- foreach ($points as $timestamp => $value) {
- $series[$key][] = ['x' => $timestamp * 1000, 'y' => $value];
- }
- }
- // Align memory by timestamps, never by index; absent data stays null.
- $used = array_column($series['used'], 'y', 'x');
- $free = array_column($series['free'], 'y', 'x');
- $times = array_unique(array_merge(array_keys($used), array_keys($free)));
- sort($times, SORT_NUMERIC);
- $total = [];
- $percent = [];
- foreach ($times as $time) {
- $u = $used[$time] ?? null;
- $f = $free[$time] ?? null;
- $sum = $u !== null && $f !== null ? $u + $f : null;
- if ($sum !== null && !is_finite((float) $sum)) {
- throw new \RuntimeException('Graphite memory overflow.');
- }
- $total[] = ['x' => $time, 'y' => $sum];
- $percent[] = ['x' => $time, 'y' => $sum !== null && $sum > 0 ? ($u / $sum) * 100 : null];
- }
- return [
- 'range' => $range,
- 'load' => ['1 min' => $series['load1'], '5 min' => $series['load5'], '15 min' => $series['load15']],
- 'memory' => ['Usada' => $series['used'], 'Total' => $total],
- 'memoryPercent' => $percent,
- 'network' => ['RX' => $series['rx'], 'TX' => $series['tx']], // bytes/s; presentation converts once.
- ];
- }
-
- private static function request(string $url): array
- {
- if (!function_exists('curl_init')) {
- throw new \RuntimeException('Graphite transport unavailable.');
- }
- $ch = curl_init($url);
- $body = '';
- curl_setopt_array($ch, [
- CURLOPT_CONNECTTIMEOUT => 3,
- CURLOPT_TIMEOUT => 5,
- CURLOPT_FOLLOWLOCATION => false,
- CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
- CURLOPT_SSL_VERIFYPEER => true,
- CURLOPT_SSL_VERIFYHOST => 2,
- CURLOPT_HTTPHEADER => ['Accept: application/json'],
- CURLOPT_WRITEFUNCTION => static function ($handle, $chunk) use (&$body) {
- if (strlen($body) + strlen($chunk) > 2097152) {
- return 0;
- }
- $body .= $chunk;
- return strlen($chunk);
- },
- ]);
- try {
- $ok = curl_exec($ch);
- $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- if ($ok === false) {
- throw new \RuntimeException('Graphite transport error.');
- }
- return ['status' => $status, 'body' => $body];
- } finally {
- curl_close($ch);
- }
- }
- }
|