mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(metrics): even out chart density on long ranges
Sentinel keeps raw samples for the last 24-48 hours and one-minute averages before that. LTTB buckets by point count, so 7-day and 30-day charts gave most points to the raw part. Average those ranges per minute before LTTB. Closes #12281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6699b436e8
commit
f72041c513
@@ -172,6 +172,11 @@ trait HasMetrics
|
||||
/**
|
||||
* Map raw rows to [timestampMs, value] pairs, downsampling long ranges.
|
||||
*
|
||||
* Sentinel keeps raw samples for the last 24 to 48 hours and one-minute averages
|
||||
* before that. LTTB buckets by point count, so on ranges that reach past the raw
|
||||
* window the raw part got most of the chart points. Those ranges are averaged per
|
||||
* minute first, the same as Sentinel does, so the density is even over time.
|
||||
*
|
||||
* @param array<int, array<string, mixed>>|null $rows
|
||||
*/
|
||||
private function mapRows(?array $rows, string $valueField, int $mins): ?array
|
||||
@@ -184,6 +189,10 @@ trait HasMetrics
|
||||
return [(int) $metric['time'], (float) ($metric[$valueField] ?? 0.0)];
|
||||
})->toArray();
|
||||
|
||||
if ($mins > 24 * 60) {
|
||||
$metrics = $this->averagePerMinute($metrics);
|
||||
}
|
||||
|
||||
if ($mins > 60 && count($metrics) > 1000) {
|
||||
$metrics = downsampleLTTB($metrics, 1000);
|
||||
}
|
||||
@@ -191,6 +200,28 @@ trait HasMetrics
|
||||
return $metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Average [timestampMs, value] pairs into one-minute buckets, timestamped at the
|
||||
* bucket start like Sentinel's own downsampling. Samples that are a minute or more
|
||||
* apart keep their own value.
|
||||
*
|
||||
* @param array<int, array{0: int, 1: float}> $metrics
|
||||
* @return array<int, array{0: int, 1: float}>
|
||||
*/
|
||||
private function averagePerMinute(array $metrics): array
|
||||
{
|
||||
$sums = [];
|
||||
$counts = [];
|
||||
foreach ($metrics as [$time, $value]) {
|
||||
$bucket = intdiv($time, 60_000) * 60_000;
|
||||
$sums[$bucket] = ($sums[$bucket] ?? 0.0) + $value;
|
||||
$counts[$bucket] = ($counts[$bucket] ?? 0) + 1;
|
||||
}
|
||||
ksort($sums);
|
||||
|
||||
return array_map(fn (int $bucket) => [$bucket, $sums[$bucket] / $counts[$bucket]], array_keys($sums));
|
||||
}
|
||||
|
||||
private function metricsToken(Server $server): string
|
||||
{
|
||||
$previousToken = null;
|
||||
|
||||
@@ -85,3 +85,25 @@ it('runs the batched metrics script with sudo for a non-root user', function ()
|
||||
->and($commands[0])->not->toContain('sudo curl')
|
||||
->and($commands[0])->not->toContain('sudo printf');
|
||||
});
|
||||
|
||||
it('keeps long-range charts evenly dense across mixed-resolution Sentinel history', function () {
|
||||
// Sentinel keeps raw samples (here every 10 s) for the last one to two days and
|
||||
// one-minute averages before that. A 7-day chart must not crowd the raw days.
|
||||
$end = 1_800_000_000_000;
|
||||
$rawStart = $end - 2 * 86_400_000;
|
||||
$rows = [];
|
||||
for ($time = $end - 7 * 86_400_000; $time < $rawStart; $time += 60_000) {
|
||||
$rows[] = ['time' => $time, 'percent' => 10];
|
||||
}
|
||||
for ($time = $rawStart; $time < $end; $time += 10_000) {
|
||||
$rows[] = ['time' => $time, 'percent' => ($time / 10_000) % 2 === 0 ? 0 : 20];
|
||||
}
|
||||
Process::fake(fn () => Process::result(json_encode($rows)));
|
||||
|
||||
$metrics = $this->server->getCpuMetrics(10080);
|
||||
$recent = array_filter($metrics, fn (array $point) => $point[0] >= $rawStart);
|
||||
|
||||
expect(count($metrics))->toBeLessThanOrEqual(1000)
|
||||
->and(count($recent) / count($metrics))->toBeLessThan(0.35)
|
||||
->and(array_values(array_unique(array_column($recent, 1))))->toBe([10.0]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user