108 lines
3.6 KiB
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();
|
|
}
|
|
}
|