fix: prevent metric charts from freezing on page navigation (#7848)
This commit is contained in:
commit
b448b08058
16 changed files with 568 additions and 943 deletions
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Services\ConfigurationGenerator;
|
use App\Services\ConfigurationGenerator;
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasConfiguration;
|
use App\Traits\HasConfiguration;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -111,7 +112,7 @@
|
||||||
|
|
||||||
class Application extends BaseModel
|
class Application extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
private static $parserVersion = '5';
|
private static $parserVersion = '5';
|
||||||
|
|
||||||
|
|
@ -1977,54 +1978,6 @@ public static function getDomainsByUuid(string $uuid): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
if ($server->isMetricsEnabled()) {
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
if ($server->isMetricsEnabled()) {
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLimits(): array
|
public function getLimits(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
use App\Notifications\Server\Unreachable;
|
use App\Notifications\Server\Unreachable;
|
||||||
use App\Services\ConfigurationRepository;
|
use App\Services\ConfigurationRepository;
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
|
@ -103,7 +104,7 @@
|
||||||
|
|
||||||
class Server extends BaseModel
|
class Server extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, SchemalessAttributesTrait, SoftDeletes;
|
||||||
|
|
||||||
public static $batch_counter = 0;
|
public static $batch_counter = 0;
|
||||||
|
|
||||||
|
|
@ -667,141 +668,6 @@ public function checkSentinel()
|
||||||
CheckAndStartSentinelJob::dispatch($this);
|
CheckAndStartSentinelJob::dispatch($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
if ($this->isMetricsEnabled()) {
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
|
|
||||||
if (str($cpu)->contains('error')) {
|
|
||||||
$error = json_decode($cpu, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$cpu = json_decode($cpu, true);
|
|
||||||
|
|
||||||
$metrics = collect($cpu)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
})->toArray();
|
|
||||||
|
|
||||||
// Downsample for intervals > 60 minutes to prevent browser freeze
|
|
||||||
if ($mins > 60 && count($metrics) > 1000) {
|
|
||||||
$metrics = $this->downsampleLTTB($metrics, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return collect($metrics);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
if ($this->isMetricsEnabled()) {
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
|
|
||||||
if (str($memory)->contains('error')) {
|
|
||||||
$error = json_decode($memory, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$memory = json_decode($memory, true);
|
|
||||||
$metrics = collect($memory)->map(function ($metric) {
|
|
||||||
$usedPercent = $metric['usedPercent'] ?? 0.0;
|
|
||||||
|
|
||||||
return [(int) $metric['time'], (float) $usedPercent];
|
|
||||||
})->toArray();
|
|
||||||
|
|
||||||
// Downsample for intervals > 60 minutes to prevent browser freeze
|
|
||||||
if ($mins > 60 && count($metrics) > 1000) {
|
|
||||||
$metrics = $this->downsampleLTTB($metrics, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return collect($metrics);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downsample metrics using the Largest-Triangle-Three-Buckets (LTTB) algorithm.
|
|
||||||
* This preserves the visual shape of the data better than simple averaging.
|
|
||||||
*
|
|
||||||
* @param array $data Array of [timestamp, value] pairs
|
|
||||||
* @param int $threshold Target number of points
|
|
||||||
* @return array Downsampled data
|
|
||||||
*/
|
|
||||||
private function downsampleLTTB(array $data, int $threshold): array
|
|
||||||
{
|
|
||||||
$dataLength = count($data);
|
|
||||||
|
|
||||||
// Return unchanged if threshold >= data length, or if threshold <= 2
|
|
||||||
// (threshold <= 2 would cause division by zero in bucket calculation)
|
|
||||||
if ($threshold >= $dataLength || $threshold <= 2) {
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sampled = [];
|
|
||||||
$sampled[] = $data[0]; // Always keep first point
|
|
||||||
|
|
||||||
$bucketSize = ($dataLength - 2) / ($threshold - 2);
|
|
||||||
|
|
||||||
$a = 0; // Index of previous selected point
|
|
||||||
|
|
||||||
for ($i = 0; $i < $threshold - 2; $i++) {
|
|
||||||
// Calculate bucket range
|
|
||||||
$bucketStart = (int) floor(($i + 1) * $bucketSize) + 1;
|
|
||||||
$bucketEnd = (int) floor(($i + 2) * $bucketSize) + 1;
|
|
||||||
$bucketEnd = min($bucketEnd, $dataLength - 1);
|
|
||||||
|
|
||||||
// Calculate average point for next bucket (used as reference)
|
|
||||||
$nextBucketStart = (int) floor(($i + 2) * $bucketSize) + 1;
|
|
||||||
$nextBucketEnd = (int) floor(($i + 3) * $bucketSize) + 1;
|
|
||||||
$nextBucketEnd = min($nextBucketEnd, $dataLength - 1);
|
|
||||||
|
|
||||||
$avgX = 0;
|
|
||||||
$avgY = 0;
|
|
||||||
$nextBucketCount = $nextBucketEnd - $nextBucketStart + 1;
|
|
||||||
|
|
||||||
if ($nextBucketCount > 0) {
|
|
||||||
for ($j = $nextBucketStart; $j <= $nextBucketEnd; $j++) {
|
|
||||||
$avgX += $data[$j][0];
|
|
||||||
$avgY += $data[$j][1];
|
|
||||||
}
|
|
||||||
$avgX /= $nextBucketCount;
|
|
||||||
$avgY /= $nextBucketCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find point in current bucket with largest triangle area
|
|
||||||
$maxArea = -1;
|
|
||||||
$maxAreaIndex = $bucketStart;
|
|
||||||
|
|
||||||
$pointAX = $data[$a][0];
|
|
||||||
$pointAY = $data[$a][1];
|
|
||||||
|
|
||||||
for ($j = $bucketStart; $j <= $bucketEnd; $j++) {
|
|
||||||
// Triangle area calculation
|
|
||||||
$area = abs(
|
|
||||||
($pointAX - $avgX) * ($data[$j][1] - $pointAY) -
|
|
||||||
($pointAX - $data[$j][0]) * ($avgY - $pointAY)
|
|
||||||
) * 0.5;
|
|
||||||
|
|
||||||
if ($area > $maxArea) {
|
|
||||||
$maxArea = $area;
|
|
||||||
$maxAreaIndex = $j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$sampled[] = $data[$maxAreaIndex];
|
|
||||||
$a = $maxAreaIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sampled[] = $data[$dataLength - 1]; // Always keep last point
|
|
||||||
|
|
||||||
return $sampled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDiskUsage(): ?string
|
public function getDiskUsage(): ?string
|
||||||
{
|
{
|
||||||
return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
|
return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
|
|
||||||
class StandaloneClickhouse extends BaseModel
|
class StandaloneClickhouse extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
|
@ -320,50 +321,6 @@ public function scheduledBackups()
|
||||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isBackupSolutionAvailable()
|
public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
|
|
||||||
class StandaloneDragonfly extends BaseModel
|
class StandaloneDragonfly extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
|
@ -316,50 +317,6 @@ public function scheduledBackups()
|
||||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isBackupSolutionAvailable()
|
public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
|
|
||||||
class StandaloneKeydb extends BaseModel
|
class StandaloneKeydb extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
|
@ -316,50 +317,6 @@ public function scheduledBackups()
|
||||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isBackupSolutionAvailable()
|
public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -11,7 +12,7 @@
|
||||||
|
|
||||||
class StandaloneMariadb extends BaseModel
|
class StandaloneMariadb extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
|
@ -319,50 +320,6 @@ public function sslCertificates()
|
||||||
return $this->morphMany(SslCertificate::class, 'resource');
|
return $this->morphMany(SslCertificate::class, 'resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isBackupSolutionAvailable()
|
public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
|
|
||||||
class StandaloneMongodb extends BaseModel
|
class StandaloneMongodb extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
|
@ -341,50 +342,6 @@ public function scheduledBackups()
|
||||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isBackupSolutionAvailable()
|
public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
|
|
||||||
class StandaloneMysql extends BaseModel
|
class StandaloneMysql extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
|
@ -320,50 +321,6 @@ public function scheduledBackups()
|
||||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isBackupSolutionAvailable()
|
public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
|
|
||||||
class StandalonePostgresql extends BaseModel
|
class StandalonePostgresql extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
|
@ -337,51 +338,4 @@ public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [
|
|
||||||
(int) $metric['time'],
|
|
||||||
(float) ($metric['percent'] ?? 0.0),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
|
use App\Traits\HasMetrics;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
|
|
||||||
class StandaloneRedis extends BaseModel
|
class StandaloneRedis extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
|
@ -332,50 +333,6 @@ public function scheduledBackups()
|
||||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCpuMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['percent']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMemoryMetrics(int $mins = 5)
|
|
||||||
{
|
|
||||||
$server = $this->destination->server;
|
|
||||||
$container_name = $this->uuid;
|
|
||||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
||||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
||||||
if (str($metrics)->contains('error')) {
|
|
||||||
$error = json_decode($metrics, true);
|
|
||||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
||||||
if ($error === 'Unauthorized') {
|
|
||||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
||||||
}
|
|
||||||
throw new \Exception($error);
|
|
||||||
}
|
|
||||||
$metrics = json_decode($metrics, true);
|
|
||||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
||||||
return [(int) $metric['time'], (float) $metric['used']];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $parsedCollection->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isBackupSolutionAvailable()
|
public function isBackupSolutionAvailable()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
74
app/Traits/HasMetrics.php
Normal file
74
app/Traits/HasMetrics.php
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
trait HasMetrics
|
||||||
|
{
|
||||||
|
public function getCpuMetrics(int $mins = 5): ?array
|
||||||
|
{
|
||||||
|
return $this->getMetrics('cpu', $mins, 'percent');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMemoryMetrics(int $mins = 5): ?array
|
||||||
|
{
|
||||||
|
$field = $this->isServerMetrics() ? 'usedPercent' : 'used';
|
||||||
|
|
||||||
|
return $this->getMetrics('memory', $mins, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMetrics(string $type, int $mins, string $valueField): ?array
|
||||||
|
{
|
||||||
|
$server = $this->getMetricsServer();
|
||||||
|
if (! $server->isMetricsEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||||
|
$endpoint = $this->getMetricsEndpoint($type, $from);
|
||||||
|
|
||||||
|
$response = instant_remote_process(
|
||||||
|
["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" {$endpoint}'"],
|
||||||
|
$server,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (str($response)->contains('error')) {
|
||||||
|
$error = json_decode($response, true);
|
||||||
|
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||||
|
if ($error === 'Unauthorized') {
|
||||||
|
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||||
|
}
|
||||||
|
throw new \Exception($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metrics = collect(json_decode($response, true))->map(function ($metric) use ($valueField) {
|
||||||
|
return [(int) $metric['time'], (float) ($metric[$valueField] ?? 0.0)];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
if ($mins > 60 && count($metrics) > 1000) {
|
||||||
|
$metrics = downsampleLTTB($metrics, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isServerMetrics(): bool
|
||||||
|
{
|
||||||
|
return $this instanceof \App\Models\Server;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMetricsServer(): \App\Models\Server
|
||||||
|
{
|
||||||
|
return $this->isServerMetrics() ? $this : $this->destination->server;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMetricsEndpoint(string $type, string $from): string
|
||||||
|
{
|
||||||
|
$base = 'http://localhost:8888/api';
|
||||||
|
if ($this->isServerMetrics()) {
|
||||||
|
return "{$base}/{$type}/history?from={$from}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{$base}/container/{$this->uuid}/{$type}/history?from={$from}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3416,3 +3416,81 @@ function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $compon
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downsample metrics using the Largest-Triangle-Three-Buckets (LTTB) algorithm.
|
||||||
|
* This preserves the visual shape of the data better than simple averaging.
|
||||||
|
*
|
||||||
|
* @param array $data Array of [timestamp, value] pairs
|
||||||
|
* @param int $threshold Target number of points
|
||||||
|
* @return array Downsampled data
|
||||||
|
*/
|
||||||
|
function downsampleLTTB(array $data, int $threshold): array
|
||||||
|
{
|
||||||
|
$dataLength = count($data);
|
||||||
|
|
||||||
|
// Return unchanged if threshold >= data length, or if threshold <= 2
|
||||||
|
// (threshold <= 2 would cause division by zero in bucket calculation)
|
||||||
|
if ($threshold >= $dataLength || $threshold <= 2) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sampled = [];
|
||||||
|
$sampled[] = $data[0]; // Always keep first point
|
||||||
|
|
||||||
|
$bucketSize = ($dataLength - 2) / ($threshold - 2);
|
||||||
|
|
||||||
|
$a = 0; // Index of previous selected point
|
||||||
|
|
||||||
|
for ($i = 0; $i < $threshold - 2; $i++) {
|
||||||
|
// Calculate bucket range
|
||||||
|
$bucketStart = (int) floor(($i + 1) * $bucketSize) + 1;
|
||||||
|
$bucketEnd = (int) floor(($i + 2) * $bucketSize) + 1;
|
||||||
|
$bucketEnd = min($bucketEnd, $dataLength - 1);
|
||||||
|
|
||||||
|
// Calculate average point for next bucket (used as reference)
|
||||||
|
$nextBucketStart = (int) floor(($i + 2) * $bucketSize) + 1;
|
||||||
|
$nextBucketEnd = (int) floor(($i + 3) * $bucketSize) + 1;
|
||||||
|
$nextBucketEnd = min($nextBucketEnd, $dataLength - 1);
|
||||||
|
|
||||||
|
$avgX = 0;
|
||||||
|
$avgY = 0;
|
||||||
|
$nextBucketCount = $nextBucketEnd - $nextBucketStart + 1;
|
||||||
|
|
||||||
|
if ($nextBucketCount > 0) {
|
||||||
|
for ($j = $nextBucketStart; $j <= $nextBucketEnd; $j++) {
|
||||||
|
$avgX += $data[$j][0];
|
||||||
|
$avgY += $data[$j][1];
|
||||||
|
}
|
||||||
|
$avgX /= $nextBucketCount;
|
||||||
|
$avgY /= $nextBucketCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find point in current bucket with largest triangle area
|
||||||
|
$maxArea = -1;
|
||||||
|
$maxAreaIndex = $bucketStart;
|
||||||
|
|
||||||
|
$pointAX = $data[$a][0];
|
||||||
|
$pointAY = $data[$a][1];
|
||||||
|
|
||||||
|
for ($j = $bucketStart; $j <= $bucketEnd; $j++) {
|
||||||
|
// Triangle area calculation
|
||||||
|
$area = abs(
|
||||||
|
($pointAX - $avgX) * ($data[$j][1] - $pointAY) -
|
||||||
|
($pointAX - $data[$j][0]) * ($avgY - $pointAY)
|
||||||
|
) * 0.5;
|
||||||
|
|
||||||
|
if ($area > $maxArea) {
|
||||||
|
$maxArea = $area;
|
||||||
|
$maxAreaIndex = $j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sampled[] = $data[$maxAreaIndex];
|
||||||
|
$a = $maxAreaIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sampled[] = $data[$dataLength - 1]; // Always keep last point
|
||||||
|
|
||||||
|
return $sampled;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,230 +29,179 @@ class="pt-5">
|
||||||
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
checkTheme();
|
(function() {
|
||||||
const optionsServerCpu = {
|
checkTheme();
|
||||||
stroke: {
|
const optionsServerCpu = {
|
||||||
curve: 'straight',
|
stroke: {
|
||||||
width: 2,
|
curve: 'straight',
|
||||||
},
|
width: 2,
|
||||||
chart: {
|
},
|
||||||
height: '150px',
|
chart: {
|
||||||
id: '{!! $chartId !!}-cpu',
|
height: '150px',
|
||||||
type: 'area',
|
id: '{!! $chartId !!}-cpu',
|
||||||
toolbar: {
|
type: 'area',
|
||||||
show: true,
|
toolbar: {
|
||||||
tools: {
|
show: true,
|
||||||
download: false,
|
tools: {
|
||||||
selection: false,
|
download: false,
|
||||||
zoom: true,
|
selection: false,
|
||||||
zoomin: false,
|
zoom: true,
|
||||||
zoomout: false,
|
zoomin: false,
|
||||||
pan: false,
|
zoomout: false,
|
||||||
reset: true
|
pan: false,
|
||||||
|
reset: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animations: {
|
fill: {
|
||||||
enabled: true,
|
type: 'gradient',
|
||||||
},
|
},
|
||||||
},
|
dataLabels: {
|
||||||
fill: {
|
|
||||||
type: 'gradient',
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false,
|
|
||||||
offsetY: -10,
|
|
||||||
style: {
|
|
||||||
colors: ['#FCD452'],
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
}
|
offsetY: -10,
|
||||||
},
|
style: {
|
||||||
grid: {
|
colors: ['#FCD452'],
|
||||||
show: true,
|
},
|
||||||
borderColor: '',
|
background: {
|
||||||
},
|
enabled: false,
|
||||||
colors: [cpuColor],
|
}
|
||||||
xaxis: {
|
},
|
||||||
type: 'datetime',
|
grid: {
|
||||||
},
|
show: true,
|
||||||
series: [{
|
borderColor: '',
|
||||||
name: "CPU %",
|
|
||||||
data: []
|
|
||||||
}],
|
|
||||||
noData: {
|
|
||||||
text: 'Loading...',
|
|
||||||
style: {
|
|
||||||
color: textColor,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true,
|
|
||||||
marker: {
|
|
||||||
show: false,
|
|
||||||
},
|
},
|
||||||
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
colors: [cpuColor],
|
||||||
const value = series[seriesIndex][dataPointIndex];
|
|
||||||
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
|
||||||
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
|
||||||
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
|
||||||
date.getUTCFullYear() + '-' +
|
|
||||||
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(date.getUTCDate()).padStart(2, '0');
|
|
||||||
return '<div class="apexcharts-tooltip-custom">' +
|
|
||||||
'<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
|
||||||
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
|
||||||
'</div>';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
|
|
||||||
serverCpuChart.render();
|
|
||||||
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
|
||||||
checkTheme();
|
|
||||||
serverCpuChart.updateOptions({
|
|
||||||
series: [{
|
|
||||||
data: chartData[0].seriesData,
|
|
||||||
}],
|
|
||||||
colors: [cpuColor],
|
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
|
||||||
show: true,
|
|
||||||
style: {
|
|
||||||
colors: textColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
yaxis: {
|
series: [{
|
||||||
show: true,
|
name: "CPU %",
|
||||||
labels: {
|
data: []
|
||||||
show: true,
|
}],
|
||||||
style: {
|
|
||||||
colors: textColor,
|
|
||||||
},
|
|
||||||
formatter: function(value) {
|
|
||||||
return Math.round(value) + ' %';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
noData: {
|
noData: {
|
||||||
text: 'Loading...',
|
text: 'Loading...',
|
||||||
style: {
|
style: {
|
||||||
color: textColor,
|
color: textColor,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
marker: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
||||||
|
const value = series[seriesIndex][dataPointIndex];
|
||||||
|
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
||||||
|
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
||||||
|
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
||||||
|
date.getUTCFullYear() + '-' +
|
||||||
|
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(date.getUTCDate()).padStart(2, '0');
|
||||||
|
return '<div class="apexcharts-tooltip-custom">' +
|
||||||
|
'<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
||||||
|
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
|
||||||
|
serverCpuChart.render();
|
||||||
|
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
||||||
|
checkTheme();
|
||||||
|
serverCpuChart.updateOptions({
|
||||||
|
series: [{
|
||||||
|
data: chartData[0].seriesData,
|
||||||
|
}],
|
||||||
|
colors: [cpuColor],
|
||||||
|
xaxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
style: {
|
||||||
|
colors: textColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
show: true,
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
style: {
|
||||||
|
colors: textColor,
|
||||||
|
},
|
||||||
|
formatter: function(value) {
|
||||||
|
return Math.round(value) + ' %';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
noData: {
|
||||||
|
text: 'Loading...',
|
||||||
|
style: {
|
||||||
|
color: textColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h4>Memory Usage</h4>
|
<h4>Memory Usage</h4>
|
||||||
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
checkTheme();
|
(function() {
|
||||||
const optionsServerMemory = {
|
checkTheme();
|
||||||
stroke: {
|
const optionsServerMemory = {
|
||||||
curve: 'straight',
|
stroke: {
|
||||||
width: 2,
|
curve: 'straight',
|
||||||
},
|
width: 2,
|
||||||
chart: {
|
},
|
||||||
height: '150px',
|
chart: {
|
||||||
id: '{!! $chartId !!}-memory',
|
height: '150px',
|
||||||
type: 'area',
|
id: '{!! $chartId !!}-memory',
|
||||||
toolbar: {
|
type: 'area',
|
||||||
show: true,
|
toolbar: {
|
||||||
tools: {
|
show: true,
|
||||||
download: false,
|
tools: {
|
||||||
selection: false,
|
download: false,
|
||||||
zoom: true,
|
selection: false,
|
||||||
zoomin: false,
|
zoom: true,
|
||||||
zoomout: false,
|
zoomin: false,
|
||||||
pan: false,
|
zoomout: false,
|
||||||
reset: true
|
pan: false,
|
||||||
|
reset: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animations: {
|
fill: {
|
||||||
enabled: true,
|
type: 'gradient',
|
||||||
},
|
},
|
||||||
},
|
dataLabels: {
|
||||||
fill: {
|
|
||||||
type: 'gradient',
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false,
|
|
||||||
offsetY: -10,
|
|
||||||
style: {
|
|
||||||
colors: ['#FCD452'],
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
}
|
offsetY: -10,
|
||||||
},
|
style: {
|
||||||
grid: {
|
colors: ['#FCD452'],
|
||||||
show: true,
|
},
|
||||||
borderColor: '',
|
background: {
|
||||||
},
|
enabled: false,
|
||||||
colors: [ramColor],
|
}
|
||||||
xaxis: {
|
},
|
||||||
type: 'datetime',
|
grid: {
|
||||||
labels: {
|
|
||||||
show: true,
|
show: true,
|
||||||
style: {
|
borderColor: '',
|
||||||
colors: textColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: "Memory (MB)",
|
|
||||||
data: []
|
|
||||||
}],
|
|
||||||
noData: {
|
|
||||||
text: 'Loading...',
|
|
||||||
style: {
|
|
||||||
color: textColor,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true,
|
|
||||||
marker: {
|
|
||||||
show: false,
|
|
||||||
},
|
},
|
||||||
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
colors: [ramColor],
|
||||||
const value = series[seriesIndex][dataPointIndex];
|
|
||||||
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
|
||||||
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
|
||||||
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
|
||||||
date.getUTCFullYear() + '-' +
|
|
||||||
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(date.getUTCDate()).padStart(2, '0');
|
|
||||||
return '<div class="apexcharts-tooltip-custom">' +
|
|
||||||
'<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + ' MB</span></div>' +
|
|
||||||
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
|
||||||
'</div>';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
|
|
||||||
optionsServerMemory);
|
|
||||||
serverMemoryChart.render();
|
|
||||||
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
|
||||||
checkTheme();
|
|
||||||
serverMemoryChart.updateOptions({
|
|
||||||
series: [{
|
|
||||||
data: chartData[0].seriesData,
|
|
||||||
}],
|
|
||||||
colors: [ramColor],
|
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
|
|
@ -262,27 +211,82 @@ class="pt-5">
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
yaxis: {
|
series: [{
|
||||||
min: 0,
|
name: "Memory (MB)",
|
||||||
show: true,
|
data: []
|
||||||
labels: {
|
}],
|
||||||
show: true,
|
|
||||||
style: {
|
|
||||||
colors: textColor,
|
|
||||||
},
|
|
||||||
formatter: function(value) {
|
|
||||||
return Math.round(value) + ' MB';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
noData: {
|
noData: {
|
||||||
text: 'Loading...',
|
text: 'Loading...',
|
||||||
style: {
|
style: {
|
||||||
color: textColor,
|
color: textColor,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
marker: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
||||||
|
const value = series[seriesIndex][dataPointIndex];
|
||||||
|
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
||||||
|
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
||||||
|
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
||||||
|
date.getUTCFullYear() + '-' +
|
||||||
|
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(date.getUTCDate()).padStart(2, '0');
|
||||||
|
return '<div class="apexcharts-tooltip-custom">' +
|
||||||
|
'<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + ' MB</span></div>' +
|
||||||
|
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
|
||||||
|
optionsServerMemory);
|
||||||
|
serverMemoryChart.render();
|
||||||
|
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
||||||
|
checkTheme();
|
||||||
|
serverMemoryChart.updateOptions({
|
||||||
|
series: [{
|
||||||
|
data: chartData[0].seriesData,
|
||||||
|
}],
|
||||||
|
colors: [ramColor],
|
||||||
|
xaxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
style: {
|
||||||
|
colors: textColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
min: 0,
|
||||||
|
show: true,
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
style: {
|
||||||
|
colors: textColor,
|
||||||
|
},
|
||||||
|
formatter: function(value) {
|
||||||
|
return Math.round(value) + ' MB';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
noData: {
|
||||||
|
text: 'Loading...',
|
||||||
|
style: {
|
||||||
|
color: textColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -23,145 +23,16 @@
|
||||||
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
checkTheme();
|
(function() {
|
||||||
const optionsServerCpu = {
|
|
||||||
stroke: {
|
|
||||||
curve: 'straight',
|
|
||||||
width: 2,
|
|
||||||
},
|
|
||||||
chart: {
|
|
||||||
height: '150px',
|
|
||||||
id: '{!! $chartId !!}-cpu',
|
|
||||||
type: 'area',
|
|
||||||
toolbar: {
|
|
||||||
show: true,
|
|
||||||
tools: {
|
|
||||||
download: false,
|
|
||||||
selection: false,
|
|
||||||
zoom: true,
|
|
||||||
zoomin: false,
|
|
||||||
zoomout: false,
|
|
||||||
pan: false,
|
|
||||||
reset: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animations: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
type: 'gradient',
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false,
|
|
||||||
offsetY: -10,
|
|
||||||
style: {
|
|
||||||
colors: ['#FCD452'],
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
enabled: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
show: true,
|
|
||||||
borderColor: '',
|
|
||||||
},
|
|
||||||
colors: [cpuColor],
|
|
||||||
xaxis: {
|
|
||||||
type: 'datetime',
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: 'CPU %',
|
|
||||||
data: []
|
|
||||||
}],
|
|
||||||
noData: {
|
|
||||||
text: 'Loading...',
|
|
||||||
style: {
|
|
||||||
color: textColor,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true,
|
|
||||||
marker: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
|
||||||
const value = series[seriesIndex][dataPointIndex];
|
|
||||||
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
|
||||||
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
|
||||||
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
|
||||||
date.getUTCFullYear() + '-' +
|
|
||||||
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(date.getUTCDate()).padStart(2, '0');
|
|
||||||
return '<div class="apexcharts-tooltip-custom">' +
|
|
||||||
'<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
|
||||||
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
|
||||||
'</div>';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`),
|
|
||||||
optionsServerCpu);
|
|
||||||
serverCpuChart.render();
|
|
||||||
document.addEventListener('livewire:init', () => {
|
|
||||||
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
|
||||||
checkTheme();
|
|
||||||
serverCpuChart.updateOptions({
|
|
||||||
series: [{
|
|
||||||
data: chartData[0].seriesData,
|
|
||||||
}],
|
|
||||||
colors: [cpuColor],
|
|
||||||
xaxis: {
|
|
||||||
type: 'datetime',
|
|
||||||
labels: {
|
|
||||||
show: true,
|
|
||||||
style: {
|
|
||||||
colors: textColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
show: true,
|
|
||||||
labels: {
|
|
||||||
show: true,
|
|
||||||
style: {
|
|
||||||
colors: textColor,
|
|
||||||
},
|
|
||||||
formatter: function(value) {
|
|
||||||
return Math.round(value) + ' %';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
noData: {
|
|
||||||
text: 'Loading...',
|
|
||||||
style: {
|
|
||||||
color: textColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4>Memory Usage</h4>
|
|
||||||
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
checkTheme();
|
checkTheme();
|
||||||
const optionsServerMemory = {
|
const optionsServerCpu = {
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: 'straight',
|
curve: 'straight',
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
chart: {
|
chart: {
|
||||||
height: '150px',
|
height: '150px',
|
||||||
id: '{!! $chartId !!}-memory',
|
id: '{!! $chartId !!}-cpu',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: true,
|
show: true,
|
||||||
|
|
@ -196,18 +67,12 @@
|
||||||
show: true,
|
show: true,
|
||||||
borderColor: '',
|
borderColor: '',
|
||||||
},
|
},
|
||||||
colors: [ramColor],
|
colors: [cpuColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
},
|
||||||
show: true,
|
series: [{
|
||||||
style: {
|
name: 'CPU %',
|
||||||
colors: textColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: "Memory (%)",
|
|
||||||
data: []
|
data: []
|
||||||
}],
|
}],
|
||||||
noData: {
|
noData: {
|
||||||
|
|
@ -232,7 +97,7 @@
|
||||||
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||||
String(date.getUTCDate()).padStart(2, '0');
|
String(date.getUTCDate()).padStart(2, '0');
|
||||||
return '<div class="apexcharts-tooltip-custom">' +
|
return '<div class="apexcharts-tooltip-custom">' +
|
||||||
'<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
'<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
||||||
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
@ -241,17 +106,16 @@
|
||||||
show: false
|
show: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
|
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`),
|
||||||
optionsServerMemory);
|
optionsServerCpu);
|
||||||
serverMemoryChart.render();
|
serverCpuChart.render();
|
||||||
document.addEventListener('livewire:init', () => {
|
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
||||||
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
checkTheme();
|
||||||
checkTheme();
|
serverCpuChart.updateOptions({
|
||||||
serverMemoryChart.updateOptions({
|
|
||||||
series: [{
|
series: [{
|
||||||
data: chartData[0].seriesData,
|
data: chartData[0].seriesData,
|
||||||
}],
|
}],
|
||||||
colors: [ramColor],
|
colors: [cpuColor],
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
|
|
@ -262,16 +126,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
min: 0,
|
|
||||||
show: true,
|
show: true,
|
||||||
labels: {
|
labels: {
|
||||||
show: true,
|
show: true,
|
||||||
style: {
|
style: {
|
||||||
colors: textColor,
|
colors: textColor,
|
||||||
},
|
},
|
||||||
formatter: function(value) {
|
formatter: function(value) {
|
||||||
return Math.round(value) + ' %';
|
return Math.round(value) + ' %';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
noData: {
|
noData: {
|
||||||
|
|
@ -282,7 +145,144 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4>Memory Usage</h4>
|
||||||
|
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
checkTheme();
|
||||||
|
const optionsServerMemory = {
|
||||||
|
stroke: {
|
||||||
|
curve: 'straight',
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
height: '150px',
|
||||||
|
id: '{!! $chartId !!}-memory',
|
||||||
|
type: 'area',
|
||||||
|
toolbar: {
|
||||||
|
show: true,
|
||||||
|
tools: {
|
||||||
|
download: false,
|
||||||
|
selection: false,
|
||||||
|
zoom: true,
|
||||||
|
zoomin: false,
|
||||||
|
zoomout: false,
|
||||||
|
pan: false,
|
||||||
|
reset: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
offsetY: -10,
|
||||||
|
style: {
|
||||||
|
colors: ['#FCD452'],
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
show: true,
|
||||||
|
borderColor: '',
|
||||||
|
},
|
||||||
|
colors: [ramColor],
|
||||||
|
xaxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
style: {
|
||||||
|
colors: textColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: "Memory (%)",
|
||||||
|
data: []
|
||||||
|
}],
|
||||||
|
noData: {
|
||||||
|
text: 'Loading...',
|
||||||
|
style: {
|
||||||
|
color: textColor,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
marker: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
||||||
|
const value = series[seriesIndex][dataPointIndex];
|
||||||
|
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
||||||
|
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
||||||
|
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
||||||
|
date.getUTCFullYear() + '-' +
|
||||||
|
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(date.getUTCDate()).padStart(2, '0');
|
||||||
|
return '<div class="apexcharts-tooltip-custom">' +
|
||||||
|
'<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
||||||
|
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
|
||||||
|
optionsServerMemory);
|
||||||
|
serverMemoryChart.render();
|
||||||
|
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
||||||
|
checkTheme();
|
||||||
|
serverMemoryChart.updateOptions({
|
||||||
|
series: [{
|
||||||
|
data: chartData[0].seriesData,
|
||||||
|
}],
|
||||||
|
colors: [ramColor],
|
||||||
|
xaxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
style: {
|
||||||
|
colors: textColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
min: 0,
|
||||||
|
show: true,
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
style: {
|
||||||
|
colors: textColor,
|
||||||
|
},
|
||||||
|
formatter: function(value) {
|
||||||
|
return Math.round(value) + ' %';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
noData: {
|
||||||
|
text: 'Loading...',
|
||||||
|
style: {
|
||||||
|
color: textColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
use App\Models\StandaloneClickhouse;
|
use App\Models\StandaloneClickhouse;
|
||||||
|
|
||||||
test('clickhouse uses clickhouse_db field in internal connection string', function () {
|
test('clickhouse uses clickhouse_db field in internal connection string', function () {
|
||||||
$clickhouse = new StandaloneClickhouse();
|
$clickhouse = new StandaloneClickhouse;
|
||||||
$clickhouse->clickhouse_admin_user = 'testuser';
|
$clickhouse->clickhouse_admin_user = 'testuser';
|
||||||
$clickhouse->clickhouse_admin_password = 'testpass';
|
$clickhouse->clickhouse_admin_password = 'testpass';
|
||||||
$clickhouse->clickhouse_db = 'mydb';
|
$clickhouse->clickhouse_db = 'mydb';
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clickhouse defaults to default database when clickhouse_db is null', function () {
|
test('clickhouse defaults to default database when clickhouse_db is null', function () {
|
||||||
$clickhouse = new StandaloneClickhouse();
|
$clickhouse = new StandaloneClickhouse;
|
||||||
$clickhouse->clickhouse_admin_user = 'testuser';
|
$clickhouse->clickhouse_admin_user = 'testuser';
|
||||||
$clickhouse->clickhouse_admin_password = 'testpass';
|
$clickhouse->clickhouse_admin_password = 'testpass';
|
||||||
$clickhouse->clickhouse_db = null;
|
$clickhouse->clickhouse_db = null;
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clickhouse external url uses correct database', function () {
|
test('clickhouse external url uses correct database', function () {
|
||||||
$clickhouse = new StandaloneClickhouse();
|
$clickhouse = new StandaloneClickhouse;
|
||||||
$clickhouse->clickhouse_admin_user = 'admin';
|
$clickhouse->clickhouse_admin_user = 'admin';
|
||||||
$clickhouse->clickhouse_admin_password = 'secret';
|
$clickhouse->clickhouse_admin_password = 'secret';
|
||||||
$clickhouse->clickhouse_db = 'production';
|
$clickhouse->clickhouse_db = 'production';
|
||||||
|
|
@ -38,12 +38,19 @@
|
||||||
$clickhouse->is_public = true;
|
$clickhouse->is_public = true;
|
||||||
$clickhouse->public_port = 8123;
|
$clickhouse->public_port = 8123;
|
||||||
|
|
||||||
$clickhouse->destination = new class {
|
$clickhouse->destination = new class
|
||||||
|
{
|
||||||
public $server;
|
public $server;
|
||||||
public function __construct() {
|
|
||||||
$this->server = new class {
|
public function __construct()
|
||||||
public function __get($name) {
|
{
|
||||||
if ($name === 'getIp') return '1.2.3.4';
|
$this->server = new class
|
||||||
|
{
|
||||||
|
public function __get($name)
|
||||||
|
{
|
||||||
|
if ($name === 'getIp') {
|
||||||
|
return '1.2.3.4';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\Server;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to call the private downsampleLTTB method on Server model via reflection.
|
* Tests for the downsampleLTTB helper function used for metrics downsampling.
|
||||||
|
* This function implements the Largest-Triangle-Three-Buckets algorithm.
|
||||||
*/
|
*/
|
||||||
function callDownsampleLTTB(array $data, int $threshold): array
|
|
||||||
{
|
|
||||||
$server = new Server;
|
|
||||||
$reflection = new ReflectionClass($server);
|
|
||||||
$method = $reflection->getMethod('downsampleLTTB');
|
|
||||||
|
|
||||||
return $method->invoke($server, $data, $threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
it('returns data unchanged when below threshold', function () {
|
it('returns data unchanged when below threshold', function () {
|
||||||
$data = [
|
$data = [
|
||||||
[1000, 10.5],
|
[1000, 10.5],
|
||||||
|
|
@ -21,7 +11,7 @@ function callDownsampleLTTB(array $data, int $threshold): array
|
||||||
[3000, 15.7],
|
[3000, 15.7],
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = callDownsampleLTTB($data, 1000);
|
$result = downsampleLTTB($data, 1000);
|
||||||
|
|
||||||
expect($result)->toBe($data);
|
expect($result)->toBe($data);
|
||||||
});
|
});
|
||||||
|
|
@ -35,10 +25,10 @@ function callDownsampleLTTB(array $data, int $threshold): array
|
||||||
[5000, 12.0],
|
[5000, 12.0],
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = callDownsampleLTTB($data, 2);
|
$result = downsampleLTTB($data, 2);
|
||||||
expect($result)->toBe($data);
|
expect($result)->toBe($data);
|
||||||
|
|
||||||
$result = callDownsampleLTTB($data, 1);
|
$result = downsampleLTTB($data, 1);
|
||||||
expect($result)->toBe($data);
|
expect($result)->toBe($data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -52,7 +42,7 @@ function callDownsampleLTTB(array $data, int $threshold): array
|
||||||
$data[] = [$i * 1000, mt_rand(0, 100) / 10];
|
$data[] = [$i * 1000, mt_rand(0, 100) / 10];
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = callDownsampleLTTB($data, 10);
|
$result = downsampleLTTB($data, 10);
|
||||||
|
|
||||||
expect(count($result))->toBe(10);
|
expect(count($result))->toBe(10);
|
||||||
});
|
});
|
||||||
|
|
@ -63,7 +53,7 @@ function callDownsampleLTTB(array $data, int $threshold): array
|
||||||
$data[] = [$i * 1000, $i * 1.5];
|
$data[] = [$i * 1000, $i * 1.5];
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = callDownsampleLTTB($data, 20);
|
$result = downsampleLTTB($data, 20);
|
||||||
|
|
||||||
// First point should be preserved
|
// First point should be preserved
|
||||||
expect($result[0])->toBe($data[0]);
|
expect($result[0])->toBe($data[0]);
|
||||||
|
|
@ -78,7 +68,7 @@ function callDownsampleLTTB(array $data, int $threshold): array
|
||||||
$data[] = [$i * 60000, sin($i / 10) * 50 + 50]; // Sine wave pattern
|
$data[] = [$i * 60000, sin($i / 10) * 50 + 50]; // Sine wave pattern
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = callDownsampleLTTB($data, 50);
|
$result = downsampleLTTB($data, 50);
|
||||||
|
|
||||||
// Verify all timestamps are in non-decreasing order
|
// Verify all timestamps are in non-decreasing order
|
||||||
$previousTimestamp = -1;
|
$previousTimestamp = -1;
|
||||||
|
|
@ -100,7 +90,7 @@ function callDownsampleLTTB(array $data, int $threshold): array
|
||||||
}
|
}
|
||||||
|
|
||||||
$startTime = microtime(true);
|
$startTime = microtime(true);
|
||||||
$result = callDownsampleLTTB($data, 1000);
|
$result = downsampleLTTB($data, 1000);
|
||||||
$executionTime = microtime(true) - $startTime;
|
$executionTime = microtime(true) - $startTime;
|
||||||
|
|
||||||
expect(count($result))->toBe(1000);
|
expect(count($result))->toBe(1000);
|
||||||
|
|
@ -121,7 +111,7 @@ function callDownsampleLTTB(array $data, int $threshold): array
|
||||||
$data[] = [$i * 1000, $value];
|
$data[] = [$i * 1000, $value];
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = callDownsampleLTTB($data, 20);
|
$result = downsampleLTTB($data, 20);
|
||||||
|
|
||||||
// The peak (100) and valley (0) should be preserved due to LTTB algorithm
|
// The peak (100) and valley (0) should be preserved due to LTTB algorithm
|
||||||
$values = array_column($result, 1);
|
$values = array_column($result, 1);
|
||||||
Loading…
Reference in a new issue