Разработка · 7 мин чтения

Laravel Telescope: как найти причину медленных запросов

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 только действительно нужными для задачи и настраиваю регулярную очистку вместо постоянной работы всех вотчеров разом.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно