CluPilotCloud/app/Models/InstanceMetric.php

108 lines
3.6 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
/**
* A day in the life of one instance: how full it was, and what it moved.
*
* Read by the customer panel to draw the storage ring and the transfer trend.
* Written by CollectInstanceTraffic, which already visits every active
* instance — a second scheduler entry hitting the same Proxmox API would only
* double the load and the failure modes.
*/
class InstanceMetric extends Model
{
protected $fillable = ['instance_id', 'day', 'disk_used_bytes', 'disk_total_bytes', 'rx_bytes', 'tx_bytes', 'checks_total', 'checks_ok'];
protected function casts(): array
{
return [
'day' => 'date',
'disk_used_bytes' => 'integer',
'disk_total_bytes' => 'integer',
'rx_bytes' => 'integer',
'tx_bytes' => 'integer',
'checks_total' => 'integer',
'checks_ok' => 'integer',
];
}
public function instance(): BelongsTo
{
return $this->belongsTo(Instance::class);
}
/**
* The last $days days, oldest first, with gaps left as gaps.
*
* A missing day is not a zero. Filling it would draw a cliff into the chart
* on every day the sampler could not reach the host, and the customer would
* read an outage that never happened.
*
* @return Collection<int, self>
*/
public static function series(Instance $instance, int $days = 14): Collection
{
return self::query()
->where('instance_id', $instance->id)
->where('day', '>=', Carbon::today()->subDays($days - 1))
->orderBy('day')
->get();
}
/**
* Availability over the window, as a percentage, or null.
*
* Null when nothing was ever checked. Reporting an unmonitored instance as
* 100 % is the single most dishonest number this application could print —
* it is the figure a customer would quote to their own auditor.
*/
public static function availability(Instance $instance, int $days = 30): ?float
{
$row = self::query()
->where('instance_id', $instance->id)
->where('day', '>=', Carbon::today()->subDays($days - 1))
->selectRaw('SUM(checks_total) as total, SUM(checks_ok) as ok')
->first();
$total = (int) ($row->total ?? 0);
return $total === 0 ? null : round((int) $row->ok / $total * 100, 2);
}
/**
* Daily availability for the trend, oldest first.
*
* Days without a single check are skipped rather than drawn at zero: our
* monitoring being down is not the customer's cloud being down.
*
* @return array<int, float>
*/
public static function availabilitySeries(Instance $instance, int $days = 30): array
{
return self::query()
->where('instance_id', $instance->id)
->where('day', '>=', Carbon::today()->subDays($days - 1))
->where('checks_total', '>', 0)
->orderBy('day')
->get()
->map(fn (self $m) => round($m->checks_ok / max(1, $m->checks_total) * 100, 2))
->all();
}
/** The most recent day that carries a disk reading, or null. */
public static function latestDisk(Instance $instance): ?self
{
return self::query()
->where('instance_id', $instance->id)
->whereNotNull('disk_used_bytes')
->orderByDesc('day')
->first();
}
}