How does Laravel Telescope work?

How does Laravel Telescope work?

Introduction

ဒီ article မှာတော့ Laravel ရဲ့ First Party Package တစ်ခုဖြစ်တဲ့ Laravel Telescope ရဲ့အလုပ်လုပ်ပုံကို overview ကြည့်ကြမှာဖြစ်ပါတယ်။ Telescope မတိုင်ခင်က Laravel Development တွေ အတွက် Debugging Tools (Debugbar, PHP Clockwork and so on.) တွေကပြန့်ကြဲနေပြီးတော့ တစ်ခုချင်းစီမှာလည်း အားသာချက် အားနည်းချက်တွေနဲ့ ရှိနေပါတယ်။ ဒါကို Laravel Ecosystem ကသူတွေအတွက်ပိုလည်း အဆင်ပြေအောင် Laravel အနေနဲ့ First Party Package ဖြစ်တဲ့ Laravel Telescope ကိုထုတ်ပြီးတော့ Development ကော Production ကော အဆင်ပြေအောင် လုပ်ပေးလိုက်ပုံပါပဲ။

ဒီအချိန်မှာ Telescope က အကောင်းဆုံး tools တစ်ခုဖြစ်လာတာကလည်း Laravel Team ကိုယ်တိုင်က ရှိပြီးသား Built-in events တွေ အပြင် Telescope အတွက်လိုအပ်မယ့် Events တွေကိုပါ Framework Level ကနေ fire လုပ်ပေးလိုက်လို့ပါပဲ။ ဒါကတော့ First Party Package တွေရဲ့အားသာချက်ပါ။ Framework Level ကိုပါပြင်ပြီးလိုအပ်တာအကုန် ထောက်ပံ့ပေးထားတော့ တစ်ခြား Third Party Package တွေထက်ကို ပိုပြီးပြည့်စုံကောင်းမွန်ဖို့ အခွင့်အလမ်းများပါတယ်။

Event-Driven Architecture

Telescope ကို Event-Driven Architecture ကိုသုံးပြီးတော့ တည်ဆောက်ထားပါတယ်။ ဆိုလိုတာက Request Life-Cycle အတွင်းမှာ Laravel Framework ကနေ လိုအပ်တဲ့ Events တွေ Fire လုပ်ပေးမယ်။ Telescope က အဲ့ Events တွေကို Listen လုပ်ပြီးတော့ လိုအပ်သလို Record ယူမယ်၊ သင့်တော်သလို Viewer နဲ့ပြန်ပြပေးမယ် အဲ့တာပါပဲ။

တစ်ကယ်လို့ code diving အပိုင်းကိုပဲကြည့်ချင်တယ်ဆိုရင် Let's dive into the code ကိုတန်းသွားလို့ရပါတယ်။

Available Watchers

Telescope ကိုသုံးပြီး Watch လုပ်လို့ရနိုင်မယ့်ဟာတွေကတော့ အောက်မှာပြထားပါတယ်။ Reference - Available Watchers

Batch Watcher − Batched Jobs တွေကို Queue လုပ်ထားတဲ့ records တွေကို watch လုပ်ဖ်ို့သုံးနိုင်ပါတယ်။

Cache Watcher - Cache ထဲကို interact လုပ်သမျှကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Command Watcher - Command တွေ Run တဲ့အခါ arguments, options, output တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Dump Watcher - dump() ဆိုတဲ့ global function ကိုသုံးပြီး ထုတ်ထားသမျှကို watch လုပ်ဖို့အတွက်သုံးနိုင်ပါတယ်။

Event Watcher - App ထဲမှာ fire/dispatch လုပ်သွားတဲ့ events တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Exception Watcher - Reportable Exceptions တွေတက်တဲ့အခါ watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Gate Watcher - Gate တွေကနေဖြတ်သွားတဲ့ data တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

HTTP Client Watcher - Laravel HTTP Client ကိုသုံးပြီးတော့ တစ်ခြား application တွေကိုလှမ်းခေါ်တဲ့ outgoing requests တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။ ဒီနေရာမှာ သတိထားဖို့လိုတာက Laravel HTTP Client ကိုမသုံးပဲ သူ့အောက်က guzzlehttp/guzzle ကိုတိုက်ရိုက်သွားသုံးမယ်ဆိုရင်တော့ Event Fire လုပ်မပေးတော့တာမလို့ Telescope ရဲ့ HTTP Client အပိုင်းမှာ entries တွေကိုတွေ့ရတော့မှာ မဟုတ်ပါဘူး။

Job Watcher - Job တွေကို watch လုပ်ဖို့အတွက်သုံးနိုင်ပါတယ်။

Log Watcher − Log ထုတ်ထားတာတွေကိုကြည့်ဖို့သုံးနိုင်ပါတယ် (အရင်က တစ်ခြား Package တွေထည့်ပြီးတော့ကြည့်ခဲ့ရတာပါ။ အခုတော့ Telescope တစ်ခုနဲ့တင်အကုန်ပြီးပါတယ်။)

Mail Watcher - App ကပို့တဲ့ mails တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Model Watcher - Model Events တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Notification Watcher - Notifications တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Query Watcher - SQL QUERY တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။ တော်တော် လူသုံးများတဲ့ watcher တစ်ခုဖြစ်ပါတယ်။

Redis Watcher - Redis Command တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Request Watcher - Request တစ်ခုလုံးရဲ့ information တွေကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

Schedule Watcher - Scheduler ကို watch လုပ်ဖို့သုံးနိုင်ပါတယ်။

View Watcher - Blade နဲ့ပတ်သတ်တဲ့ information တွေကိုကြည့်ဖို့သုံးနိုင်ပါတယ်။

Let's dive into the code

Source Diving အပိုင်းမှာတော့ လိုအပ်တဲ့အပိုင်းတွေကိုပဲ ဖြတ်ထုတ်ပြီးတော့ ရှင်းပြသွားမှာဖြစ်ပါတယ်။ ဒီအခြေခံကိုအသုံးချပြီးတော့ ကိုယ်တိုင် ဆက်ပြီးတော့ dive လုပ်နိုင်ပါတယ်။

အရင်ဆုံး Laravel Package တစ်ခုရဲ့ entry point ဖြစ်တဲ့ Service Provider ကိုကြည့်ကြရအောင်။

TelescopeServiceProvider.php

<?php

namespace Laravel\Telescope;

use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Laravel\Telescope\Contracts\ClearableRepository;
use Laravel\Telescope\Contracts\EntriesRepository;
use Laravel\Telescope\Contracts\PrunableRepository;
use Laravel\Telescope\Storage\DatabaseEntriesRepository;

class TelescopeServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->registerCommands();
        $this->registerPublishing();

        if (! config('telescope.enabled')) {
            return;
        }

        Route::middlewareGroup('telescope', config('telescope.middleware', []));

        $this->registerRoutes();
        $this->registerMigrations();

        Telescope::start($this->app);
        Telescope::listenForStorageOpportunities($this->app);

        $this->loadViewsFrom(
            __DIR__.'/../resources/views', 'telescope'
        );
    }

    // Code to be ignored in this article

}

TelescopeServiceProvider ရဲ့ boot method ထဲမှာဆိုရင် commands၊ publishables တွေကို အရင် register လုပ်ပါတယ်။ ပြီးတော့ telescope ကို Enable လုပ်ထားလားစစ်ပါတယ်။ မလုပ်ထားဘူးဆိုရင် telescope နဲ့ပါတ်သတ်တာတွေကို ရပ်လိုက်မှာဖြစ်ပြီးတော့ လုပ်ထားတယ် ဆိုရင် telescope နဲ့ပါတ်သတ်တာတွေကို ဆက်လုပ်မှာပါ။ ဒီ option ကိုတော့ Telescope config ကနေ control လုပ်နိုင်မှာပါ။ ကျန်တာတွေကတော့ routes၊ migrations၊ views တွေကိုဆက်ပြီးတော့ register လုပ်ပါတယ်။ ဒီနေရာမှာ အသားပေးချင်တာကတော့ Telescope::start($this->app) ပဲဖြစ်ပါတယ်။

Telescope.php

<?php

namespace Laravel\Telescope;

use Closure;
use Exception;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Support\Testing\Fakes\EventFake;
use Laravel\Telescope\Contracts\EntriesRepository;
use Laravel\Telescope\Contracts\TerminableRepository;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Throwable;

class Telescope
{
    use AuthorizesRequests,
        ExtractsMailableTags,
        ListensForStorageOpportunities,
        RegistersWatchers;


    // Code to be ignored in this article

    public static function start($app)
    {
        if (! config('telescope.enabled')) {
            return;
        }

        static::registerWatchers($app);

        static::registerMailableTagExtractor();

        if (! static::runningWithinOctane($app) &&
            (static::runningApprovedArtisanCommand($app) ||
            static::handlingApprovedRequest($app))
        ) {
            static::startRecording($loadMonitoredTags = false);
        }

    // Code to be ignored in this article
    }

start method မှာဆိုရင် ထပ်ပြီးတော့ telescope ကို enable လုပ်ထားလားဆိုတာစစ်ပါတယ်။ အဲ့နောက်မှာတော့ အပေါ်မှာပြောထားတဲ့ watchers တွေကို စပိးတော့ register လုပ်ပါတယ်။ အဲ့တာပြီးရင် telescope entries လို့ခေါ်တဲ့ records တွေကို စသိမ်းသင့်မသိမ်းသင့်ဆိုတာဆုံးဖြတ်ပါတယ်။ ဒီနေရာမှာစိတ်ဝင်စားဖို့ကောင်းတာက အဲ့ဆုံးဖြတ်တဲ့ working flow ဖြစ်ပါတယ်။

တစ်ကယ်လို့ Laravel Octane ကိုမသုံးထားရင် (and) approved artisan command (or) approved request တွေဆိုရင်သိမ်းမယ်လို့ဆုံးဖြတ်ထားပါတယ်။

    protected static function runningApprovedArtisanCommand($app)
    {
        return $app->runningInConsole() && ! in_array(
            $_SERVER['argv'][1] ?? null,
            array_merge([
                // 'migrate',
                'migrate:rollback',
                'migrate:fresh',
                // 'migrate:refresh',
                'migrate:reset',
                'migrate:install',
                'package:discover',
                'queue:listen',
                'queue:work',
                'horizon',
                'horizon:work',
                'horizon:supervisor',
            ], config('telescope.ignoreCommands', []), config('telescope.ignore_commands', []))
        );
    }

App ကိုလည်း console ကနေ run နေတယ်၊ Telescope config ရဲ့ ignore_commands list ထဲမှာလက်ရှိ run နေတဲ့ command ကမပါဘူး ဆိုရင် approvedArtisanCommand (သိမ်းသင့်တဲ့ command) လို့သတ်မှတ်ပါတယ်။

    protected static function handlingApprovedRequest($app)
    {
        if ($app->runningInConsole()) {
            return false;
        }

        return static::requestIsToApprovedDomain($app['request']) &&
            static::requestIsToApprovedUri($app['request']);
    }

    protected static function requestIsToApprovedDomain($request): bool
    {
        return is_null(config('telescope.domain')) ||
            config('telescope.domain') !== $request->getHost();
    }

    protected static function requestIsToApprovedUri($request)
    {
        if (! empty($only = config('telescope.only_paths', []))) {
            return $request->is($only);
        }

        return ! $request->is(
            collect([
                'telescope-api*',
                'vendor/telescope*',
                (config('horizon.path') ?? 'horizon').'*',
                'vendor/horizon*',
            ])
            ->merge(config('telescope.ignore_paths', []))
            ->unless(is_null(config('telescope.path')), function ($paths) {
                return $paths->prepend(config('telescope.path').'*');
            })
            ->all()
        );
    }

approved request ဟုတ်မဟုတ်ကိုဆုံးဖြတ်တဲ့နေရာမှာ Telescope Domain ကနေဝင်လာတဲ့ request ဟုတ်မဟုတ်ကို စစ်ပါတယ်။ နောက်တစ်ခုကတော့ ​Application Logic နဲ့မဆိုင်တဲ့ Tools (Telescope, Horizon) စတာတွေကို request လုပ်တာလားဆိုတာကိုစစ်ပါတယ်။ သဘောကတော့ Application Logic နဲ့မဆိုင်တာတွေဝင်လာရင်််entries တွေကိုမသိမ်းတော့ပဲ ကျော်လိုက်မယ်လို့ဆိုလိုတာဖြစ်ပါတယ်။ မဟုတ်ရင် မလိုတဲ့ entries တွေကို database ထဲသိမ်းမိပြီးတော့ အသုံးမဝင်ဖြစ်မှာစိုးလို့ဖြစ်ပါတယ်။ သေချာတည်ဆောက်ထားတာကိုဒီနေရာမှာမြင်တွေ့ရပါတယ်။

ဘာတွေသိမ်းမယ်၊ ဘယ်လိုအလုပ်လုပ်မယ်ဆိုတာသိပြီဆိုတော့ အခု registerWatchers ကိုပြန်သွားရအောင်။

    protected static function registerWatchers($app)
    {
        foreach (config('telescope.watchers') as $key => $watcher) {
            if (is_string($key) && $watcher === false) {
                continue;
            }

            if (is_array($watcher) && ! ($watcher['enabled'] ?? true)) {
                continue;
            }

            $watcher = $app->make(is_string($key) ? $key : $watcher, [
                'options' => is_array($watcher) ? $watcher : [],
            ]);

            static::$watchers[] = get_class($watcher);

            $watcher->register($app);
        }
    }

Telescope config ရဲ့ watchers စာရင်းကိုဖတ်ပါတယ်။ ပြီးတော့ enable လုပ်ထားတာတွေကိုပဲယူပြီးတော့ သက်ဆိုင်ရာ watcher ရဲ့ register method ကိုလှမ်းခေါ်ပေးပါတယ်။ ဒီနေရာမှာ watchers တိုင်းကိုလိုက် dive မပြတော့ပဲနဲ့ Log Watcher ကိုပဲ dive ပြပါမယ်။ ကျန်တာတွေကိုတော့ ဒီနေရာ မှာဆက်ဖတ်ကြည့်လို့ရပါတယ်။

<?php

namespace Laravel\Telescope\Watchers;

use Illuminate\Log\Events\MessageLogged;
use Illuminate\Support\Arr;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Psr\Log\LogLevel;
use Throwable;

class LogWatcher extends Watcher
{
    /**
     * The available log level priorities.
     */
    private const PRIORITIES = [
        LogLevel::DEBUG => 100,
        LogLevel::INFO => 200,
        LogLevel::NOTICE => 250,
        LogLevel::WARNING => 300,
        LogLevel::ERROR => 400,
        LogLevel::CRITICAL => 500,
        LogLevel::ALERT => 550,
        LogLevel::EMERGENCY => 600,
    ];

    /**
     * Register the watcher.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function register($app)
    {
        $app['events']->listen(MessageLogged::class, [$this, 'recordLog']);
    }

    /**
     * Record a message was logged.
     *
     * @param  \Illuminate\Log\Events\MessageLogged  $event
     * @return void
     */
    public function recordLog(MessageLogged $event)
    {
        if (! Telescope::isRecording() || $this->shouldIgnore($event)) {
            return;
        }

        Telescope::recordLog(
            IncomingEntry::make([
                'level' => $event->level,
                'message' => (string) $event->message,
                'context' => Arr::except($event->context, ['telescope']),
            ])->tags($this->tags($event))
        );
    }

    /**
     * Extract tags from the given event.
     *
     * @param  \Illuminate\Log\Events\MessageLogged  $event
     * @return array
     */
    private function tags($event)
    {
        return $event->context['telescope'] ?? [];
    }

    /**
     * Determine if the event should be ignored.
     *
     * @param  mixed  $event
     * @return bool
     */
    private function shouldIgnore($event)
    {
        if (isset($event->context['exception']) && $event->context['exception'] instanceof Throwable) {
            return true;
        }

        $minimumTelescopeLogLevel = static::PRIORITIES[$this->options['level'] ?? 'debug']
                ?? static::PRIORITIES[LogLevel::DEBUG];

        $eventLogLevel = static::PRIORITIES[$event->level]
                ?? static::PRIORITIES[LogLevel::DEBUG];

        return $eventLogLevel < $minimumTelescopeLogLevel;
    }
}

registerWatchers ကနေတစ်ဆင့် watcher တွေရဲ့ register method ကိုလှမ်းခေါ်တယ်ဆိုတာကို အရှေ့မှာပြပြီးဖြစ်ပါတယ်။ အခု အဲ့ register ကနေဆက်ကြည့်ရအောင်။ Laravel မှာ Log ထုတ်ရင် App ထဲကနေ Illuminate\Log\Events\MessageLogged::class ဆိုတဲ့ event ကို fire/dispatch ပေးပါတယ်။ အဲ့ event ကို register method မှာ listen လုပ်နေပြီး အဲ့ event လာတာနဲ့ recordLog ဆိုတဲ့ method ကိုခေါ်ပေးပါတယ်။ ဒီနေရာမှာ Event-Driven Architecture ဆိုတာကိုပြန်မြင်ဖို့လိုလာပါတယ်။ Telescope package ရဲ့ watchers တိုင်းက အဲ့ architecture ကိုသုံးထားတယ်ဆိုတာ watcher တိုင်းရဲ့ register method ကိုလိုက်ကြည့်နိုင်ပါတယ်။

recordLog မှာတော့ Telescope က recording လုပ်နေတယ်ဆိုတာရယ်၊ event က ignore မလုပ်သင့်ဘူးဆိုတာရယ်သေချာရင် database ထဲကို စမှတ်ပါတယ်။ ပြီးရင်တော့ frontend မှာ telescope dashboard ဆောက်ပြီးတော့ လိုအပ်သလို data ကိုပြလိုက်တာပဲဖြစ်ပါတယ်။

Extending a custom watcher

Watcher တစ်ခုရဲ့အလုပ်လုပ်ပုံကိုသိပြီဆိုတော့ တစ်ကယ်လို့သူ့မှာမပါတဲ့ watcher ကို ကိုယ်တိုင် implement လုပ်ချင်ရင်လည်းလွယ်သွားပါပြီ။ Custom Watcher File တစ်ခုဆောက်ပြီး Laravel\Telescope\Watchers\Watcher ကို extends လုပ်ပြီးတော့ ကိုယ်လိုချင်တဲ့ event ကို register method မှာဖမ်း၊ Telescope config ကို publish လုပ်ပြီးတော့ Custom Watcher File ကို telescope ရဲ့ watchers section မှာ register ပြီးတော့ enable လုပ်ပေးလိုက်တာနဲ့ telescope က handle လုပ်ပေးသွားမှာပါ။ ဒီနေရာမှာ ကိုယ့် custom watcher အတွက်လိုအပ်တဲ့ dashboard component (view) ကိုတော့ ကိုယ်တိုင်တည်ဆောက်ပြီး api endpoints အနည်းငယ်ကိုလည်း ထုတ်ပေးဖို့လိုပါလိမ့်မယ်။ Laravel က ထုတ်ပေးတဲ့ event list ကို php artisan event:list ဆိုတာကို run ပြီးတော့ ကြည့်လို့ရပါတယ်။ ကိုယ်တိုင် custom event fire/dispatch လုပ်ပြီး ပြန် listen လုပ်ချင်တယ်ဆိုလည်းရပါတယ်။

Extra

Larave Telescope ကို production မှာသုံးမယ်ဆိုရင်တော့ တစ်ခုသတိထားဖို့လိုပါတယ်။ သူက ​entries တွေကို database ထဲမှာသိမ်းတာမလို့ ကြာလာတာနဲ့ အမျှ database size ကြီးလာပါလိမ့်မယ်။ အဲ့အတွက် prune command ကို built-in ထောက်ပံ့ပေးထားပါတယ်။ ကိုယ့် Application ရဲ့ requirements ပေါ်မူတည်ပြီးလိုအပ်သလို configure ဖို့လိုအပ်ပါလိမ့်မယ်။

Complete Event List

ဟိုးအရင် Laravel version အဟောင်းတွေမှာတော့ Framework + Plugins တွေက fire လုပ်ပေးတဲ့ event စာရင်းကို ပြဖို့ command ပေးမထားပေမယ့် နောက်ပိုင်း version တွေမှာတော့ Laravel က command တစ်ခုကိုထုတ်ပေးထားပါတယ်။

php artisan event:list

Conclusion

Telescope ရဲ့အလုပ်လုပ်ပုံကို code ကော logic ပါသိပြီဆိုတော့ လိုအပ်သလို အသုံးချ၊ ပြုပြင်နိုင်မယ်လို့ထင်ပါတယ်။ တစ်ခြား package တွေ framework တွေကိုလည်း ကိုယ်တိုင် dive ပြီး အသင့်တော်ဆုံးကိုရွေးနိုင်၊ ပြုပြင်အသုံးချနိုင်မယ်လို့မျှော်လင့်ပါတယ်။

Nay Thu Khant

Solution Architect @ onenex.co