13. Laravel Health. Налаштування та використання в docker healthcheck.

Відео версія
(YouTube - для зворотнього зв'язку)

TL; DR

Пакет Laravel Health не є заміною повноцінної моніторингової системи, але може її доповнити. Використовуйте зовнішні моніторингові системи для нотифікацій та запуску команди перевірки.

Для мого проєкту я створив окремий модуль для health check та виніс методи до Action. Якщо ваш проєкт не використовує модулі або Actions - раджу це зробити.

Обов'язково закривайте web-view на продакшені та stage. Ідеально надати доступ лише з корпоративної IP адреси!

Робочий процес

Важливий момент - в документації радять повісити перевірку на schedule в проєкті. Це не є найкращим підходом, оскільки якщо ваш додаток "помре", ніхто не запустить schedule перевірку і жодної нотифікації не буде.

Також, відсилати нотифікації про зламаний додаток зі поламаного додатка не ок. Тому логіка роботи така:

  1. Встановлюємо або купуємо будь-який зовнішній сервіс моніторингу. Наприклад, uptime kuma. Ідеально - на іншому сервері, ніж APP, що моніторимо.
  2. Сервіс пінгує /health-check шлях та надсилає нотифікації до потрібного нам каналу.
  3. /health-check поверне статус 503, коли хоча б одна з перевірок не пройшла. Тут треба бути обережним з налаштуванням - статуси skipped також вважаються помилкою. Якщо це не влаштовує - пишемо свою реалізацію контролера SimpleHealthCheckController.
  4. При нотифікації розробники йдуть до /health, якщо той доступний, якщо ні - на сервер і так далі.

Авторизація

Не забувайте про авторизацію. Корпоративний ІР - все ще кращий варіант.

Реалізація конфігурації для авторизації в Service Provider:

config/health.php
[
  'path' => env('HEALTH_PATH', 'health'),
  'check_path' => env('HEALTH_CHECK_PATH', 'health-check'),

  'middleware' => [
      'web',
      VeryBasicAuth::class
  ]
]
app/Providers/HealthCheckServiceProvider.php
class HealthCheckServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->registerRoutes();
    }
    
    protected function registerRoutes(): void
    {
        if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) {
            return;
        }
    
        Route::get(config('health.path'), HealthCheckResultsController::class)
            ->middleware(config('health.middleware', 'web'));

        Route::get(config('health.check_path'), CheckSystemHealth::class)
            ->middleware(config('health.middleware', 'web'));

        Route::get('/up', fn() => '');
    }
}

Тут:

  • HEALTH_PATH - шлях до Laravel health web інтерфейсу. Обов'язково закриваємо від користувачів.
  • HEALTH_CHECK_PATH - шлях до простої перевірки - "Все ок". Також рекомендую закривати.
  • /up - проста перевірка, чи піднятий APP. Тут раджу не закривати.

Мікро ? Оптимізація

Збереження історії health перевірок має сенс саме в продакшені. Розуміння, що не ок було вночі, коли всі спали, важливе.

Тому я реалізував свій варіант SimpleHealthCheckController.php:

Modules/HealthCheck/Actions/CheckSystemHealth.php
<?php

namespace Modules\HealthCheck\Actions;

use Illuminate\Http\Response;
use Lorisleiva\Actions\Concerns\AsController;
use Spatie\Health\Enums\Status as HealthCheckStatus;
use Spatie\Health\Models\HealthCheckResultHistoryItem;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Throwable;

class CheckSystemHealth
{
    use AsController;

    /**
     * @throws Throwable
     */
    public function handle(): Response
    {
        $unhealthy = HealthCheckResultHistoryItem::query()
            ->whereNotIn(
                'health_check_result_history_items.status',
                [
                    HealthCheckStatus::ok(),
                    HealthCheckStatus::skipped(),
                ]
            )
            ->where(
                'health_check_result_history_items.batch',
                fn ($query) => $query
                    ->select('health_check_result_history_items.batch')
                    ->from('health_check_result_history_items')
                    ->latest()
                    ->limit(1)
            )
            ->exists();

        throw_if($unhealthy, new ServiceUnavailableHttpException('Application not healthy'));

        return response()->noContent();
    }
}

Laravel Health та Docker

Для docker healthcheck перевірок також пакет не пропонує готових рішень. Тому реалізував власну команду:

Modules/HealthCheck/Actions/CheckSystemHealth.php
<?php

namespace Modules\HealthCheck\Actions;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsCommand;
use Spatie\Health\Commands\RunHealthChecksCommand;
use Spatie\Health\Enums\Status as HealthCheckStatus;
use Spatie\Health\Models\HealthCheckResultHistoryItem;

class CheckPropertyHealth
{
    use AsCommand;

    public $commandSignature = 'health:check-property {name} {--fresh}';
    public $commandDescription = 'Check single health property by name';

    public function handle(
        string $name
    ): ?string {
        return HealthCheckResultHistoryItem::query()
            ->whereRaw("LOWER(health_check_result_history_items.check_name) = '{$name}'")
            ->orderByDesc('health_check_result_history_items.created_at')
            ->value('health_check_result_history_items.status');
    }

    public function asCommand(Command $command)
    {
        $name = Str::lower($command->argument('name'));

        if ($command->option('fresh')) {
            Artisan::call(RunHealthChecksCommand::class);
        }

        $status = $this->handle($name);

        if (!$status) {
            $command->warn("No health check results found for {$name}");
            return $command::INVALID;
        }

        if ($status !== HealthCheckStatus::ok()->value) {
            $command->error($status);
            return $command::FAILURE;
        }

        $command->info($status);
        return $command::SUCCESS;
    }
}

Та інтегрував її в docker healthcheck:

docker-compose.yml
app:
 # ...
 healthcheck:
   test: php artisan health:check-property octane --fresh
   interval: 60s
   timeout: 5s
   retries: 3

horizon:
 # ...
 depends_on:
  - app
 healthcheck:
   test: php artisan health:check-property horizon
   start_period: 5s
   interval: 60s
   timeout: 5s
   retries: 3

schedule:
 # ...
 depends_on:
   - app
 healthcheck:
   test: php artisan health:check-property schedule
   start_period: 5s
   interval: 60s
   timeout: 10s
   retries: 3

Тут важливо --fresh - запускає health check команду. Викликати її як schedule - не варіант. Бо це перевірка самого себе.

Прийняті перевірки


Набір перевірок

Health::checks
OctaneCheck::new()
  • Використовую в healthcheck app сервісу. Будьте уважні, якщо запускаєте з schedule сервісу - там октан не запущений.
Health::checks
HorizonCheck::new(),
  • Перевіряє, чи Active статус хорайзон. ТОП для healthcheck Horizon сервісу.
Health::checks
ScheduleCheck::new()->useCacheStore(config('cache.default')),
  • Тут важливо розуміти принцип роботи. Обов'язково треба додати команду перевірки. Вона кожну хвилину записує мітку часу до кешу. Якщо час в кеші відстає більше ніж на 2 хвилини - команда не виконалась і schedule сервіс не працює. Звісно для healthcheck schedule контейнеру.
registerSchedule some where
$schedule->command(ScheduleCheckHeartbeatCommand::class)->everyMinute();
Health::checks
DatabaseConnectionCountCheck::new()
    ->failWhenMoreConnectionsThan(20),
  • Тут треба розуміння. Для Laravel Octane - одне з'єднання на один worker та декілька системних процесів: autovacuum, background writer і так далі. Це фішка Octane - одне постійне з'єднання. Якщо ви не використовуєте Octane - кожен запит = +1 з'єднання (а може й більше). Тому у випадку з Octane 20 з'єднань звучить як ок. Для інших - залежить від навантаження на систему.
Health::checks
DatabaseSizeCheck::new()
             ->failWhenSizeAboveGb(errorThresholdGb: 25.0),
  • Ця перевірка корисна, якщо використовуєте хмарні сервіси, і боїтесь, що завтра прилетить великий рахунок за обслуговування. А загалом просто приємно бачити, як "росте" ваш проєкт.
Health::checks
QueueSizeCheck::new()
             ->queue('default', 100),
  • Кількість процесів в черзі - дуже корисний показник. Він покаже аномальну активність або випадки, коли все погано і процеси накопичуються.
Health::checks
UsedDiskSpaceCheck::new()->unless($isLocal),
  • Дуже корисно для тих, хто зберігає файли або тримає БД на сервері з кодом (не раджу). Але й в іншому випадку допоможе вчасно зреагувати, якщо пам'ять забилась, наприклад, docker-зображеннями або надмірними лог-файлами. Локально немає сенсу запускати.
Health::checks
RedisCheck::new()->unless($isLocal),
  • Має сенс, нагадає, якщо з Redis щось не ок. Локально можна не вмикати.
Health::checks
CpuLoadCheck::new()
             ->unless($isLocal)
             ->failWhenLoadIsHigherInTheLastMinute(8)
             ->failWhenLoadIsHigherInTheLast5Minutes(8.0)
             ->failWhenLoadIsHigherInTheLast15Minutes(8.5),
  • Тут треба знати про Unix CPU load numbers. Якщо коротко, то це навантаження на процесори за останню хвилину, 5 хвилин та 15 хвилин. 1 - одне ядро. Показник корисний для своєчасного виявлення проблем. Наприклад, у випадку неприємних зациклених процесів тощо. Ліміти встановлюйте в залежності від показників вашого сервера! Локально сенсу немає.
Health::checks
DebugModeCheck::new()
    ->unless($isLocal),
  • Дуже корисно на серверах. Як мінімум дасть по руках при першому деплої, бо DevOps зазвичай не звертає увагу на ваш DEBUG_MODE. І як максимум - якщо вам таки довелось його врубити вручну - буде нагадувати, поки не відключите. Ну і на всяк випадок, DEBUG_MODE - показує дуже багато критичної інформації. НЕ ВМИКАЙТЕ ЙОГО НА СЕРВЕРАХ!
Health::checks
RedisMemoryUsageCheck::new()
                ->unless($isLocal)
                ->failWhenAboveMb(3000),
  • Допоможе виявити якісь зациклені записи в кеш. І звісно, якщо використовуєте хмарні рішення - одразу натякне, що щось не так і гроші зараз полетять.
Health::checks
SecurityAdvisoriesCheck::new()
    ->unless($isLocal),
  • Про перевірку безпеки я розповідав в рамках PHP Insigts пакету. І тут така перевірка - дуже приємний сюрприз. Абсолютно корисна, якщо проєкт вже на релізі й не оновлюється кожен день. Надасть змогу реагувати на вразливості раніше, ніж недобросовісні конкуренти.
Health::checks
SslCertificationExpiredCheck::new()
    ->unless($isLocal)
    ->url($appUrl)->warnWhenSslCertificationExpiringDay(7)
    ->failWhenSslCertificationExpiringDay(3),
SslCertificationValidCheck::new()
    ->unless($isLocal)
    ->url($appUrl),
  • Дуже корисно знати й вчасно реагувати на expired сертифікатів. Цю перевірку часто мають і зовнішні монітори, тож можна використовувати їх. І якщо проєкт має багато доменів - вказуйте кожен.
Health::checks
OptimizedAppCheck::new()
    ->unless($isLocal),
  • Обов'язково для prod та stage. Розробники дуже часто забувають про оптимізацію. DevOps це взагалі не хвилює. І звісно ж бувають випадки, коли розробник почистив кеш прямо на сервері та забув повернути назад.
Health::checks
CacheCheck::new()
   ->unless($isLocal)
  • Перевіряє, чи все ок з кешем. Теоретично збігається з перевіркою Redis. Але все ж виявляє проблеми з конфігурацією і доступом до директорій, якщо що.

Інші перевірки

  • BackupsCheck - якщо зберігаєте БД або файли на сервері разом з проєктом. Я не раджу. S3 для файлів, Cloud для баз даних. Якщо ж використовуєте такі backups - звісно перевіряйте.
  • DatabaseCheck - корисно, якщо маєте більше однієї бази даних. Або не використовуєте базу даних як сховище для перевірок. У моєму випадку перевірка і так, і так впаде, якщо БД помре.
  • EnvironmentCheck - звучить як ок, але абсолютно не розумію, як цим користуватися в роботі. Перевіряти чи це prod лише якщо prod env?
  • FlareErrorOccurrenceCountCheck - специфічно, якщо використовуєте Flare як баг треккер.
  • MeiliSearch - якщо використовуєте саме MeiliSearch для fulltext пошуку. Для інших достатньо PingCheck.
  • PingCheck - корисно для мікросервісів, зовнішніх HTTP сервісів і так далі. Використаю, як з'явиться необхідність.
  • QueueCheck - має сенс повісити на черги з особливо важливим тегом, або коли не використовуєте HorizonCheck. Ну або ж декілька сервісів черг.
  • EnvVars - цікава перевірка, допоможе відловити випадкове видалення якоїсь конфігурації. Але не бачу сенсу перевіряти кожну хвилину (можливо, я не правий). Теоретично лише при деплої буде ок.

Telescope setup

Unfortunately, much of the health functionality could not be ignored in Laravel Telescope. But some things are possible

config/telescope.php
Watchers\CommandWatcher::class => [
        'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
        'ignore' => [
            'health:check-property',
            'health:check',
            'health:schedule-check-heartbeat'
        ],
    ],