Логирование внешних HTTP-запросов с Laravel Telescope
Самая большая проблема при работе с внешними API заключается в том, что у нас очень мало прозрачности.
Мы интегрируем их в нашу кодовую базу и тестируем, но мы не знаем, насколько часто мы их используем, если только API, с которым мы интегрируемся, не предоставляет метрики, которые мы можем использовать. Я довольно долго был расстроен этим, но появилось элегантное решение.
Laravel Telescope — это помощник по отладке для вашего приложения, который умеет логировать и давать вам представление о том, что происходит на высоком уровне. Мы можем подключиться к этому и добавить пользовательские наблюдатели для повышения отладки и логирования, и именно это мы сделаем в этом коротком руководстве.
После установки Laravel Telescope убедитесь, что вы опубликовали конфигурацию и выполнили миграцию базы данных, мы можем начать создавать наш наблюдатель для Guzzle — клиента под фасадом Http.
Самое логичное место для хранения этих классов, по крайней мере для меня, — внутри app/Telescope/Watchers, поскольку код принадлежит нашему приложению — но мы расширяем сам Telescope. Но как выглядит стандартный Наблюдатель?
class YourWatcher extends Watcher
{
public function register($app): void
{
// handle code for watcher here.
}
}
Вы можете добавить столько методов, сколько нужно, чтобы создать Наблюдателя, который работает для вас. Итак, без лишних слов, давайте создадим новый класс app/Telescope/Watchers/GuzzleRequestWatcher.php, и мы разберем, что он должен делать.
declare(strict_types=1);
namespace App\\Telescope\\Watchers;
use GuzzleHttp\\Client;
use GuzzleHttp\\TransferStats;
use Laravel\\Telescope\\IncomingEntry;
use Laravel\\Telescope\\Telescope;
use Laravel\\Telescope\\Watchers\\FetchesStackTrace;
use Laravel\\Telescope\\Watchers\\Watcher;
final class GuzzleRequestWatcher extends Watcher
{
use FetchesStackTrace;
}
Сначала нам нужно включить трейт FetchesStackTrace, поскольку он позволяет перехватывать запросы. Если мы рефакторим эти HTTP-вызовы в другие места, мы можем убедиться, что вызываем их так, как задумано. Далее нам нужно добавить метод для регистрации нашего Наблюдателя:
declare(strict_types=1);
namespace App\\Telescope\\Watchers;
use GuzzleHttp\\Client;
use GuzzleHttp\\TransferStats;
use Laravel\\Telescope\\IncomingEntry;
use Laravel\\Telescope\\Telescope;
use Laravel\\Telescope\\Watchers\\FetchesStackTrace;
use Laravel\\Telescope\\Watchers\\Watcher;
final class GuzzleRequestWatcher extends Watcher
{
use FetchesStackTrace;
public function register($app)
{
$app->bind(
abstract: Client::class,
concrete: $this->buildClient(
app: $app,
),
);
}
}
Мы перехватываем клиент Guzzle и регистрируем его в контейнере, но для этого мы хотим указать, как мы хотим, чтобы клиент был построен. Давайте посмотрим на метод buildClient:
private function buildClient(Application $app): Closure
{
return static function (Application $app): Client {
$config = $app['config']['guzzle'] ?? [];
if (Telescope::isRecording()) {
// Record our Http query.
}
return new Client(
config: $config,
);
};
}
Здесь мы возвращаем статическую функцию, которая строит наш клиент Guzzle. Сначала мы получаем любую конфигурацию guzzle — а затем, если Telescope записывает, мы добавляем способ записи запроса. Наконец, мы возвращаем клиент с его конфигурацией. Так как же мы записываем наш HTTP-запрос? Давайте посмотрим:
if (Telescope::isRecording()) {
$config['on_stats'] = static function (TransferStats $stats): void {
$caller = $this->getCallerFromStackTrace(); // This comes from the trait we included.
Telescope::recordQuery(
entry: IncomingEntry::make([
'connection' => 'guzzle',
'bindings' => [],
'sql' => (string) $stats->getEffectiveUri(),
'time' => number_format(
num: $stats->getTransferTime() * 1000,
decimals: 2,
thousand_separator: '',
),
'slow' => $stats->getTransferTime() > 1,
'file' => $caller['file'],
'line' => $caller['line'],
'hash' => md5((string) $stats->getEffectiveUri())
]),
);
};
}
Таким образом, мы расширяем конфигурацию, добавляя опцию on_stats, которая является колбэком. Этот колбэк получит стек-трейс и запишет новый запрос. Эта новая запись будет содержать все релевантные вещи, связанные с запросом, которые мы можем записать. Итак, если собрать все вместе:
declare(strict_types=1);
namespace App\Telescope\Watchers;
use Closure;
use GuzzleHttp\Client;
use GuzzleHttp\TransferStats;
use Illuminate\Foundation\Application;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Laravel\Telescope\Watchers\FetchesStackTrace;
use Laravel\Telescope\Watchers\Watcher;
final class GuzzleRequestWatcher extends Watcher
{
use FetchesStackTrace;
public function register($app): void
{
$app->bind(
abstract: Client::class,
concrete: $this->buildClient(
app: $app,
),
);
}
private function buildClient(Application $app): Closure
{
return static function (Application $app): Client {
$config = $app['config']['guzzle'] ?? [];
if (Telescope::isRecording()) {
$config['on_stats'] = function (TransferStats $stats) {
$caller = $this->getCallerFromStackTrace();
Telescope::recordQuery(
entry: IncomingEntry::make([
'connection' => 'guzzle',
'bindings' => [],
'sql' => (string) $stats->getEffectiveUri(),
'time' => number_format(
num: $stats->getTransferTime() * 1000,
decimals: 2,
thousands_separator: '',
),
'slow' => $stats->getTransferTime() > 1,
'file' => $caller['file'],
'line' => $caller['line'],
'hash' => md5((string) $stats->getEffectiveUri()),
]),
);
};
}
return new Client(
config: $config,
);
};
}
}
Теперь все, что нам нужно сделать, — это убедиться, что мы регистрируем этот новый Наблюдатель внутри config/telescope.php, и мы должны увидеть, как логируются наши HTTP-запросы.
'watchers' => [
// all other watchers
App\\Telescope\\Watchers\\GuzzleRequestWatcher::class,
]
Чтобы протестировать его, создайте тестовый маршрут:
Route::get('/guzzle-test', function () {
Http::post('<https://jsonplaceholder.typicode.com/posts>', ['title' => 'test']);
});
Когда вы откроете Telescope, вы должны увидеть элемент навигации сбоку под названием HTTP Client, и если вы откроете его, то увидите логи — вы можете проверить заголовки, полезную нагрузку и статус запроса.
Таким образом, если вы начнете видеть сбои от интеграций API, это сильно поможет вам в отладке.

