{"id":36317240,"url":"https://github.com/fanly/log2dingding","last_synced_at":"2026-01-11T11:04:08.536Z","repository":{"id":56981514,"uuid":"133218070","full_name":"fanly/log2dingding","owner":"fanly","description":null,"archived":false,"fork":false,"pushed_at":"2018-05-13T09:45:13.000Z","size":24,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-27T02:50:36.875Z","etag":null,"topics":["dingding","laravel","log"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fanly.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-05-13T08:15:34.000Z","updated_at":"2020-04-05T12:54:57.000Z","dependencies_parsed_at":"2022-08-21T11:20:38.095Z","dependency_job_id":null,"html_url":"https://github.com/fanly/log2dingding","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/fanly/log2dingding","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fanly%2Flog2dingding","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fanly%2Flog2dingding/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fanly%2Flog2dingding/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fanly%2Flog2dingding/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fanly","download_url":"https://codeload.github.com/fanly/log2dingding/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fanly%2Flog2dingding/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28301422,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-11T08:21:30.231Z","status":"ssl_error","status_checked_at":"2026-01-11T08:21:26.882Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dingding","laravel","log"],"created_at":"2026-01-11T11:04:07.932Z","updated_at":"2026-01-11T11:04:08.530Z","avatar_url":"https://github.com/fanly.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"我们在写代码时，都想自己的代码尽可能的不影响现有的代码。\n\n或者说，最大化不改动任何代码的情况下，如何嵌入我们的新功能？这是我们常说的「非侵入式」的开发方式。\n\n使用「非侵入式」的开发模式，主要在提供第三方插件和功能中最为常见。今天借助「Rollbar」第三方工具来说说如何做到「非侵入式」开发。\n\n本文主要能学到:\n\n\u003e 1. Laravel Event / Listener 原理；\n\u003e 2. Rollbar for Laravel 的使用\n\u003e 3. 创建一个 Log to Dingding 群的功能\n\n## Laravel Event / Listener 原理\n\n在 Laravel，主要利用 `EventServiceProvider` 来加载 `Events / Listeners`:\n\n```php\n\u003c?php\n\nnamespace Illuminate\\Events;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Queue\\Factory as QueueFactoryContract;\n\nclass EventServiceProvider extends ServiceProvider\n{\n    /**\n     * Register the service provider.\n     *\n     * @return void\n     */\n    public function register()\n    {\n        $this-\u003eapp-\u003esingleton('events', function ($app) {\n            return (new Dispatcher($app))-\u003esetQueueResolver(function () use ($app) {\n                return $app-\u003emake(QueueFactoryContract::class);\n            });\n        });\n    }\n}\n```\n\n`EventServiceProvider` 返回的是 `Dispatcher` 对象。我们看看 `Dispatcher` 类：\n\n```php\n\u003c?php\n\nnamespace Illuminate\\Events;\n\nuse Exception;\nuse ReflectionClass;\nuse Illuminate\\Support\\Arr;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Container\\Container;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast;\nuse Illuminate\\Contracts\\Events\\Dispatcher as DispatcherContract;\nuse Illuminate\\Contracts\\Broadcasting\\Factory as BroadcastFactory;\nuse Illuminate\\Contracts\\Container\\Container as ContainerContract;\n\nclass Dispatcher implements DispatcherContract\n{\n    /**\n     * The IoC container instance.\n     *\n     * @var \\Illuminate\\Contracts\\Container\\Container\n     */\n    protected $container;\n\n    /**\n     * The registered event listeners.\n     *\n     * @var array\n     */\n    protected $listeners = [];\n\n    /**\n     * The wildcard listeners.\n     *\n     * @var array\n     */\n    protected $wildcards = [];\n\n    /**\n     * The queue resolver instance.\n     *\n     * @var callable\n     */\n    protected $queueResolver;\n\n    /**\n     * Create a new event dispatcher instance.\n     *\n     * @param  \\Illuminate\\Contracts\\Container\\Container|null  $container\n     * @return void\n     */\n    public function __construct(ContainerContract $container = null)\n    {\n        $this-\u003econtainer = $container ?: new Container;\n    }\n\n    /**\n     * Register an event listener with the dispatcher.\n     *\n     * @param  string|array  $events\n     * @param  mixed  $listener\n     * @return void\n     */\n    public function listen($events, $listener)\n    {\n        foreach ((array) $events as $event) {\n            if (Str::contains($event, '*')) {\n                $this-\u003esetupWildcardListen($event, $listener);\n            } else {\n                $this-\u003elisteners[$event][] = $this-\u003emakeListener($listener);\n            }\n        }\n    }\n\n...\n\n}\n```\n主要作用是绑定 `Events` 和 `Listeners`，当 `Events`触发时，直接执行 `Listeners`。\n\n\n我们希望 log 除了在本地文件存储输出外，也想把 log 信息实时发到其他平台和渠道上，这时候我们就需要借助 `LogServiceProvider` 的 `events / listeners`绑定实现了。现在来看看 `LogServiceProvider`:\n\n\n```php\n\u003c?php\n\nnamespace Illuminate\\Log;\n\nuse Monolog\\Logger as Monolog;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass LogServiceProvider extends ServiceProvider\n{\n    /**\n     * Register the service provider.\n     *\n     * @return void\n     */\n    public function register()\n    {\n        $this-\u003eapp-\u003esingleton('log', function () {\n            return $this-\u003ecreateLogger();\n        });\n    }\n\n    /**\n     * Create the logger.\n     *\n     * @return \\Illuminate\\Log\\Writer\n     */\n    public function createLogger()\n    {\n        $log = new Writer(\n            new Monolog($this-\u003echannel()), $this-\u003eapp['events']\n        );\n\n        if ($this-\u003eapp-\u003ehasMonologConfigurator()) {\n            call_user_func($this-\u003eapp-\u003egetMonologConfigurator(), $log-\u003egetMonolog());\n        } else {\n            $this-\u003econfigureHandler($log);\n        }\n\n        return $log;\n    }\n\n   ...\n}\n```\n\n这里将 `$this-\u003eapp['events']` 也就是 `Dispatcher` 传入，用户事件的注册：\n\n\n```php\n    /**\n     * Register a new callback handler for when a log event is triggered.\n     *\n     * @param  \\Closure  $callback\n     * @return void\n     *\n     * @throws \\RuntimeException\n     */\n    public function listen(Closure $callback)\n    {\n        if (! isset($this-\u003edispatcher)) {\n            throw new RuntimeException('Events dispatcher has not been set.');\n        }\n\n        $this-\u003edispatcher-\u003elisten(MessageLogged::class, $callback);\n    }\n```\n\n有了 `ServiceProvider` 和 `listen` 就可以做到「非入侵」开发了。\n\n## Rollbar\n\n\u003e 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'.\n\u003e \n\u003e 参考：[https://docs.rollbar.com/docs/laravel](https://docs.rollbar.com/docs/laravel)\n\n### 简单使用\n\n使用该工具，只要在其官网注册账号，并产生一个 `access token` 即可\n\n安装该工具，也只需要简单的两步：\n\n```bash\ncomposer require rollbar/rollbar-laravel\n\n// .env\nROLLBAR_TOKEN=[your Rollbar project access token]\n\n// 如果 \u003c Laravel 5.5，则需要在 app.php 中添加\nRollbar\\Laravel\\RollbarServiceProvider::class,\n```\n\n测试，只要有 Log 输出，rollbar 后台都可以收到信息，方便查看，而再也不需要去看 log 文件了。\n\n![](http://ow20g4tgj.bkt.clouddn.com/2018-05-13-15261913480148.jpg)\n\n\n### 剖析实现原理\n\n我们来看看 rollbar 是不是我们所设想的那样实现的？\n\n![](http://ow20g4tgj.bkt.clouddn.com/2018-05-13-15261914714008.jpg)\n\n我们先看看 `RollbarServiceProvider`\n\n\n```php\n\u003c?php namespace Rollbar\\Laravel;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse InvalidArgumentException;\nuse Rollbar\\Rollbar;\nuse Rollbar\\Laravel\\RollbarLogHandler;\n\nclass RollbarServiceProvider extends ServiceProvider\n{\n    /**\n     * Indicates if loading of the provider is deferred.\n     *\n     * @var bool\n     */\n    protected $defer = false;\n\n    /**\n     * Bootstrap the application events.\n     */\n    public function boot()\n    {\n        // Don't boot rollbar if it is not configured.\n        if ($this-\u003estop() === true) {\n            return;\n        }\n\n        $app = $this-\u003eapp;\n\n        // Listen to log messages.\n        $app['log']-\u003elisten(function () use ($app) {\n            $args = func_get_args();\n\n            // Laravel 5.4 returns a MessageLogged instance only\n            if (count($args) == 1) {\n                $level = $args[0]-\u003elevel;\n                $message = $args[0]-\u003emessage;\n                $context = $args[0]-\u003econtext;\n            } else {\n                $level = $args[0];\n                $message = $args[1];\n                $context = $args[2];\n            }\n\n            $app['Rollbar\\Laravel\\RollbarLogHandler']-\u003elog($level, $message, $context);\n        });\n    }\n\n    /**\n     * Register the service provider.\n     */\n    public function register()\n    {\n        // Don't register rollbar if it is not configured.\n        if ($this-\u003estop() === true) {\n            return;\n        }\n\n        $app = $this-\u003eapp;\n\n        $this-\u003eapp-\u003esingleton('Rollbar\\RollbarLogger', function ($app) {\n\n            $defaults = [\n                'environment'       =\u003e $app-\u003eenvironment(),\n                'root'              =\u003e base_path(),\n                'handle_exception'  =\u003e true,\n                'handle_error'      =\u003e true,\n                'handle_fatal'      =\u003e true,\n            ];\n            $config = array_merge($defaults, $app['config']-\u003eget('services.rollbar', []));\n            $config['access_token'] = getenv('ROLLBAR_TOKEN') ?: $app['config']-\u003eget('services.rollbar.access_token');\n\n            if (empty($config['access_token'])) {\n                throw new InvalidArgumentException('Rollbar access token not configured');\n            }\n\n            $handleException = (bool) array_pull($config, 'handle_exception');\n            $handleError = (bool) array_pull($config, 'handle_error');\n            $handleFatal = (bool) array_pull($config, 'handle_fatal');\n\n            Rollbar::init($config, $handleException, $handleError, $handleFatal);\n\n            return Rollbar::logger();\n        });\n\n        $this-\u003eapp-\u003esingleton('Rollbar\\Laravel\\RollbarLogHandler', function ($app) {\n\n            $level = getenv('ROLLBAR_LEVEL') ?: $app['config']-\u003eget('services.rollbar.level', 'debug');\n\n            return new RollbarLogHandler($app['Rollbar\\RollbarLogger'], $app, $level);\n        });\n    }\n\n    /**\n     * Check if we should prevent the service from registering\n     *\n     * @return boolean\n     */\n    public function stop()\n    {\n        $level = getenv('ROLLBAR_LEVEL') ?: $this-\u003eapp-\u003econfig-\u003eget('services.rollbar.level', null);\n        $token = getenv('ROLLBAR_TOKEN') ?: $this-\u003eapp-\u003econfig-\u003eget('services.rollbar.access_token', null);\n        $hasToken = empty($token) === false;\n\n        return $hasToken === false || $level === 'none';\n    }\n}\n```\n\n这个比较好理解，先利用 `register` 注册两个 `singleton`，然后在 `boot` 方法中，注册 `listener`\n\n\n```php\n    $app['log']-\u003elisten(function () use ($app){});\n```\n\n其中 `$app['log']`，就是我们的上文说的 `LogServiceProvider`，将 `listener` 注册到 `EventServiceProvider` 中。\n\n```php\n$this-\u003edispatcher-\u003elisten(MessageLogged::class, $callback);\n```\n\n最后我们看看 `Rollbar` facades 返回的是：`RollbarLogHandler` 对象\n\n```php\n\u003c?php namespace Rollbar\\Laravel\\Facades;\n\nuse Illuminate\\Support\\Facades\\Facade;\n\nclass Rollbar extends Facade\n{\n    /**\n     * Get a schema builder instance for the default connection.\n     *\n     * @return \\Rollbar\\Laravel\\RollbarLogHandler\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'Rollbar\\Laravel\\RollbarLogHandler';\n    }\n}\n\n```\n\n看看 `RollbarLogHandler` 实现，也主要是将 log 信息反馈到Rollbar 中，此处不做分析了。\n\n## 模拟实现\n\n通过对 `Rollbar` 简单的分析，就会发现原来通过简单 `Listener`，不用改现在的任何功能和代码，就能实现将 log 实时发到你想接收的地方。\n\n所以我们可以尝试也写一个这样的功能，将 log 信息发到钉钉上。\n\n好了，我们开始写 `Log2Dingding` 插件。\n\n根据之前的文章我们可以很方便的组织好插件结构:\n\n![](http://ow20g4tgj.bkt.clouddn.com/2018-05-13-15261990158468.jpg)\n\n`composer.json` 设置:\n\n```php\n{\n    \"name\": \"fanly/log2dingding\",\n    \"description\": \"Laravel Log to DingDing\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"fanly\",\n            \"email\": \"yemeishu@126.com\"\n        }\n    ],\n    \"require\": {},\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Fanly\\\\Log2dingding\\\\FanlyLog2dingdingServiceProvider\"\n            ]\n        }\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Fanly\\\\Log2dingding\\\\\": \"src/\"\n        }\n    }\n}\n\n```\n\n我们定义 `ServiceProvider`:\n\n```php\n\u003c?php\n/**\n * User: yemeishu\n * Date: 2018/5/13\n * Time: 下午2:56\n */\nnamespace Fanly\\Log2dingding;\n\nuse Fanly\\Log2dingding\\Dingtalk\\Messager;\nuse Illuminate\\Support\\ServiceProvider;\nuse Fanly\\Log2dingding\\Support\\Client;\n\nclass FanlyLog2dingdingServiceProvider extends ServiceProvider {\n\n    protected function registerFacade()\n    {\n        // Don't register rollbar if it is not configured.\n        if ($this-\u003estop() === true) {\n            return;\n        }\n\n        $this-\u003eapp-\u003esingleton('fanlylog2dd', function ($app) {\n            $config['access_token'] = getenv('FANLYLOG_TOKEN') ?: $app['config']-\u003eget('services.fanly.log2dd.access_token');\n\n            if (empty($config['access_token'])) {\n                throw new InvalidArgumentException('log2dd access token not configured');\n            }\n\n            return (new Messager(new Client()))-\u003eaccessToken($config['access_token']);\n        });\n    }\n\n    /**\n     * Bootstrap the application services.\n     */\n    public function boot()\n    {\n        // Don't boot rollbar if it is not configured.\n        if ($this-\u003estop() === true) {\n            return;\n        }\n\n        $app = $this-\u003eapp;\n\n        // Listen to log messages.\n        $app['log']-\u003elisten(function () use ($app) {\n            $args = func_get_args();\n\n            // Laravel 5.4 returns a MessageLogged instance only\n            if (count($args) == 1) {\n                $level = $args[0]-\u003elevel;\n                $message = $args[0]-\u003emessage;\n                $context = $args[0]-\u003econtext;\n            } else {\n                $level = $args[0];\n                $message = $args[1];\n                $context = $args[2];\n            }\n\n            $app['fanlylog2dd']-\u003emessage(\"[ $level ] $message\\n\".implode($context))-\u003esend();\n        });\n\n    }\n\n    /**\n     * Register the application services.\n     */\n    public function register()\n    {\n        $this-\u003eregisterFacade();\n    }\n\n    private function stop()\n    {\n        $level = getenv('FANLYLOG_LEVEL') ?: $this-\u003eapp-\u003econfig-\u003eget('services.rollbar.level', null);\n        $token = getenv('FANLYLOG_TOKEN') ?: $this-\u003eapp-\u003econfig-\u003eget('services.rollbar.access_token', null);\n        $hasToken = empty($token) === false;\n\n        return $hasToken === false || $level === 'none';\n    }\n}\n```\n\n我们主要是创建一个发钉钉消息的单例，然后再注册 `listener`，只要获取 log 信息，就发送信息到钉钉上。\n\n测试一下：\n\n![](http://ow20g4tgj.bkt.clouddn.com/2018-05-13-15261992511566.jpg)\n\n\n## 总结\n\n最后做成插件，和 `Rollbar` 一样，引入：\n\n```bash\ncomposer require \"fanly/log2dingding\"\n\n// .env\nFANLYLOG_TOKEN=56331868f7056a3e645e7dba034c5550e7af***\n```\n\n同样的，其他信息都不需要设置，跑一个测试：\n\n![](http://ow20g4tgj.bkt.clouddn.com/2018-05-13-15262016735782.jpg)\n\nLaravel 框架的一大好处在于，可以以友好的方式实现我们「非入侵」开发，只要借助「`ServiceProvider`」和「`Events/Listner`」，就可以扩展我们的功能。\n\n*参考*\n\n* 「12步」制作 Laravel 插件 (一)[https://mp.weixin.qq.com/s/AD05BiKjPsI2ehC-mhQJQw](https://mp.weixin.qq.com/s/AD05BiKjPsI2ehC-mhQJQw)\n* 「3步」发布 Laravel 插件 (二)[https://mp.weixin.qq.com/s/RSYeHU7aR4gyJyLNwdjbJg](https://mp.weixin.qq.com/s/RSYeHU7aR4gyJyLNwdjbJg)\n* fanly/log2dingding [https://packagist.org/packages/fanly/log2dingding](https://packagist.org/packages/fanly/log2dingding)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffanly%2Flog2dingding","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffanly%2Flog2dingding","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffanly%2Flog2dingding/lists"}