Systemstatus im Sysadmin-Bereich und /health fuer externe Ueberwachung

- HealthProbe: Datenbank, Cache, Queue (sync / database / Horizon),
  fehlgeschlagene Jobs (24 h), Scheduler-Herzschlag, Plattenplatz
- /health auf jeder Domain, ohne web-Gruppe, nur mit MONITORING_HEALTH_TOKEN
- monitoring:heartbeat minuetlich im Scheduler
- Sysadmin -> Systemstatus (my.mivita.care/sysadmin/status) mit Version,
  Umgebung und zuletzt fehlgeschlagenen Jobs
- Anleitung Abschnitt G

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-10-07 11:49:14 +00:00
parent 22c85fc746
commit fec88ee265
17 changed files with 783 additions and 3 deletions

View file

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers;
use App\Services\Monitoring\HealthProbe;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Tiefer Health-Check für den externen Monitor.
*
* `/up` zeigt nur, dass die App bootet. `/health` prüft zusätzlich DB,
* Cache, Queue, fehlgeschlagene Jobs, Scheduler-Herzschlag und Plattenplatz: 200 solange
* nichts kritisch ist, sonst 503. Zugang nur mit MONITORING_HEALTH_TOKEN
* (Bearer oder `?token=`); ohne konfiguriertes Token gibt es den Endpunkt nicht.
*/
class HealthController extends Controller
{
public function __invoke(Request $request, HealthProbe $probe): JsonResponse
{
$token = (string) config('monitoring.health.token');
$given = (string) ($request->bearerToken() ?? $request->query('token', ''));
abort_if($token === '' || ! hash_equals($token, $given), 404);
$checks = $probe->checks();
$status = HealthProbe::worstStatus($checks);
return response()->json([
'status' => $status,
'version' => config('app.version'),
'checked_at' => now()->toIso8601String(),
'checks' => collect($checks)->mapWithKeys(fn (array $check): array => [
$check['key'] => ['status' => $check['status'], 'detail' => $check['detail']],
]),
], $status === HealthProbe::StatusCritical ? 503 : 200)
->header('Cache-Control', 'no-store');
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\SyS;
use App\Http\Controllers\Controller;
use App\Services\Monitoring\HealthProbe;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Throwable;
/**
* Systemstatus im Sysadmin-Bereich: dieselben Prüfungen wie `/health`,
* dazu Umgebung und die zuletzt fehlgeschlagenen Jobs.
*/
class SystemStatusController extends Controller
{
public function index(HealthProbe $probe): View
{
$checks = $probe->checks();
return view('sys.status.index', [
'checks' => $checks,
'overall' => HealthProbe::worstStatus($checks),
'environment' => [
'Version' => config('app.version') ?? 'unbekannt',
'Umgebung' => app()->environment(),
'PHP' => PHP_VERSION,
'Laravel' => app()->version(),
'Queue-Treiber' => config('queue.default'),
'Cache-Treiber' => config('cache.default'),
],
'healthTokenConfigured' => filled(config('monitoring.health.token')),
'horizonAvailable' => config('queue.default') === 'redis',
'failedJobs' => $this->recentFailedJobs(),
]);
}
/**
* @return Collection<int, array{uuid: ?string, job: string, queue: string, failed_at: string, error: string}>
*/
private function recentFailedJobs(): Collection
{
try {
$rows = DB::table('failed_jobs')->latest('failed_at')->limit(10)->get();
} catch (Throwable) {
return collect();
}
return $rows->map(fn (object $row): array => [
'uuid' => $row->uuid ?? null,
'job' => json_decode($row->payload, true)['displayName'] ?? 'unbekannt',
'queue' => $row->connection.' / '.$row->queue,
'failed_at' => (string) $row->failed_at,
'error' => Str::limit(Str::before((string) $row->exception, "\n"), 200),
]);
}
}