Senior: питання на співбесіді з теми «Помилки й логування»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
2 питання
Сценарій: зовнішній API ліг, і кожен запит, що до нього звертається, кидає той самий виняток. За хвилину - десятки тисяч однакових звітів: лог росте на гігабайти, Sentry вичерпує квоту за годину, а справжні нові помилки тонуть у шумі.
throttle() у bootstrap/app.php - вибіркове звітування:
use Illuminate\Support\Lottery;
use Illuminate\Cache\RateLimiting\Limit;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
// кожен 1000-й звіт про часті помилки
if ($e instanceof ApiMonitoringException) {
return Lottery::odds(1, 1000);
}
// не більше 300 звітів на хвилину для збоїв зовнішніх сервісів
if ($e instanceof BroadcastException) {
return Limit::perMinute(300);
}
// ліміт окремо для кожного типу винятку
return Limit::perMinute(300)->by($e::class);
});
})
Два режими:
Lottery |
Limit |
|
|---|---|---|
| що робить | звітує випадкову частку | звітує не більше N за період |
| стан | не потрібен | лічильник у кеші (потрібен спільний драйвер, наприклад Redis) |
| для чого | дуже часті, неважливі за кількістю | сплески, при яких важливо бачити кожен перший випадок |
by() визначає, що вважається «тим самим»: за класом винятку, за повідомленням, за ідентифікатором тенанта. Без by() усі винятки ділять один спільний ліміт.
Чого throttle не замінює:
- circuit breaker - якщо сервіс лежить, не варто взагалі звертатися до нього тисячі разів: це і зайве навантаження, і повільні запити користувачів;
- агрегацію в Sentry чи Flare: вони групують однакові помилки, але кожна подія все одно рахується в квоту - тому обмеження на боці застосунку економить гроші;
- сповіщення: про те, що помилка стала частою, мають сповіщати метрики й моніторинг, а не кількість записів у лозі.
Ще кілька захистів від шуму:
dontReportDuplicates()- один виняток, переданий уreport()кілька разів, звітується раз;- рівні:
level(ConnectException::class, LogLevel::WARNING)- збій зовнішнього сервісу не повинен виглядати якerrorзастосунку; - ротація логів (канал
dailyзdays) і збір логів у централізоване сховище замість файлу на диску.
Перевірка: зробити штучний сплеск у staging і подивитися, скільки записів реально пішло в лог і в Sentry.
Докладніше в документації: Помилки: обмеження частоти звітів
Файл storage/logs/laravel.log зручний локально, але на продакшені з кількома серверами чи контейнерами логи мають збиратися централізовано (Loki, ELK, CloudWatch, Datadog), і читати їх буде не людина в tail, а система пошуку.
1. Канал у stderr для контейнерів:
// config/logging.php
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'info'),
'handler' => StreamHandler::class,
'handler_with' => ['stream' => 'php://stderr'],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
LOG_CHANNEL=stderr
LOG_STDERR_FORMATTER=Monolog\Formatter\JsonFormatter
Docker збирає stderr контейнера, а JSON - по одному об'єкту на рядок - збирачі розбирають без регулярних виразів: рівень, повідомлення, контекст стають полями для фільтрації.
2. Процесори Monolog - додають дані до кожного запису:
'processors' => [
PsrLogMessageProcessor::class, // підставляє {placeholders} з контексту в повідомлення
WebProcessor::class, // url, метод, ip
MemoryUsageProcessor::class,
],
3. tap - довільне налаштування логера каналу класом:
'stack' => [
'driver' => 'stack',
'tap' => [App\Logging\AddHostname::class],
'channels' => ['stderr'],
],
final class AddHostname
{
public function __invoke(Logger $logger): void
{
foreach ($logger->getHandlers() as $handler) {
$handler->pushProcessor(function (LogRecord $record): LogRecord {
return $record->with(extra: [...$record->extra, 'host' => gethostname()]);
});
}
}
}
4. Стек каналів - різні рівні в різні місця:
'stack' => ['driver' => 'stack', 'channels' => ['stderr', 'slack'], 'ignore_exceptions' => false],
'slack' => ['driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'level' => 'critical'],
Що ще важливо:
- кореляція:
Context::add('trace_id', ...)у middleware - і всі записи запиту та його завдань у черзі пов'язані одним ідентифікатором; - канал
deprecations- окремо від основного логу, щоб попередження про застарілий код не тонули й не засмічували його; - рівень на продакшені -
infoчиwarning;debugшвидко генерує гігабайти; - локальні файли без ротації на сервері рано чи пізно заповнять диск -
dailyзdaysабо зовнішній збирач; - логи - не сповіщення: про критичні помилки має повідомляти система моніторингу помилок, а лог - для розслідування.
Докладніше в документації: Логування: налаштування Monolog для каналів