🔭

Laravel+TelescopeでSQLの呼び出し階層を出力する

2021/10/01に公開

追記

この記事の内容を「Telescope Plus」というパッケージにして、Composer公式リポジトリに登録しました。
以下の記事をご覧ください。

https://zenn.dev/yamabiko/articles/laravel-telescope-plus

はじめに

みなさん、Telescopeを使っていますか?
Telescopeでは発行したSQL、キャッシュ、セッション、メールなどなど様々な情報を確認できてすごく便利ですよね。
でも、SQLの呼び出し階層をもっと詳しく知ることができたらいいのに・・・って思ったことはありませんか?
この記事を読むと、以下の画像のように、SQLの呼び出し階層をクラス名、または、ファイル名、関数名、行番号を(色付きで)表示できるようになります。

バージョン情報

  • PHP 8.0.9
  • laravel/framework: 8.54
  • laravel/telescope: 4.6

Telesopceのインストール

Laravel公式ドキュメント通りにTelesopeをローカルのみへインストールします。

$ composer require laravel/telescope --dev

$ php artisan telescope:install

$ php artisan migrate
App\Providers\AppServiceProvider
/**
 * 全アプリケーションサービスの登録
 *
 * @return void
 */
public function register()
{
    if ($this->app->environment('local')) {
        $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
        $this->app->register(TelescopeServiceProvider::class);
    }
}
composer.json
"extra": {
    "laravel": {
        "dont-discover": [
+            "laravel/telescope"
        ]
    }
},

ソースコードの改修

vendor以下のソースコードですが、直接改修してしまいます。

まず、以下のQueryWatcher.phpにデバッグトレースを整形するメソッドを追加します。
文字列のハイライト表示は以下の通りです。

項目 カラー
APP配下のクラス
データベース関連のクラス
ミドルウェア

呼び出し階層の初期値は30階層ですが、お好みの階層に調整してください。

vendor/laravel/telescope/src/Watchers/QueryWatcher.php
    /**
     * デバッグトレースを整形する
     *
     * @param integer $limit
     * @return void
     */
    protected function formatBacktrace($limit = 30)
    {
        try {
            $backTrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
            $result = collect($backTrace)->map(function($item){
                if (array_key_exists('class', $item)) {
                    // クラス名を含む場合、クラス名を出力する
                    if (array_key_exists('line', $item)) {
                        // 行番号を含む場合、行番号を出力する
                        $line = $item['class'] . ":" . $item['line'] . " " . $item['function'];
                    } else {
                        // 行番号を含まない場合(クロージャの場合)、行番号を出力しない
                        $line = $item['class'] . " " . $item['function'];
                    }
                } else {
                    // クラス名を含まない場合、ファイル名を出力する
                    $line = $item['file'] . ":" . $item['line'] . " " . $item['function'];
                }

                if(str_starts_with($line, 'Laravel\\Telescope\\')) {
                    // Telescopeのメソッド呼び出しは出力しない
                    return;
                }

                if(str_starts_with($line, 'App\\')) {
                    // APP\始まる場合、赤でハイライト表示する
                    $htmlLine = "<span style=\"font-weight: bold;color:red\">${line}</span>";
                } else if(str_starts_with($line, 'Illuminate\\Database\\')) {
                    // Illuminate\Database\で始まる場合、青でハイライト表示する
                    $htmlLine = "<span style=\"font-weight: bold;color:blue\">${line}</span>";
                } else if(strpos($line, '\\Middleware\\') !== false) {
                    // \Middleware\を含む場合、緑でハイライト表示する
                    $htmlLine = "<span style=\"font-weight: bold;color:green\">${line}</span>";
                } else {
                    $htmlLine = $line;
                }

                return $htmlLine;
            })->join('<br>');

             return $result;
        } catch (Exception $e) {
            Log::error($e->getMessage());
        }
    }

次に、追加したformatBacktraceメソッドを呼び出すよう、同じQueryWatcher.phpを改修します。

vendor/laravel/telescope/src/Watchers/QueryWatcher.php
    /**
     * Record a query was executed.
     *
     * @param  \Illuminate\Database\Events\QueryExecuted  $event
     * @return void
     */
    public function recordQuery(QueryExecuted $event)
    {
        if (! Telescope::isRecording()) {
            return;
        }

        $time = $event->time;

        if ($caller = $this->getCallerFromStackTrace()) {
            Telescope::recordQuery(IncomingEntry::make([
                'connection' => $event->connectionName,
                'bindings' => [],
                'sql' => $this->replaceBindings($event),
                'time' => number_format($time, 2, '.', ''),
                'slow' => isset($this->options['slow']) && $time >= $this->options['slow'],
+                'file' => $this->formatBacktrace(),
-                'file' => $caller['file'],
                'line' => $caller['line'],
                'hash' => $this->familyHash($event),
            ])->tags($this->tags($event)));
        }
    }

画面を改修します。
呼び出し階層をhtml形式で出力するので、エスケープしないようv-htmlディレクティブで出力するよう修正します。

vendor/laravel/telescope/resources/js/screens/queries/preview.vue
            <tr  v-if="slotProps.entry.content.file">
                <td class="table-fit font-weight-bold">Location</td>
+                <td v-html="slotProps.entry.content.file"></td>
-                <td>
-                    {{slotProps.entry.content.file}}:{{slotProps.entry.content.line}}
-                </td>
            </tr>

Laravel Mixを実行して、app.jsをコピーします。

$ cd vendor/laravel/telescope/
$ npm install
$ sail npm run dev
$ cp ./public/app.js ../../../public/vendor/telescope/

以上でソースコードの改修は完了です。

実行結果

画面を操作してTelescopeでSQLを確認すると、以下のように呼び出し階層が表示されます。

まとめ

「なぜ、SQL呼び出し階層を見える化しようと思ったか?」なんですが、Laravel Breezeの仕様を調べていたことがきっかけでした。
いつ、どんなSQLが発行されるのか把握したくて、QueryWatcherクラスにブレークポイントを貼ってスタックトレースをひたすら追っていたんですよね。
次第に面倒になってきて、「ええい!スタックトレースを出力してしまおう!」と考えたことが、この記事を書くきっかけになりました😅
SQLの呼び出し階層が見えるようになったので、どのような経緯でSQLが発行されたのか?、把握しやすくなったと思います。
どこにブレークポイントを設定したらよいかわかりやすくなるので、デバッグも捗りそうですね😄
他にも、redisコマンドの出力なども改善の余地があるように思います。
順番に対応できたらいいなと思っています。

Discussion