作者 竞泽

init

/vendor/
.idea
composer.lock
... ...
{
"name": "lackoxygen/exception-push",
"type": "library",
"description": "异常推送",
"autoload": {
"psr-4": {
"Lackoxygen\\ExceptionPush\\": "src/"
}
},
"authors": [
{
"name": "ojz",
"email": "jingzeou@outlook.com"
}
],
"autoload-dev": {
"psr-4": {
"Lackoxygen\\ExceptionPush\\Tests\\": "tests/"
}
},
"require": {
"php": ">=7.4",
"wujunze/dingtalk-exception": "^2.2"
},
"require-dev": {
"phpunit/phpunit": "^9.5.10"
},
"extra": {
"laravel": {
"providers": [
"Lackoxygen\\ExceptionPush\\ExceptionPushProvider"
]
}
}
}
... ...
<?php
use Lackoxygen\ExceptionPush\Agents\{Ding, Wx};
return [
'agents' => [
Wx::class => [
'key' => '', 'enable' => false
], Ding::class => [
'token' => '', 'secret' => '', 'enable' => false
]
],
'client' => [
'timeout' => 30.00,
],
'callbacks' => [
'formatter' => function (\Lackoxygen\ExceptionPush\Attribute\Context $context) { },
'dispatcher' => function () { }
]
];
... ...
<?php
namespace Lackoxygen\ExceptionPush\Agents;
use DingNotice\DingTalkService;
use Lackoxygen\ExceptionPush\Attribute\Attribute;
use Lackoxygen\ExceptionPush\Contracts\AgentInterface;
class Ding implements AgentInterface
{
use Attribute;
public string $token = '';
public string $secret = '';
public bool $enable = true;
public function report(array $content)
{
$talk = new DingTalkService([
]);
$talk->setTextMessage(join("\n", $content))->send();
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush\Agents;
use GuzzleHttp\RequestOptions;
use Lackoxygen\ExceptionPush\Attribute\Attribute;
use Lackoxygen\ExceptionPush\Client;
use Lackoxygen\ExceptionPush\Contracts\AgentInterface;
class Wx implements AgentInterface
{
use Attribute;
public string $key = '';
public bool $enable = true;
public function report(array $content)
{
/**
* @var \GuzzleHttp\Client $client
*/
$client = Client::new('https://qyapi.weixin.qq.com');
$client->post('cgi-bin/webhook/send', [
RequestOptions::HEADERS => [
'content-type' => 'application/json'
], RequestOptions::QUERY => [
'key' => $this->key,
], RequestOptions::JSON => [
'msgtype' => 'text', 'text' => [
'content' => join("\n", $content)
]
]
]);
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush\Attribute;
trait Attribute
{
public function __set($name, $value)
{
$this->$name = $value;
}
public function __get($name)
{
if (property_exists($this, $name)) {
return $this->$name;
}
return null;
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush\Attribute;
use Lackoxygen\ExceptionPush\Contracts\AgentInterface;
class Context
{
private string $exception;
private string $message;
private array $input;
private string $code;
private string $line;
private string $file;
private array $trace;
private string $path;
private string $method;
private string $ip;
private array $extras = [];
/**
* @return string
*/
public function getException(): string
{
return $this->exception;
}
/**
* @param string $exception
*/
public function setException(string $exception): void
{
$this->exception = $exception;
}
/**
* @return string
*/
public function getMessage(): string
{
return $this->message;
}
/**
* @param string $message
*/
public function setMessage(string $message): void
{
$this->message = $message;
}
/**
* @return mixed
*/
public function getCode(): string
{
return $this->code;
}
/**
* @param string $code
*/
public function setCode(string $code): void
{
$this->code = $code;
}
/**
* @return string
*/
public function getLine(): string
{
return $this->line;
}
/**
* @param string $line
*/
public function setLine(string $line): void
{
$this->line = $line;
}
/**
* @return string
*/
public function getFile(): string
{
return $this->file;
}
/**
* @param string $file
*/
public function setFile(string $file): void
{
$this->file = $file;
}
/**
* @return array
*/
public function getTrace(): array
{
return $this->trace;
}
/**
* @param array $trace
*/
public function setTrace(array $trace): void
{
$this->trace = $trace;
}
/**
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @param string $path
*/
public function setPath(string $path): void
{
$this->path = $path;
}
/**
* @return string
*/
public function getMethod(): string
{
return $this->method;
}
/**
* @param string $method
*/
public function setMethod(string $method): void
{
$this->method = $method;
}
/**
* @return array
*/
public function getAgents(): array
{
return $this->agents;
}
/**
* @param AgentInterface $agent
*/
public function pushAgent(AgentInterface $agent): void
{
$this->agents[] = $agent;
}
/**
* @param string $ip
*/
public function setIp(string $ip): void
{
$this->ip = $ip;
}
/**
* @return string
*/
public function getIp(): string
{
return $this->ip;
}
/**
* @param array $input
*/
public function setInput(array $input): void
{
$this->input = $input;
}
/**
* @return array
*/
public function getInput(): array
{
return $this->input;
}
/**
* @param array $extras
*/
public function setExtras(array $extras): void
{
$this->extras = $extras;
}
/**
* @return array
*/
public function getExtras(): array
{
return $this->extras;
}
/**
* @return array
*/
public function __serialize(): array
{
return [
'exception' => $this->exception, 'message' => $this->message, 'ip' => $this->ip, 'code' => $this->code,
'line' => $this->line, 'file' => $this->file, 'trace' => $this->trace, 'path' => $this->path,
'method' => $this->method, 'agents' => $this->agents, 'input' => $this->input
];
}
public function __unserialize(array $data): void
{
foreach ($data as $k => $v) {
$this->{$k} = $v;
}
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush;
use GuzzleHttp\RequestOptions;
class Client
{
protected \GuzzleHttp\Client $engine;
public static function new($baseUri): Client
{
return new static($baseUri);
}
public function __construct(string $baseUri)
{
$this->engine = new \GuzzleHttp\Client([
'base_uri' => $baseUri, RequestOptions::TIMEOUT => ExceptionPush::config('client.timeout', 30),
RequestOptions::VERIFY => false
]);
}
public function __call($name, $arguments)
{
return call_user_func_array([$this->engine, $name], $arguments);
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush\Contracts;
interface AgentInterface
{
public function report(array $content);
}
... ...
<?php
namespace Lackoxygen\ExceptionPush\Contracts;
interface CallbackInterface
{
public function default(): \Closure;
public function config(): ?\Closure;
public static function callback(): \Closure;
}
... ...
<?php
namespace Lackoxygen\ExceptionPush\Contracts;
interface ExceptionHandler
{
/**
* @param \Throwable $e
*
* @return mixed
*/
public function handle(\Throwable $e);
/**
* @param \Throwable $e
*
* @return mixed
*/
public function render(\Throwable $e);
/**
* @param \Throwable $e
*
* @return mixed
*/
public function report(\Throwable $e);
}
... ...
<?php
namespace Lackoxygen\ExceptionPush;
use Lackoxygen\ExceptionPush\Contracts\AgentInterface;
use Lackoxygen\ExceptionPush\Contracts\CallbackInterface;
class Dispatcher implements CallbackInterface
{
/**
* @return \Closure|null
*/
public function config(): ?\Closure
{
$dispatcher = ExceptionPush::config('callbacks.dispatcher');
if ($dispatcher instanceof \Closure) {
return $dispatcher;
}
return null;
}
/**
* @return \Closure
*/
public function default(): \Closure
{
return function ($agents, $body) {
foreach ($agents as $agent) {
if ($agent instanceof AgentInterface) {
try {
$agent->report($body);
} catch (\Exception $exception) {
app('log')->error($exception->getMessage());
}
}
}
};
}
/**
* @return \Closure
*/
public static function callback(): \Closure
{
$that = new static;
if ($dispatcher = $that->config()) {
return $dispatcher;
}
return $that->default();
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush\Exception;
use Lackoxygen\ExceptionPush\Contracts\ExceptionHandler;
class Handler implements ExceptionHandler
{
public function handle(\Throwable $e)
{
app('exception.push')->boot();
}
public function render(\Throwable $e)
{
app('exception.push')->boot();
}
public function report(\Throwable $e)
{
app('exception.push')->boot();
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush;
use Illuminate\Support\Arr;
class ExceptionPush
{
/**
* @var Parser
*/
protected Parser $parser;
/**
* @var array
*/
protected array $agents;
public function __construct()
{
$this->parser = new Parser;
$this->agents = $this->getAgents();
}
/**
* @return array
*/
protected function getAgents(): array
{
$agentOpts = (array) static::config('agents');
$agents = [];
foreach ($agentOpts as $agentName => $opts) {
$agent = new $agentName;
if (!is_array($opts)) {
continue;
}
foreach ($opts as $key => $value) {
echo "{$key} => $value\n";
$agent->{$key} = $value;
}
$agents[] = $agent;
}
return $agents;
}
/**
* @param $key
* @param $default
*
* @return array|\ArrayAccess|mixed
*/
public static function config($key = null, $default = null)
{
$config = \config('exception.push');
return Arr::get($config, $key, $default);
}
/**
* @return void
*/
public function boot(\Throwable $e)
{
$this->parser->extract($e);
$this->dispatch($this->format());
}
protected function format(): array
{
$formatter = Formatter::callback();
$body = $formatter($this->parser->context());
if (!is_array($body)) {
throw new \RuntimeException('Custom function must return array format');
}
return $body;
}
/**
* @param array $body
*
* @return void
*/
protected function dispatch(array $body)
{
$dispatcher = Dispatcher::callback();
$dispatcher($this->agents, $body);
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush;
use Illuminate\Support\ServiceProvider;
use Lackoxygen\ExceptionPush\Contracts\ExceptionHandler;
use Lackoxygen\ExceptionPush\Exception\Handler;
class ExceptionPushProvider extends ServiceProvider
{
protected array $commands = [];
public function register()
{
$this->app->singleton(ExceptionHandler::class, Handler::class);
$this->app->singleton('exception.push', ExceptionHandler::class);
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush;
use Carbon\Carbon;
use Lackoxygen\ExceptionPush\Attribute\Context;
use Lackoxygen\ExceptionPush\Contracts\CallbackInterface;
class Formatter implements CallbackInterface
{
/**
* @return \Closure
*/
public function default(): \Closure
{
return function (Context $context) {
return [
'时间:'.Carbon::now()->toDateTimeString(), '环境:'.config('app.env'), '项目:'.config('app.name'),
'参数:'.json_encode($context->getInput()), 'runtime:'.php_sapi_name(), '地址:'.$context->getPath(),
'请求方法:'.$context->getMethod(), 'IP:'.$context->getIp(),
'异常:'.sprintf('%s(%s)(code:%d):at %s:%d', $context->getException(), $context->getMessage(),
$context->getCode(), $context->getFile(), $context->getLine()),
'trace:'.implode(PHP_EOL, $context->getTrace()),
];
};
}
/**
* @return \Closure|null
*/
public function config(): ?\Closure
{
$formatter = ExceptionPush::config('callbacks.formatter');
if ($formatter instanceof \Closure) {
return $formatter;
}
}
/**
* @return \Closure
*/
public static function callback(): \Closure
{
$that = new static;
if ($formatter = $that->config()) {
return $formatter;
}
return $that->default();
}
}
... ...
<?php
namespace Lackoxygen\ExceptionPush;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Lackoxygen\ExceptionPush\Attribute\Context;
class Parser
{
protected \Throwable $throw;
protected Context $context;
protected Request $request;
public function __construct()
{
$this->context = new Context;
$this->request = app(Request::class);
}
/**
* @return array
*/
protected function simpleTrace(): array
{
return array_map(function ($line) {
return $this->realpath($line);
}, array_slice(explode(PHP_EOL, $this->throw->getTraceAsString()), 0, 4));
}
/**
* @param string $line
*
* @return string
*/
protected function realpath(string $line): string
{
return Str::replace(app()->basePath(), '', $line);
}
/**
* @param \Throwable $e
*
* @return void
*/
public function extract(\Throwable $e): void
{
$this->throw = $e;
$this->context->setException(get_class($this->throw));
$this->context->setMethod($this->request->getMethod());
$this->context->setPath($this->request->path());
$this->context->setCode((string) $this->throw->getCode());
$this->context->setFile($this->realpath($this->throw->getFile()));
$this->context->setLine($this->throw->getLine());
$this->context->setMessage($this->throw->getMessage());
$this->context->setTrace($this->simpleTrace());
$this->context->setInput($this->request->post());
$this->context->setIp($this->request->ip());
}
/**
* @return Context
*/
public function context(): Context
{
return $this->context;
}
}
... ...