簡單兩步就能將 Laravel Log 資訊發到其他平臺上

coding01發表於2018-05-13

我們在寫程式碼時,都想自己的程式碼儘可能的不影響現有的程式碼。

或者說,最大化不改動任何程式碼的情況下,如何嵌入我們的新功能?這是我們常說的「非侵入式」的開發方式。

使用「非侵入式」的開發模式,主要在提供第三方外掛和功能中最為常見。今天藉助「Rollbar」第三方工具來說說如何做到「非侵入式」開發。

本文主要能學到:

  1. Laravel Event / Listener 原理;
  2. Rollbar for Laravel 的使用
  3. 建立一個 Log to Dingding 群的功能

Laravel Event / Listener 原理

在 Laravel,主要利用 EventServiceProvider 來載入 Events / Listeners:

<?php

namespace Illuminate\Events;

use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;

class EventServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('events', function ($app) {
            return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
                return $app->make(QueueFactoryContract::class);
            });
        });
    }
}
複製程式碼

EventServiceProvider 返回的是 Dispatcher 物件。我們看看 Dispatcher 類:

<?php

namespace Illuminate\Events;

use Exception;
use ReflectionClass;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Container\Container;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastFactory;
use Illuminate\Contracts\Container\Container as ContainerContract;

class Dispatcher implements DispatcherContract
{
    /**
     * The IoC container instance.
     *
     * @var \Illuminate\Contracts\Container\Container
     */
    protected $container;

    /**
     * The registered event listeners.
     *
     * @var array
     */
    protected $listeners = [];

    /**
     * The wildcard listeners.
     *
     * @var array
     */
    protected $wildcards = [];

    /**
     * The queue resolver instance.
     *
     * @var callable
     */
    protected $queueResolver;

    /**
     * Create a new event dispatcher instance.
     *
     * @param  \Illuminate\Contracts\Container\Container|null  $container
     * @return void
     */
    public function __construct(ContainerContract $container = null)
    {
        $this->container = $container ?: new Container;
    }

    /**
     * Register an event listener with the dispatcher.
     *
     * @param  string|array  $events
     * @param  mixed  $listener
     * @return void
     */
    public function listen($events, $listener)
    {
        foreach ((array) $events as $event) {
            if (Str::contains($event, '*')) {
                $this->setupWildcardListen($event, $listener);
            } else {
                $this->listeners[$event][] = $this->makeListener($listener);
            }
        }
    }

...

}
複製程式碼

主要作用是繫結 EventsListeners,當 Events觸發時,直接執行 Listeners

我們希望 log 除了在本地檔案儲存輸出外,也想把 log 資訊實時發到其他平臺和渠道上,這時候我們就需要藉助 LogServiceProviderevents / listeners繫結實現了。現在來看看 LogServiceProvider:

<?php

namespace Illuminate\Log;

use Monolog\Logger as Monolog;
use Illuminate\Support\ServiceProvider;

class LogServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('log', function () {
            return $this->createLogger();
        });
    }

    /**
     * Create the logger.
     *
     * @return \Illuminate\Log\Writer
     */
    public function createLogger()
    {
        $log = new Writer(
            new Monolog($this->channel()), $this->app['events']
        );

        if ($this->app->hasMonologConfigurator()) {
            call_user_func($this->app->getMonologConfigurator(), $log->getMonolog());
        } else {
            $this->configureHandler($log);
        }

        return $log;
    }

   ...
}
複製程式碼

這裡將 $this->app['events'] 也就是 Dispatcher 傳入,使用者事件的註冊:

    /**
     * Register a new callback handler for when a log event is triggered.
     *
     * @param  \Closure  $callback
     * @return void
     *
     * @throws \RuntimeException
     */
    public function listen(Closure $callback)
    {
        if (! isset($this->dispatcher)) {
            throw new RuntimeException('Events dispatcher has not been set.');
        }

        $this->dispatcher->listen(MessageLogged::class, $callback);
    }
複製程式碼

有了 ServiceProviderlisten 就可以做到「非入侵」開發了。

Rollbar

Rollbar error monitoring integration for Laravel projects. This library adds a listener to Laravel's logging component. Laravel's session information will be sent in to Rollbar, as well as some other helpful information such as 'environment', 'server', and 'session'.

參考:docs.rollbar.com/docs/larave…

簡單使用

使用該工具,只要在其官網註冊賬號,併產生一個 access token 即可

安裝該工具,也只需要簡單的兩步:

composer require rollbar/rollbar-laravel

// .env
ROLLBAR_TOKEN=[your Rollbar project access token]

// 如果 < Laravel 5.5,則需要在 app.php 中新增
Rollbar\Laravel\RollbarServiceProvider::class,
複製程式碼

測試,只要有 Log 輸出,rollbar 後臺都可以收到資訊,方便檢視,而再也不需要去看 log 檔案了。

簡單兩步就能將 Laravel Log 資訊發到其他平臺上

剖析實現原理

我們來看看 rollbar 是不是我們所設想的那樣實現的?

簡單兩步就能將 Laravel Log 資訊發到其他平臺上

我們先看看 RollbarServiceProvider

<?php namespace Rollbar\Laravel;

use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;
use Rollbar\Rollbar;
use Rollbar\Laravel\RollbarLogHandler;

class RollbarServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = false;

    /**
     * Bootstrap the application events.
     */
    public function boot()
    {
        // Don't boot rollbar if it is not configured.
        if ($this->stop() === true) {
            return;
        }

        $app = $this->app;

        // Listen to log messages.
        $app['log']->listen(function () use ($app) {
            $args = func_get_args();

            // Laravel 5.4 returns a MessageLogged instance only
            if (count($args) == 1) {
                $level = $args[0]->level;
                $message = $args[0]->message;
                $context = $args[0]->context;
            } else {
                $level = $args[0];
                $message = $args[1];
                $context = $args[2];
            }

            $app['Rollbar\Laravel\RollbarLogHandler']->log($level, $message, $context);
        });
    }

    /**
     * Register the service provider.
     */
    public function register()
    {
        // Don't register rollbar if it is not configured.
        if ($this->stop() === true) {
            return;
        }

        $app = $this->app;

        $this->app->singleton('Rollbar\RollbarLogger', function ($app) {

            $defaults = [
                'environment'       => $app->environment(),
                'root'              => base_path(),
                'handle_exception'  => true,
                'handle_error'      => true,
                'handle_fatal'      => true,
            ];
            $config = array_merge($defaults, $app['config']->get('services.rollbar', []));
            $config['access_token'] = getenv('ROLLBAR_TOKEN') ?: $app['config']->get('services.rollbar.access_token');

            if (empty($config['access_token'])) {
                throw new InvalidArgumentException('Rollbar access token not configured');
            }

            $handleException = (bool) array_pull($config, 'handle_exception');
            $handleError = (bool) array_pull($config, 'handle_error');
            $handleFatal = (bool) array_pull($config, 'handle_fatal');

            Rollbar::init($config, $handleException, $handleError, $handleFatal);

            return Rollbar::logger();
        });

        $this->app->singleton('Rollbar\Laravel\RollbarLogHandler', function ($app) {

            $level = getenv('ROLLBAR_LEVEL') ?: $app['config']->get('services.rollbar.level', 'debug');

            return new RollbarLogHandler($app['Rollbar\RollbarLogger'], $app, $level);
        });
    }

    /**
     * Check if we should prevent the service from registering
     *
     * @return boolean
     */
    public function stop()
    {
        $level = getenv('ROLLBAR_LEVEL') ?: $this->app->config->get('services.rollbar.level', null);
        $token = getenv('ROLLBAR_TOKEN') ?: $this->app->config->get('services.rollbar.access_token', null);
        $hasToken = empty($token) === false;

        return $hasToken === false || $level === 'none';
    }
}
複製程式碼

這個比較好理解,先利用 register 註冊兩個 singleton,然後在 boot 方法中,註冊 listener

    $app['log']->listen(function () use ($app){});
複製程式碼

其中 $app['log'],就是我們的上文說的 LogServiceProvider,將 listener 註冊到 EventServiceProvider 中。

$this->dispatcher->listen(MessageLogged::class, $callback);
複製程式碼

最後我們看看 Rollbar facades 返回的是:RollbarLogHandler 物件

<?php namespace Rollbar\Laravel\Facades;

use Illuminate\Support\Facades\Facade;

class Rollbar extends Facade
{
    /**
     * Get a schema builder instance for the default connection.
     *
     * @return \Rollbar\Laravel\RollbarLogHandler
     */
    protected static function getFacadeAccessor()
    {
        return 'Rollbar\Laravel\RollbarLogHandler';
    }
}

複製程式碼

看看 RollbarLogHandler 實現,也主要是將 log 資訊反饋到Rollbar 中,此處不做分析了。

模擬實現

通過對 Rollbar 簡單的分析,就會發現原來通過簡單 Listener,不用改現在的任何功能和程式碼,就能實現將 log 實時發到你想接收的地方。

所以我們可以嘗試也寫一個這樣的功能,將 log 資訊發到釘釘上。

好了,我們開始寫 Log2Dingding 外掛。

根據之前的文章我們可以很方便的組織好外掛結構:

簡單兩步就能將 Laravel Log 資訊發到其他平臺上

composer.json 設定:

{
    "name": "fanly/log2dingding",
    "description": "Laravel Log to DingDing",
    "license": "MIT",
    "authors": [
        {
            "name": "fanly",
            "email": "yemeishu@126.com"
        }
    ],
    "require": {},
    "extra": {
        "laravel": {
            "providers": [
                "Fanly\\Log2dingding\\FanlyLog2dingdingServiceProvider"
            ]
        }
    },
    "autoload": {
        "psr-4": {
            "Fanly\\Log2dingding\\": "src/"
        }
    }
}

複製程式碼

我們定義 ServiceProvider:

<?php
/**
 * User: yemeishu
 * Date: 2018/5/13
 * Time: 下午2:56
 */
namespace Fanly\Log2dingding;

use Fanly\Log2dingding\Dingtalk\Messager;
use Illuminate\Support\ServiceProvider;
use Fanly\Log2dingding\Support\Client;

class FanlyLog2dingdingServiceProvider extends ServiceProvider {

    protected function registerFacade()
    {
        // Don't register rollbar if it is not configured.
        if ($this->stop() === true) {
            return;
        }

        $this->app->singleton('fanlylog2dd', function ($app) {
            $config['access_token'] = getenv('FANLYLOG_TOKEN') ?: $app['config']->get('services.fanly.log2dd.access_token');

            if (empty($config['access_token'])) {
                throw new InvalidArgumentException('log2dd access token not configured');
            }

            return (new Messager(new Client()))->accessToken($config['access_token']);
        });
    }

    /**
     * Bootstrap the application services.
     */
    public function boot()
    {
        // Don't boot rollbar if it is not configured.
        if ($this->stop() === true) {
            return;
        }

        $app = $this->app;

        // Listen to log messages.
        $app['log']->listen(function () use ($app) {
            $args = func_get_args();

            // Laravel 5.4 returns a MessageLogged instance only
            if (count($args) == 1) {
                $level = $args[0]->level;
                $message = $args[0]->message;
                $context = $args[0]->context;
            } else {
                $level = $args[0];
                $message = $args[1];
                $context = $args[2];
            }

            $app['fanlylog2dd']->message("[ $level ] $message\n".implode($context))->send();
        });

    }

    /**
     * Register the application services.
     */
    public function register()
    {
        $this->registerFacade();
    }

    private function stop()
    {
        $level = getenv('FANLYLOG_LEVEL') ?: $this->app->config->get('services.rollbar.level', null);
        $token = getenv('FANLYLOG_TOKEN') ?: $this->app->config->get('services.rollbar.access_token', null);
        $hasToken = empty($token) === false;

        return $hasToken === false || $level === 'none';
    }
}
複製程式碼

我們主要是建立一個發釘釘訊息的單例,然後再註冊 listener,只要獲取 log 資訊,就傳送資訊到釘釘上。

測試一下:

簡單兩步就能將 Laravel Log 資訊發到其他平臺上

總結

最後做成外掛,和 Rollbar 一樣,引入:

composer require "fanly/log2dingding"

// .env
FANLYLOG_TOKEN=56331868f7056a3e645e7dba034c5550e7af***
複製程式碼

同樣的,其他資訊都不需要設定,跑一個測試:

簡單兩步就能將 Laravel Log 資訊發到其他平臺上

Laravel 框架的一大好處在於,可以以友好的方式實現我們「非入侵」開發,只要藉助「ServiceProvider」和「Events/Listner」,就可以擴充套件我們的功能。

參考

相關文章