13. Laravel Health. Налаштування та використання в docker healthcheck.
Відео версія
(YouTube - для зворотнього зв'язку)
TL; DR
Пакет Laravel Health не є заміною повноцінної моніторингової системи, але може її доповнити. Використовуйте зовнішні моніторингові системи для нотифікацій та запуску команди перевірки.
Для мого проєкту я створив окремий модуль для health check та виніс методи до Action. Якщо ваш проєкт не використовує модулі або Actions - раджу це зробити.
Обов'язково закривайте web-view на продакшені та stage. Ідеально надати доступ лише з корпоративної IP адреси!
Робочий процес
Важливий момент - в документації радять повісити перевірку на schedule в проєкті. Це не є найкращим підходом, оскільки якщо ваш додаток "помре", ніхто не запустить schedule перевірку і жодної нотифікації не буде.
Також, відсилати нотифікації про зламаний додаток зі поламаного додатка не ок. Тому логіка роботи така:
- Встановлюємо або купуємо будь-який зовнішній сервіс моніторингу. Наприклад, uptime kuma. Ідеально - на іншому сервері, ніж APP, що моніторимо.
- Сервіс пінгує
/health-check
шлях та надсилає нотифікації до потрібного нам каналу. /health-check
поверне статус503
, коли хоча б одна з перевірок не пройшла. Тут треба бути обережним з налаштуванням - статусиskipped
також вважаються помилкою. Якщо це не влаштовує - пишемо свою реалізацію контролераSimpleHealthCheckController
.- При нотифікації розробники йдуть до
/health
, якщо той доступний, якщо ні - на сервер і так далі.
Авторизація
Не забувайте про авторизацію. Корпоративний ІР - все ще кращий варіант.
Реалізація конфігурації для авторизації в Service Provider:
[
'path' => env('HEALTH_PATH', 'health'),
'check_path' => env('HEALTH_CHECK_PATH', 'health-check'),
'middleware' => [
'web',
VeryBasicAuth::class
]
]
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
:
<?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 перевірок також пакет не пропонує готових рішень. Тому реалізував власну команду:
<?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:
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 - не варіант. Бо це перевірка самого себе.
Прийняті перевірки
Набір перевірок
OctaneCheck::new()
- Використовую в healthcheck app сервісу. Будьте уважні, якщо запускаєте з schedule сервісу - там октан не запущений.
HorizonCheck::new(),
- Перевіряє, чи Active статус хорайзон. ТОП для healthcheck Horizon сервісу.
ScheduleCheck::new()->useCacheStore(config('cache.default')),
- Тут важливо розуміти принцип роботи. Обов'язково треба додати команду перевірки. Вона кожну хвилину записує мітку часу до кешу. Якщо час в кеші відстає більше ніж на 2 хвилини - команда не виконалась і schedule сервіс не працює. Звісно для healthcheck schedule контейнеру.
$schedule->command(ScheduleCheckHeartbeatCommand::class)->everyMinute();
DatabaseConnectionCountCheck::new()
->failWhenMoreConnectionsThan(20),
- Тут треба розуміння. Для Laravel Octane - одне з'єднання на один
worker
та декілька системних процесів: autovacuum, background writer і так далі. Це фішка Octane - одне постійне з'єднання. Якщо ви не використовуєте Octane - кожен запит = +1 з'єднання (а може й більше). Тому у випадку з Octane 20 з'єднань звучить як ок. Для інших - залежить від навантаження на систему.
DatabaseSizeCheck::new()
->failWhenSizeAboveGb(errorThresholdGb: 25.0),
- Ця перевірка корисна, якщо використовуєте хмарні сервіси, і боїтесь, що завтра прилетить великий рахунок за обслуговування. А загалом просто приємно бачити, як "росте" ваш проєкт.
QueueSizeCheck::new()
->queue('default', 100),
- Кількість процесів в черзі - дуже корисний показник. Він покаже аномальну активність або випадки, коли все погано і процеси накопичуються.
UsedDiskSpaceCheck::new()->unless($isLocal),
- Дуже корисно для тих, хто зберігає файли або тримає БД на сервері з кодом (не раджу). Але й в іншому випадку допоможе вчасно зреагувати, якщо пам'ять забилась, наприклад, docker-зображеннями або надмірними лог-файлами. Локально немає сенсу запускати.
RedisCheck::new()->unless($isLocal),
- Має сенс, нагадає, якщо з Redis щось не ок. Локально можна не вмикати.
CpuLoadCheck::new()
->unless($isLocal)
->failWhenLoadIsHigherInTheLastMinute(8)
->failWhenLoadIsHigherInTheLast5Minutes(8.0)
->failWhenLoadIsHigherInTheLast15Minutes(8.5),
- Тут треба знати про Unix CPU load numbers. Якщо коротко, то це навантаження на процесори за останню хвилину, 5 хвилин та 15 хвилин. 1 - одне ядро. Показник корисний для своєчасного виявлення проблем. Наприклад, у випадку неприємних зациклених процесів тощо. Ліміти встановлюйте в залежності від показників вашого сервера! Локально сенсу немає.
DebugModeCheck::new()
->unless($isLocal),
- Дуже корисно на серверах. Як мінімум дасть по руках при першому деплої, бо DevOps зазвичай не звертає увагу на ваш DEBUG_MODE. І як максимум - якщо вам таки довелось його врубити вручну - буде нагадувати, поки не відключите. Ну і на всяк випадок, DEBUG_MODE - показує дуже багато критичної інформації. НЕ ВМИКАЙТЕ ЙОГО НА СЕРВЕРАХ!
RedisMemoryUsageCheck::new()
->unless($isLocal)
->failWhenAboveMb(3000),
- Допоможе виявити якісь зациклені записи в кеш. І звісно, якщо використовуєте хмарні рішення - одразу натякне, що щось не так і гроші зараз полетять.
SecurityAdvisoriesCheck::new()
->unless($isLocal),
- Про перевірку безпеки я розповідав в рамках PHP Insigts пакету. І тут така перевірка - дуже приємний сюрприз. Абсолютно корисна, якщо проєкт вже на релізі й не оновлюється кожен день. Надасть змогу реагувати на вразливості раніше, ніж недобросовісні конкуренти.
SslCertificationExpiredCheck::new()
->unless($isLocal)
->url($appUrl)->warnWhenSslCertificationExpiringDay(7)
->failWhenSslCertificationExpiringDay(3),
SslCertificationValidCheck::new()
->unless($isLocal)
->url($appUrl),
- Дуже корисно знати й вчасно реагувати на expired сертифікатів. Цю перевірку часто мають і зовнішні монітори, тож можна використовувати їх. І якщо проєкт має багато доменів - вказуйте кожен.
OptimizedAppCheck::new()
->unless($isLocal),
- Обов'язково для prod та stage. Розробники дуже часто забувають про оптимізацію. DevOps це взагалі не хвилює. І звісно ж бувають випадки, коли розробник почистив кеш прямо на сервері та забув повернути назад.
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
Watchers\CommandWatcher::class => [
'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
'ignore' => [
'health:check-property',
'health:check',
'health:schedule-check-heartbeat'
],
],