} */ public function health(): array { $checks = $this->probe->checks(); return ['status' => HealthProbe::worstStatus($checks), 'checks' => $checks]; } /** * @return array{version: string, environment: string, debug: bool, php: string, laravel: string, deployed_at: ?Carbon} */ public function application(): array { $release = base_path('RELEASE'); return [ 'version' => (string) (config('app.version') ?? '—'), 'environment' => app()->environment(), 'debug' => (bool) config('app.debug'), 'php' => PHP_VERSION, 'laravel' => app()->version(), 'deployed_at' => is_file($release) ? Carbon::createFromTimestamp((int) filemtime($release))->timezone(self::DisplayTimezone) : null, ]; } /** * Geplante Aufgaben mit nächstem Lauf (über `schedule:list`, das die Konsolen-Routen lädt). * * @return list */ public function scheduledTasks(): array { try { Artisan::call('schedule:list', ['--json' => true]); $tasks = json_decode(Artisan::output(), true); } catch (Throwable) { return []; } return collect(is_array($tasks) ? $tasks : []) ->map(fn (array $task): array => [ 'command' => trim((string) preg_replace('/^.*?artisan\s+/', '', (string) ($task['command'] ?? ''))), 'expression' => (string) ($task['expression'] ?? ''), 'next' => filled($task['next_due_date'] ?? null) ? Carbon::parse($task['next_due_date'])->timezone(self::DisplayTimezone) : null, 'description' => (string) ($task['description'] ?? ''), ]) ->values() ->all(); } /** * @return array{count: int, latest: list} */ public function failedJobs(int $limit = 5): array { try { $table = DB::table((string) config('queue.failed.table', 'failed_jobs')); return [ 'count' => $table->count(), 'latest' => $table->orderByDesc('failed_at')->limit($limit)->get() ->map(fn (object $job): array => [ 'job' => (string) (json_decode((string) $job->payload, true)['displayName'] ?? 'Job'), 'queue' => (string) $job->queue, 'failed_at' => Carbon::parse($job->failed_at)->timezone(self::DisplayTimezone), 'error' => ErrorReportScrubber::scrubText(Str::limit(strtok((string) $job->exception, "\n") ?: '', 180)), ]) ->all(), ]; } catch (Throwable) { return ['count' => 0, 'latest' => []]; } } /** * Letzte Einträge ab einer Stufe aus dem jüngsten Log einer Art (`laravel`, `monitoring`). * * @param list $levels * @return list */ public function recentLogEntries(string $prefix, array $levels, int $limit = 10): array { $files = glob(storage_path("logs/{$prefix}*.log")) ?: []; usort($files, fn (string $a, string $b): int => filemtime($b) <=> filemtime($a)); $file = $files[0] ?? null; if ($file === null) { return []; } $handle = @fopen($file, 'rb'); if ($handle === false) { return []; } $size = (int) filesize($file); fseek($handle, max(0, $size - self::LogTailBytes)); $tail = (string) stream_get_contents($handle); fclose($handle); $entries = []; foreach (preg_split('/\R/', $tail) ?: [] as $line) { if (preg_match(self::LogLinePattern, $line, $match) !== 1 || ! in_array(strtoupper($match[2]), $levels, true)) { continue; } $message = (string) preg_replace('/\s*\{"(exception|userId)".*$/s', '', $match[3]); $entries[] = [ 'at' => Carbon::parse($match[1])->timezone(self::DisplayTimezone), 'level' => strtolower($match[2]), 'message' => ErrorReportScrubber::scrubText(Str::limit($message, 220)), ]; } return array_slice(array_reverse($entries), 0, $limit); } /** * Links zu den Diensten außerhalb der App, soweit konfiguriert. * * @return array */ public function links(): array { $links = []; $dsn = (string) config('sentry.dsn'); if (preg_match('#^https?://[^@]+@([^/]+)/(\d+)#', $dsn, $match) === 1) { $links['GlitchTip'] = "https://{$match[1]}/"; } if (filled(config('monitoring.status_url'))) { $links['Uptime Kuma'] = (string) config('monitoring.status_url'); } return $links; } }