作者 竞泽

init

  1 +/vendor/
  2 +.idea
  3 +composer.lock
  1 +{
  2 + "name": "lackoxygen/exception-push",
  3 + "type": "library",
  4 + "description": "异常推送",
  5 + "autoload": {
  6 + "psr-4": {
  7 + "Lackoxygen\\ExceptionPush\\": "src/"
  8 + }
  9 + },
  10 + "authors": [
  11 + {
  12 + "name": "ojz",
  13 + "email": "jingzeou@outlook.com"
  14 + }
  15 + ],
  16 + "autoload-dev": {
  17 + "psr-4": {
  18 + "Lackoxygen\\ExceptionPush\\Tests\\": "tests/"
  19 + }
  20 + },
  21 + "require": {
  22 + "php": ">=7.4",
  23 + "wujunze/dingtalk-exception": "^2.2"
  24 + },
  25 + "require-dev": {
  26 + "phpunit/phpunit": "^9.5.10"
  27 + },
  28 + "extra": {
  29 + "laravel": {
  30 + "providers": [
  31 + "Lackoxygen\\ExceptionPush\\ExceptionPushProvider"
  32 + ]
  33 + }
  34 + }
  35 +}
  1 +<?php
  2 +
  3 +use Lackoxygen\ExceptionPush\Agents\{Ding, Wx};
  4 +
  5 +return [
  6 + 'agents' => [
  7 + Wx::class => [
  8 + 'key' => '', 'enable' => false
  9 + ], Ding::class => [
  10 + 'token' => '', 'secret' => '', 'enable' => false
  11 + ]
  12 + ],
  13 +
  14 + 'client' => [
  15 + 'timeout' => 30.00,
  16 + ],
  17 +
  18 + 'callbacks' => [
  19 + 'formatter' => function (\Lackoxygen\ExceptionPush\Attribute\Context $context) { },
  20 + 'dispatcher' => function () { }
  21 + ]
  22 +];
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush\Agents;
  4 +
  5 +use DingNotice\DingTalkService;
  6 +use Lackoxygen\ExceptionPush\Attribute\Attribute;
  7 +use Lackoxygen\ExceptionPush\Contracts\AgentInterface;
  8 +
  9 +class Ding implements AgentInterface
  10 +{
  11 + use Attribute;
  12 +
  13 + public string $token = '';
  14 +
  15 + public string $secret = '';
  16 +
  17 + public bool $enable = true;
  18 +
  19 + public function report(array $content)
  20 + {
  21 + $talk = new DingTalkService([
  22 +
  23 + ]);
  24 + $talk->setTextMessage(join("\n", $content))->send();
  25 + }
  26 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush\Agents;
  4 +
  5 +use GuzzleHttp\RequestOptions;
  6 +use Lackoxygen\ExceptionPush\Attribute\Attribute;
  7 +use Lackoxygen\ExceptionPush\Client;
  8 +use Lackoxygen\ExceptionPush\Contracts\AgentInterface;
  9 +
  10 +class Wx implements AgentInterface
  11 +{
  12 + use Attribute;
  13 +
  14 + public string $key = '';
  15 +
  16 + public bool $enable = true;
  17 +
  18 + public function report(array $content)
  19 + {
  20 + /**
  21 + * @var \GuzzleHttp\Client $client
  22 + */
  23 + $client = Client::new('https://qyapi.weixin.qq.com');
  24 +
  25 + $client->post('cgi-bin/webhook/send', [
  26 + RequestOptions::HEADERS => [
  27 + 'content-type' => 'application/json'
  28 + ], RequestOptions::QUERY => [
  29 + 'key' => $this->key,
  30 + ], RequestOptions::JSON => [
  31 + 'msgtype' => 'text', 'text' => [
  32 + 'content' => join("\n", $content)
  33 + ]
  34 + ]
  35 + ]);
  36 + }
  37 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush\Attribute;
  4 +
  5 +trait Attribute
  6 +{
  7 + public function __set($name, $value)
  8 + {
  9 + $this->$name = $value;
  10 + }
  11 +
  12 +
  13 + public function __get($name)
  14 + {
  15 + if (property_exists($this, $name)) {
  16 + return $this->$name;
  17 + }
  18 +
  19 + return null;
  20 + }
  21 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush\Attribute;
  4 +
  5 +use Lackoxygen\ExceptionPush\Contracts\AgentInterface;
  6 +
  7 +class Context
  8 +{
  9 + private string $exception;
  10 +
  11 + private string $message;
  12 +
  13 + private array $input;
  14 +
  15 + private string $code;
  16 +
  17 + private string $line;
  18 +
  19 + private string $file;
  20 +
  21 + private array $trace;
  22 +
  23 + private string $path;
  24 +
  25 + private string $method;
  26 +
  27 + private string $ip;
  28 +
  29 + private array $extras = [];
  30 +
  31 + /**
  32 + * @return string
  33 + */
  34 + public function getException(): string
  35 + {
  36 + return $this->exception;
  37 + }
  38 +
  39 + /**
  40 + * @param string $exception
  41 + */
  42 + public function setException(string $exception): void
  43 + {
  44 + $this->exception = $exception;
  45 + }
  46 +
  47 + /**
  48 + * @return string
  49 + */
  50 + public function getMessage(): string
  51 + {
  52 + return $this->message;
  53 + }
  54 +
  55 + /**
  56 + * @param string $message
  57 + */
  58 + public function setMessage(string $message): void
  59 + {
  60 + $this->message = $message;
  61 + }
  62 +
  63 + /**
  64 + * @return mixed
  65 + */
  66 + public function getCode(): string
  67 + {
  68 + return $this->code;
  69 + }
  70 +
  71 + /**
  72 + * @param string $code
  73 + */
  74 + public function setCode(string $code): void
  75 + {
  76 + $this->code = $code;
  77 + }
  78 +
  79 + /**
  80 + * @return string
  81 + */
  82 + public function getLine(): string
  83 + {
  84 + return $this->line;
  85 + }
  86 +
  87 + /**
  88 + * @param string $line
  89 + */
  90 + public function setLine(string $line): void
  91 + {
  92 + $this->line = $line;
  93 + }
  94 +
  95 + /**
  96 + * @return string
  97 + */
  98 + public function getFile(): string
  99 + {
  100 + return $this->file;
  101 + }
  102 +
  103 + /**
  104 + * @param string $file
  105 + */
  106 + public function setFile(string $file): void
  107 + {
  108 + $this->file = $file;
  109 + }
  110 +
  111 + /**
  112 + * @return array
  113 + */
  114 + public function getTrace(): array
  115 + {
  116 + return $this->trace;
  117 + }
  118 +
  119 + /**
  120 + * @param array $trace
  121 + */
  122 + public function setTrace(array $trace): void
  123 + {
  124 + $this->trace = $trace;
  125 + }
  126 +
  127 + /**
  128 + * @return string
  129 + */
  130 + public function getPath(): string
  131 + {
  132 + return $this->path;
  133 + }
  134 +
  135 + /**
  136 + * @param string $path
  137 + */
  138 + public function setPath(string $path): void
  139 + {
  140 + $this->path = $path;
  141 + }
  142 +
  143 + /**
  144 + * @return string
  145 + */
  146 + public function getMethod(): string
  147 + {
  148 + return $this->method;
  149 + }
  150 +
  151 + /**
  152 + * @param string $method
  153 + */
  154 + public function setMethod(string $method): void
  155 + {
  156 + $this->method = $method;
  157 + }
  158 +
  159 + /**
  160 + * @return array
  161 + */
  162 + public function getAgents(): array
  163 + {
  164 + return $this->agents;
  165 + }
  166 +
  167 + /**
  168 + * @param AgentInterface $agent
  169 + */
  170 + public function pushAgent(AgentInterface $agent): void
  171 + {
  172 + $this->agents[] = $agent;
  173 + }
  174 +
  175 + /**
  176 + * @param string $ip
  177 + */
  178 + public function setIp(string $ip): void
  179 + {
  180 + $this->ip = $ip;
  181 + }
  182 +
  183 + /**
  184 + * @return string
  185 + */
  186 + public function getIp(): string
  187 + {
  188 + return $this->ip;
  189 + }
  190 +
  191 + /**
  192 + * @param array $input
  193 + */
  194 + public function setInput(array $input): void
  195 + {
  196 + $this->input = $input;
  197 + }
  198 +
  199 + /**
  200 + * @return array
  201 + */
  202 + public function getInput(): array
  203 + {
  204 + return $this->input;
  205 + }
  206 +
  207 + /**
  208 + * @param array $extras
  209 + */
  210 + public function setExtras(array $extras): void
  211 + {
  212 + $this->extras = $extras;
  213 + }
  214 +
  215 + /**
  216 + * @return array
  217 + */
  218 + public function getExtras(): array
  219 + {
  220 + return $this->extras;
  221 + }
  222 +
  223 + /**
  224 + * @return array
  225 + */
  226 + public function __serialize(): array
  227 + {
  228 + return [
  229 + 'exception' => $this->exception, 'message' => $this->message, 'ip' => $this->ip, 'code' => $this->code,
  230 + 'line' => $this->line, 'file' => $this->file, 'trace' => $this->trace, 'path' => $this->path,
  231 + 'method' => $this->method, 'agents' => $this->agents, 'input' => $this->input
  232 + ];
  233 + }
  234 +
  235 + public function __unserialize(array $data): void
  236 + {
  237 + foreach ($data as $k => $v) {
  238 + $this->{$k} = $v;
  239 + }
  240 + }
  241 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush;
  4 +
  5 +use GuzzleHttp\RequestOptions;
  6 +
  7 +class Client
  8 +{
  9 + protected \GuzzleHttp\Client $engine;
  10 +
  11 + public static function new($baseUri): Client
  12 + {
  13 + return new static($baseUri);
  14 + }
  15 +
  16 + public function __construct(string $baseUri)
  17 + {
  18 + $this->engine = new \GuzzleHttp\Client([
  19 + 'base_uri' => $baseUri, RequestOptions::TIMEOUT => ExceptionPush::config('client.timeout', 30),
  20 + RequestOptions::VERIFY => false
  21 + ]);
  22 + }
  23 +
  24 + public function __call($name, $arguments)
  25 + {
  26 + return call_user_func_array([$this->engine, $name], $arguments);
  27 + }
  28 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush\Contracts;
  4 +
  5 +interface AgentInterface
  6 +{
  7 + public function report(array $content);
  8 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush\Contracts;
  4 +
  5 +interface CallbackInterface
  6 +{
  7 + public function default(): \Closure;
  8 +
  9 + public function config(): ?\Closure;
  10 +
  11 + public static function callback(): \Closure;
  12 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush\Contracts;
  4 +
  5 +interface ExceptionHandler
  6 +{
  7 + /**
  8 + * @param \Throwable $e
  9 + *
  10 + * @return mixed
  11 + */
  12 + public function handle(\Throwable $e);
  13 +
  14 + /**
  15 + * @param \Throwable $e
  16 + *
  17 + * @return mixed
  18 + */
  19 + public function render(\Throwable $e);
  20 +
  21 + /**
  22 + * @param \Throwable $e
  23 + *
  24 + * @return mixed
  25 + */
  26 + public function report(\Throwable $e);
  27 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush;
  4 +
  5 +use Lackoxygen\ExceptionPush\Contracts\AgentInterface;
  6 +use Lackoxygen\ExceptionPush\Contracts\CallbackInterface;
  7 +
  8 +class Dispatcher implements CallbackInterface
  9 +{
  10 + /**
  11 + * @return \Closure|null
  12 + */
  13 + public function config(): ?\Closure
  14 + {
  15 + $dispatcher = ExceptionPush::config('callbacks.dispatcher');
  16 +
  17 + if ($dispatcher instanceof \Closure) {
  18 + return $dispatcher;
  19 + }
  20 +
  21 + return null;
  22 + }
  23 +
  24 + /**
  25 + * @return \Closure
  26 + */
  27 + public function default(): \Closure
  28 + {
  29 + return function ($agents, $body) {
  30 + foreach ($agents as $agent) {
  31 + if ($agent instanceof AgentInterface) {
  32 + try {
  33 + $agent->report($body);
  34 + } catch (\Exception $exception) {
  35 + app('log')->error($exception->getMessage());
  36 + }
  37 + }
  38 + }
  39 + };
  40 + }
  41 +
  42 + /**
  43 + * @return \Closure
  44 + */
  45 + public static function callback(): \Closure
  46 + {
  47 + $that = new static;
  48 +
  49 + if ($dispatcher = $that->config()) {
  50 + return $dispatcher;
  51 + }
  52 +
  53 + return $that->default();
  54 + }
  55 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush\Exception;
  4 +
  5 +use Lackoxygen\ExceptionPush\Contracts\ExceptionHandler;
  6 +
  7 +class Handler implements ExceptionHandler
  8 +{
  9 + public function handle(\Throwable $e)
  10 + {
  11 + app('exception.push')->boot();
  12 + }
  13 +
  14 +
  15 + public function render(\Throwable $e)
  16 + {
  17 + app('exception.push')->boot();
  18 + }
  19 +
  20 +
  21 + public function report(\Throwable $e)
  22 + {
  23 + app('exception.push')->boot();
  24 + }
  25 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush;
  4 +
  5 +use Illuminate\Support\Arr;
  6 +
  7 +class ExceptionPush
  8 +{
  9 + /**
  10 + * @var Parser
  11 + */
  12 + protected Parser $parser;
  13 +
  14 + /**
  15 + * @var array
  16 + */
  17 + protected array $agents;
  18 +
  19 + public function __construct()
  20 + {
  21 + $this->parser = new Parser;
  22 +
  23 + $this->agents = $this->getAgents();
  24 + }
  25 +
  26 + /**
  27 + * @return array
  28 + */
  29 + protected function getAgents(): array
  30 + {
  31 + $agentOpts = (array) static::config('agents');
  32 +
  33 + $agents = [];
  34 +
  35 + foreach ($agentOpts as $agentName => $opts) {
  36 + $agent = new $agentName;
  37 +
  38 + if (!is_array($opts)) {
  39 + continue;
  40 + }
  41 +
  42 + foreach ($opts as $key => $value) {
  43 + echo "{$key} => $value\n";
  44 + $agent->{$key} = $value;
  45 + }
  46 +
  47 + $agents[] = $agent;
  48 + }
  49 +
  50 + return $agents;
  51 + }
  52 +
  53 +
  54 + /**
  55 + * @param $key
  56 + * @param $default
  57 + *
  58 + * @return array|\ArrayAccess|mixed
  59 + */
  60 + public static function config($key = null, $default = null)
  61 + {
  62 + $config = \config('exception.push');
  63 +
  64 + return Arr::get($config, $key, $default);
  65 + }
  66 +
  67 + /**
  68 + * @return void
  69 + */
  70 + public function boot(\Throwable $e)
  71 + {
  72 + $this->parser->extract($e);
  73 +
  74 + $this->dispatch($this->format());
  75 + }
  76 +
  77 + protected function format(): array
  78 + {
  79 + $formatter = Formatter::callback();
  80 +
  81 + $body = $formatter($this->parser->context());
  82 +
  83 +
  84 + if (!is_array($body)) {
  85 + throw new \RuntimeException('Custom function must return array format');
  86 + }
  87 +
  88 + return $body;
  89 + }
  90 +
  91 + /**
  92 + * @param array $body
  93 + *
  94 + * @return void
  95 + */
  96 + protected function dispatch(array $body)
  97 + {
  98 + $dispatcher = Dispatcher::callback();
  99 + $dispatcher($this->agents, $body);
  100 + }
  101 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush;
  4 +
  5 +use Illuminate\Support\ServiceProvider;
  6 +use Lackoxygen\ExceptionPush\Contracts\ExceptionHandler;
  7 +use Lackoxygen\ExceptionPush\Exception\Handler;
  8 +
  9 +class ExceptionPushProvider extends ServiceProvider
  10 +{
  11 + protected array $commands = [];
  12 +
  13 + public function register()
  14 + {
  15 + $this->app->singleton(ExceptionHandler::class, Handler::class);
  16 + $this->app->singleton('exception.push', ExceptionHandler::class);
  17 + }
  18 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush;
  4 +
  5 +use Carbon\Carbon;
  6 +use Lackoxygen\ExceptionPush\Attribute\Context;
  7 +use Lackoxygen\ExceptionPush\Contracts\CallbackInterface;
  8 +
  9 +class Formatter implements CallbackInterface
  10 +{
  11 + /**
  12 + * @return \Closure
  13 + */
  14 + public function default(): \Closure
  15 + {
  16 + return function (Context $context) {
  17 + return [
  18 + '时间:'.Carbon::now()->toDateTimeString(), '环境:'.config('app.env'), '项目:'.config('app.name'),
  19 + '参数:'.json_encode($context->getInput()), 'runtime:'.php_sapi_name(), '地址:'.$context->getPath(),
  20 + '请求方法:'.$context->getMethod(), 'IP:'.$context->getIp(),
  21 + '异常:'.sprintf('%s(%s)(code:%d):at %s:%d', $context->getException(), $context->getMessage(),
  22 + $context->getCode(), $context->getFile(), $context->getLine()),
  23 + 'trace:'.implode(PHP_EOL, $context->getTrace()),
  24 + ];
  25 + };
  26 + }
  27 +
  28 + /**
  29 + * @return \Closure|null
  30 + */
  31 + public function config(): ?\Closure
  32 + {
  33 + $formatter = ExceptionPush::config('callbacks.formatter');
  34 +
  35 + if ($formatter instanceof \Closure) {
  36 + return $formatter;
  37 + }
  38 + }
  39 +
  40 +
  41 + /**
  42 + * @return \Closure
  43 + */
  44 + public static function callback(): \Closure
  45 + {
  46 + $that = new static;
  47 +
  48 + if ($formatter = $that->config()) {
  49 + return $formatter;
  50 + }
  51 +
  52 + return $that->default();
  53 + }
  54 +}
  1 +<?php
  2 +
  3 +namespace Lackoxygen\ExceptionPush;
  4 +
  5 +use Illuminate\Http\Request;
  6 +use Illuminate\Support\Str;
  7 +use Lackoxygen\ExceptionPush\Attribute\Context;
  8 +
  9 +class Parser
  10 +{
  11 + protected \Throwable $throw;
  12 +
  13 + protected Context $context;
  14 +
  15 + protected Request $request;
  16 +
  17 + public function __construct()
  18 + {
  19 + $this->context = new Context;
  20 +
  21 + $this->request = app(Request::class);
  22 + }
  23 +
  24 + /**
  25 + * @return array
  26 + */
  27 + protected function simpleTrace(): array
  28 + {
  29 + return array_map(function ($line) {
  30 + return $this->realpath($line);
  31 + }, array_slice(explode(PHP_EOL, $this->throw->getTraceAsString()), 0, 4));
  32 + }
  33 +
  34 + /**
  35 + * @param string $line
  36 + *
  37 + * @return string
  38 + */
  39 + protected function realpath(string $line): string
  40 + {
  41 + return Str::replace(app()->basePath(), '', $line);
  42 + }
  43 +
  44 + /**
  45 + * @param \Throwable $e
  46 + *
  47 + * @return void
  48 + */
  49 + public function extract(\Throwable $e): void
  50 + {
  51 + $this->throw = $e;
  52 +
  53 + $this->context->setException(get_class($this->throw));
  54 + $this->context->setMethod($this->request->getMethod());
  55 + $this->context->setPath($this->request->path());
  56 + $this->context->setCode((string) $this->throw->getCode());
  57 + $this->context->setFile($this->realpath($this->throw->getFile()));
  58 + $this->context->setLine($this->throw->getLine());
  59 + $this->context->setMessage($this->throw->getMessage());
  60 + $this->context->setTrace($this->simpleTrace());
  61 + $this->context->setInput($this->request->post());
  62 + $this->context->setIp($this->request->ip());
  63 + }
  64 +
  65 + /**
  66 + * @return Context
  67 + */
  68 + public function context(): Context
  69 + {
  70 + return $this->context;
  71 + }
  72 +}