簡單兩步就能將 Laravel Log 信息發到其餘平臺上

咱們在寫代碼時,都想本身的代碼儘量的不影響現有的代碼。php

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

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

本文主要能學到:bash

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

Laravel Event / Listener 原理

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

<?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 類:app

<?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觸發時,直接執行 Listenerscomposer

咱們但願 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 傳入,用戶事件的註冊:ide

/** * 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 文件了。

剖析實現原理

咱們來看看 rollbar 是否是咱們所設想的那樣實現的?

咱們先看看 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 插件。

根據以前的文章咱們能夠很方便的組織好插件結構:

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 信息,就發送信息到釘釘上。

測試一下:

總結

最後作成插件,和 Rollbar 同樣,引入:

composer require "fanly/log2dingding"

// .env
FANLYLOG_TOKEN=56331868f7056a3e645e7dba034c5550e7af***
複製代碼

一樣的,其餘信息都不須要設置,跑一個測試:

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

參考

相關文章
相關標籤/搜索