Laravel Telescope я подключаю почти на каждом Laravel-бэкенде, как только заказчик жалуется, что страница или API-эндпоинт стали заметно тормозить. Вместо того чтобы обвешивать код Log::info() и гадать, где теряется время, Telescope сразу показывает всю цепочку событий одного запроса: какие SQL-запросы выполнились, сколько миллисекунд занял каждый и из какого места в коде он был вызван. На одном проекте с приёмом вебхуков от СДЭК и оплатой через T‑Bank именно через вкладку Queries я нашёл запрос, который выполнялся по 40 раз на один вебхук вместо одного, и правка заняла 15 минут вместо дня поиска вручную.
Что показывает Laravel Telescope и почему это быстрее ручных логов
Telescope - официальный пакет от команды Laravel, он пишет данные о работе приложения в отдельные таблицы базы и показывает их через веб-интерфейс на /telescope. В отличие от логов, которые нужно расставлять руками заранее, Telescope перехватывает события автоматически через свои «вотчеры» - каждый отвечает за свой тип данных.
Для поиска медленных мест обычно смотрю такие вкладки:
- Requests - список HTTP-запросов с временем ответа целиком
- Queries - каждый SQL-запрос, его время выполнения и место вызова в коде
- Jobs - выполнение задач из очереди, полезно для фоновой обработки вебхуков
- Cache и Redis - попадания и промахи кэша
- Exceptions - исключения с полным стектрейсом
Для задачи «почему тормозит» в большинстве случаев ответ находится во вкладке Queries: либо один тяжёлый запрос без индекса, либо десятки одинаковых мелких запросов вместо одного.
Установка и первый запуск Telescope
Ставится пакет через composer, дальше публикуется конфиг и миграции, создающие таблицы для хранения записей:
composer require laravel/telescope
php artisan telescope:install
php artisan migrate
После миграции интерфейс доступен по адресу /telescope. В окружении local доступ открыт сразу, для остальных окружений нужна отдельная авторизация - расскажу про неё в разделе про продакшен.
Сразу после установки стоит заглянуть в config/telescope.php - там список вотчеров можно включать и выключать по отдельности. Если проект большой и на каждый запрос пишется много данных, лишние вотчеры (например, Dumps или Views) я обычно отключаю, чтобы не засорять базу.
Вкладка Queries: как читать список SQL-запросов
Каждая строка в Queries - это один выполненный SQL-запрос со следующими данными:
- SQL - текст запроса с уже подставленными значениями вместо плейсхолдеров, копируется одной кнопкой
- Time - время выполнения в миллисекундах
- Connection - имя соединения с базой, если их в проекте несколько
- Caller - файл и строка в коде приложения, откуда запрос был вызван
Caller собирается через backtrace и по умолчанию отфильтровывает кадры вызова из vendor, поэтому в колонке видно именно свой код, а не внутренности Eloquent. Это и есть главное преимущество перед обычным логом запросов через DB::listen() - не нужно самому парсить стектрейс, чтобы понять, откуда прилетел запрос.
Если открыть конкретную запись во вкладке Requests, внизу будет список всех запросов, выполненных именно в рамках этого запроса или джобы, в порядке выполнения. Это «водопад» одного запроса целиком, и по нему видно не только медленные единичные запросы, но и структуру - сколько всего запросов ушло в базу за один HTTP-запрос.
Порог медленных запросов (slow threshold)
В конфиге вотчера Queries есть параметр slow - все запросы дольше указанного значения подсвечиваются в списке отдельным цветом, чтобы не листать все строки в поисках нужной:
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
'ignore_packages' => true,
'ignore_paths' => [],
'slow' => 100,
],
По умолчанию порог - 100 мс, и на большинстве проектов с MySQL на обычном хостинге это разумный старт: запросы дольше этого значения уже стоит смотреть отдельно. Если бэкенд работает с внешними API внутри транзакции или с тяжёлой аналитикой, порог поднимаю до 300-500 мс, чтобы в подсветку не попадали заведомо тяжёлые, но ожидаемые операции.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Поиск N+1 запросов через Telescope
N+1 - это когда вместо одного запроса с JOIN или eager loading выполняется один запрос на список записей плюс по одному дополнительному запросу на каждую связанную запись. На списке из 50 заказов с выводом имени клиента это будет не 2 запроса, а 51.
В Telescope такие ситуации ищу глазами: открываю конкретную запись во вкладке Requests, смотрю список её запросов и вижу подряд идущие почти одинаковые строки SQL, отличающиеся только значением id в условии WHERE. Колонка Caller при этом у всех повторяющихся запросов указывает на одну и ту же строку кода - обычно это обращение к связи внутри foreach или в шаблоне.
Пример из практики - вывод списка заказов с именем клиента:
// было: 1 запрос на список + N запросов на клиента
$orders = Order::all();
foreach ($orders as $order) {
echo $order->customer->name;
}
// стало: 2 запроса независимо от количества заказов
$orders = Order::with('customer')->get();
foreach ($orders as $order) {
echo $order->customer->name;
}
После правки в Telescope в этой же секции запросов видно, что вместо десятков строк с одинаковым Caller остаётся один запрос с eager loading через with(). Проверка занимает минуту: обновил страницу, открыл последнюю запись в Requests, посмотрел количество строк в Queries.
Что делать после того, как нашёл медленный запрос
Дальше решение зависит от причины, и она обычно видна прямо по тексту SQL-запроса и его Caller:
| Причина | Что делаю |
|---|---|
| N+1 при обращении к связям | Добавляю eager loading через with() или load() |
| Нет индекса на колонке в WHERE или JOIN | Добавляю индекс миграцией, проверяю через EXPLAIN |
| SELECT * при большом числе колонок | Выбираю только нужные поля через select() |
| Тяжёлая агрегация на каждый показ страницы | Кэширую результат через Cache::remember() на разумный срок |
| Синхронный вызов внешнего API внутри запроса (СДЭК, T‑Bank) | Переношу вызов в очередь, обрабатываю job-ом |
С индексами отдельно стоит быть аккуратным: беру текст запроса из Telescope, бинды уже подставлены, и прогоняю его через EXPLAIN прямо в консоли MySQL - там видно, использует ли запрос индекс или идёт полным сканированием таблицы. Telescope сам EXPLAIN не показывает, это ручной шаг после того, как нашёл подозрительный запрос.
Telescope в продакшене: авторизация и очистка данных
По умолчанию интерфейс Telescope открыт всем в окружении local. На проде доступ нужно закрыть отдельно, а заодно продумать, кто вообще будет регулярно смотреть на такие вещи после релиза - удобно, когда этим занимается кто-то на постоянной основе, например в рамках техподдержку сайта. Доступ к самому интерфейсу ограничиваю через Gate в TelescopeServiceProvider:
protected function gate(): void
{
Gate::define('viewTelescope', function ($user) {
return in_array($user->email, [
'admin@example.com',
]);
});
}
Второй момент - каждая запись любого вотчера пишется в базу, и на активном проекте таблица telescope_entries растёт быстро. Регулярно чищу её командой telescope:prune, которую добавляю в планировщик:
protected function schedule(Schedule $schedule): void
{
$schedule->command('telescope:prune --hours=48')->daily();
}
Если Telescope нужен только на время разбора конкретной проблемы, а не постоянно, проще держать его выключенным через TELESCOPE_ENABLED=false в .env и включать точечно на время диагностики.
Laravel регулярно обновляется, поэтому перед внедрением я сверяюсь с актуальной документацией.
Частые вопросы
Можно ли использовать Laravel Telescope в продакшене?
Да, технически можно - Laravel даёт механизм авторизации через Gate в TelescopeServiceProvider, ограничивающий доступ по email пользователя. Но нужно заранее продумать нагрузку от постоянной записи entries в базу и регулярно чистить их командой telescope:prune, иначе таблица разрастётся до неудобных размеров за пару недель активного трафика.
Чем Telescope отличается от Laravel Debugbar?
Debugbar показывает панель поверх текущей страницы для одного запроса прямо в браузере во время разработки - удобно, когда сидишь за кодом и сразу видишь результат. Telescope хранит историю всех запросов, джобов и исключений в базе и даёт отдельный интерфейс с фильтрами и поиском по уже прошедшим запросам, что удобнее при разборе конкретного инцидента, о котором сообщили постфактум, или при сравнении нескольких похожих запросов между собой.
Как понять, что запрос действительно медленный, а не разовая аномалия?
Смотрю не одно значение времени, а несколько записей одного и того же запроса за какой-то период. Если время стабильно выше порога в 100 мс на разных пользователях и разных наборах данных, а не разово из-за холодного кэша или блокировки в базе на момент замера, это повод оптимизировать. Разовый скачок на 300-400 мс при в целом стабильной картине обычно можно списать на нагрузку в моменте.
Замедляет ли сам Telescope работу сайта?
Заметная лишняя нагрузка есть, поскольку каждый вотчер пишет свои записи в базу при каждом запросе. На проде поэтому включаю его точечно на нужный период, ограничиваю набор вотчеров в config/telescope.php только действительно нужными для задачи и настраиваю регулярную очистку вместо постоянной работы всех вотчеров разом.