Laravel Exception结合自定义Log服务的使用

第一部分:laravel关于错误和异常的部分源码

第二部分:自定义异常的使用(结合serviceprovider monolog elasticsearch)

过程中涉及到的重要函数请自行查看手册

error_reporting set_error_handler set_exception_handler register_shutdown_function error_get_last

laravel v6.18.40

源码部分

我们来到http kernel文件,处理请求部分

可以看到执行我们业务逻辑的sendRequestThroughRouter方法被try_catch包裹的严严实实

public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e); $response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e);
} $this->app['events']->dispatch(
new RequestHandled($request, $response)
); return $response;
}

捕获到异常后 框架做了哪些工作呢? reportException 记录了异常 renderException 响应了异常

/**
* Report the exception to the exception handler.
*
* @param \Exception $e
* @return void
*/
protected function reportException(Exception $e)
{
# 从容器中解析ExceptionHandler, 绑定位于bootstrap/app.php中
# 执行的是App\Exceptions\Handler的report方法
$this->app[ExceptionHandler::class]->report($e);
} # 跳转到App\Exceptions\Handler的report方法
public function report(Exception $exception)
{
# 继续跳转到父类的report方法
parent::report($exception);
} # 只看核心代码
Illuminate\Foundation\Exceptions\Handler::report()
/**
* Report or log an exception.
*
* @param \Exception $e
* @return void
*
* @throws \Exception
*/
public function report(Exception $e)
{
if ($this->shouldntReport($e)) {
return;
} # 判断传递进来的异常是否存在report方法,有就执行
if (is_callable($reportCallable = [$e, 'report'])) {
// 值得注意的是,此步骤通过容器调用
// 这意味着我们可以在自定义的异常类中肆无忌惮的向report方法中诸如依赖了!!!
// 后面自定义异常类的使用会提及
return $this->container->call($reportCallable);
} # 从容器中解析log服务
# Illuminate\Log\LogManager实例
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $e;
} # 记录日志 基于monolog 后面自定义日志服务会讲解monolog的使用
$logger->error(
$e->getMessage(),
array_merge(
$this->exceptionContext($e),
$this->context(),
['exception' => $e]
)
);
} # renderException方法请自行查看

通过上面的代码,我们自然而然的认为这样就可以捕获应用中产生的所有异常了,其实不然

下面我们来看框架引导阶段为处理错误和异常做的工作

逻辑同样位于框架的boot阶段

下面给出简要介绍

Illuminate\Foundation\Bootstrap\HandleExceptions::bootstrap()
public function bootstrap(Application $app)
{
self::$reservedMemory = str_repeat('x', 10240); $this->app = $app; // 尽可能显示所有错误, 甚至包括将来 PHP 可能加入的新的错误级别和常量
// -1 和 E_ALL | E_STRICT 的区别为是否向后兼容
error_reporting(-1); // 设置执行逻辑部分出现错误的回调
// 默认错误级别为 E_ALL | E_STRICT
// 网上说此函数只有warning和notice级别的错误能够触发的说法不够准确
// 个人拙见:可以触发回调的错误级别为运行时产生的错误
// 直接中断脚本执行的错误不能触发此回调 因为回调还未注册
// 为了更大范围的抓取错误,需要配合register_shutdown_function 和 error_get_last 处理
set_error_handler([$this, 'handleError']); // 设置捕获执行逻辑部分未捕获的异常回调
set_exception_handler([$this, 'handleException']); // 设置php脚本结束前最后执行的回调
register_shutdown_function([$this, 'handleShutdown']); if (! $app->environment('testing')) {
ini_set('display_errors', 'Off');
}
} # 将php错误转化为异常抛出
/**
* Convert PHP errors to ErrorException instances.
* @throws \ErrorException
*/
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
} # 注释已经非常清晰 致命错误异常不能按照普通异常处理 在此处直接记录和返回响应
/**
* Handle an uncaught exception from the application.
*
* Note: Most exceptions can be handled via the try / catch block in
* the HTTP and Console kernels. But, fatal error exceptions must
* be handled differently since they are not normal exceptions.
*
* @param \Throwable $e
* @return void
*/
public function handleException($e)
{
if (! $e instanceof Exception) {
$e = new FatalThrowableError($e);
} try {
self::$reservedMemory = null; $this->getExceptionHandler()->report($e);
} catch (Exception $e) {
//
} if ($this->app->runningInConsole()) {
$this->renderForConsole($e);
} else {
$this->renderHttpResponse($e);
}
} /**
* Handle the PHP shutdown event.
*
* @return void
*/
public function handleShutdown()
{
// 生成的异常类是symfony封装的异常类
// 例:可以在任意路由中来上一句不加分号的代码 看看测试效果
if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
$this->handleException($this->fatalExceptionFromError($error, 0));
}
}
使用部分

自定义异常的使用

方式一:直接抛出一个异常 在中判断 进行自定义的处理

1 创建一个自定义异常类
php artisan make:exception CustomException 2 在业务逻辑中抛出异常
Route::get('ex', function () {
throw new CustomException('thrown for caught');
}); 3 扩展App\Exceptions\Handler类
public function report(Exception $exception)
{
if ($exception instanceof CustomException) {
Log::channel('daily')->error($exception->getMessage(), ['type' => 'myEx']);
}
parent::report($exception);
}
# 当然你也可以扩展render方法 4 访问路由
# 查看logs下的文件

以上方法显然不够优雅,当异常变多的时候,需要配合大量的instanceof判断,并且可能会记录两份相同内容的日志

所以还可以使用第二种方式进行自定义异常的使用,利用框架自动调用report和render方法的特性,实现记录和渲染异常响应

方式二:自定义一个exception 然后让框架自动调用report方法 不进行render

1 创建自定义并编辑自定义异常
php artisan make:exception MyException
class MyException extends Exception
{
public function report()
{
Log::channel('daily')->error($this->getMessage(), array_merge($this->exceptionContext(), $this->context()));
} // 其实是不必要的 这两个方法可以在Handler中进行重写,请自行查看Handler的父类,根据需要进行扩展重写
public function exceptionContext()
{
// return [$this->getTraceAsString()];
return [];
} // 其实是不必要的 这两个方法可以在Handler中进行重写
public function context()
{
// return ['type' => 'myEx'];
return ['exception' => $this];
} public function render()
{
return 'whatever you like';
}
} 2 抛出异常
Route::get('myex', function () {
throw new MyException('thrown for caught');
}); 3 执行并查看日志文件 是不是发现和laravel原生的异常记录长得很像呢

方式三:使用自定义的日志服务记录异常

上面提到异常实例的report是通过容器调用的,这意味着我们可以注入我们自定义的日志服务

这里使用神器monolog,因为laravel的日志系统基于monolog,框架已经包含了此库。如果使用其他框架请先确保安装monolog

这里使用elasticsearch作为日志handler之一,所以请确保安装了composer require elasticsearch/elasticsearch

使用monolog作为自定义日志服务实现的原因是因为monolog本身具有替换性和通用性,其他框架稍加改动也可以使用

1 创建契约
<?php namespace App\Contracts; interface ExceptionLog
{
// 记录异常
public function recordEx(\Exception $e);
} 2 创建日志服务 并简单介绍monolog使用
# 关于monolog的更多使用方法请查看官方文档 https://github.com/Seldaek/monolog
<?php namespace App\Services\Logs; use App\Contracts\ExceptionLog;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\ElasticsearchHandler;
use Elasticsearch\ClientBuilder; class MonoException implements ExceptionLog
{
protected $logger; public function __construct()
{
// 创建本地文件存储handlers
$streamHandler = new StreamHandler(storage_path('logs/exception.log'), Logger::DEBUG); // 创建本地日期切分存储handler
$rotateHandler = new RotatingFileHandler(storage_path('logs/exception.log'), Logger::DEBUG); // 创建es 客户端 为了减小难度 就不在这里注入elasticsearch客户端了 其实是我懒 开心撸码最重要
$esClient = ClientBuilder::create()->setHosts(config('es.hosts'))->build();
// es配置
$options = [
'index' => 'my_exception_index',
'type' => 'test_exception',
]; // 创建远程elasticsearch日志存储
$esHandler = new ElasticsearchHandler($esClient, $options); // 这里没有阻止handlers的堆栈冒泡,一条日志会逐个经过es、rotate、stream日志处理器
// 更多的日志存储handler请查看文档(为了性能考量,monolog甚至为你提供了异步方式记录日志) // 创建logger 虽然叫logger但是他并没有记录日志的能力
// 真正提供记录日志能力的是提前创建好的handlers
// monolog提供非常多开箱即用的handler 请查看文档
// 并没有设置processor等等 更多api请查看官方文档
$logger = new Logger('exception', compact('streamHandler', 'rotateHandler', 'esHandler'));
$this->logger = $logger;
} public function recordEx(\Exception $e)
{
$this->logger->error($e->getMessage());
}
} 3 创建服务提供者 因为我们创建的服务在异常中调用 所以使用单例和延迟绑定更加合适
php artisan make:provider LogServiceProvider
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider;
use App\Contracts\ExceptionLog;
use App\Services\Logs\MonoException;
use Illuminate\Contracts\Support\DeferrableProvider; class LogServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->singleton(ExceptionLog::class, function () {
return new MonoException();
});
} /**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
} public function provides()
{
return [ExceptionLog::class];
}
} 4 注册服务 config/app.php
...
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
// 注册自定义日志服务
App\Providers\LogServiceProvider::class, 4 创建自定义异常 并在其中使用自定义的日志服务
<?php namespace App\Exceptions; use Exception;
use App\Contracts\ExceptionLog; class CustomException extends Exception
{
// 注入我们的日志服务
public function report(ExceptionLog $logger)
{
$logger->recordEx($this);
} public function render()
{
return 'whatever you like';
}
} 5 测试异常的使用
Route::get('cex', function () {
throw new CustomException('thrown for caught!!!');
}); 6 检查es和storage/logs下是否存在我们的日志吧
# 我们实现的日志服务可以在应用的任何地方使用,这里只是使用在了异常记录的地方,希望大家能够理解

方式四:有的道友可能吐槽,laravel好好的日志服务不香吗?折腾啥啊?

好的,那就通过laravel自带的日志服务实现和上面同样的功能

config/logging.php
<?php use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Handler\ElasticsearchHandler;
use Elasticsearch\ClientBuilder;
use Monolog\Formatter\ElasticsearchFormatter; ...
'stack' => [
'driver' => 'stack',
// 'channels' => ['single'],
'channels' => ['daily', 'es'],
'ignore_exceptions' => false,
], // 自定义es日志处理器
// 一定要设置es的formatter!!!
'es' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => ElasticsearchHandler::class,
'formatter' => ElasticsearchFormatter::class,
'formatter_with' => [
'index' => 'lara_exception',
'type' => 'lara_exception'
],
'handler_with' => [
'client' => ClientBuilder::create()->setHosts(config('es.hosts'))->build()
]
], # 这样就添加了es作为日志记录的驱动了
# 测试一下吧 比如访问这个路由 查看你的本地文件和es吧
Route::get('cex', function () {
// laravel会将其转换成一个symfony的致命异常
aaa
});

其他方式:想要维持一个高性能的、功能强大的日志服务的话,可以考虑添加一个异步的日志handler,其实monolog也已经提供了开箱即用的handler

更多用法请查看laravel组件的日志部分文档,感兴趣的道友可以自行查看laravel log和monolog的源码

今天没有下集预告,发现错误欢迎指正,感谢!!!

Laravel Exception结合自定义Log服务的使用的更多相关文章

  1. Laravel Exception处理逻辑解析

    Laravel Exception处理逻辑解析 vendor/laravel/framework/src/Illuminate/Foundation/Application.php app首先继承了c ...

  2. 阿里云容器服务--配置自定义路由服务应对DDOS攻击

    阿里云容器服务--配置自定义路由服务应对DDOS攻击 摘要: 容器服务中,除了slb之外,自定义路由服务(基于HAProxy)也可以作为DDOS攻击的一道防线,本文阐述了几种方法来应对普通规模的DDO ...

  3. PHP laravel+thrift+swoole打造微服务框架

    Laravel作为最受欢迎的php web框架一直广受广大互联网公司的喜爱. 笔者也参与过一些由laravel开发的项目.虽然laravel的性能广受诟病但是业界也有一些比较好的解决方案,比如堆机器, ...

  4. Laravel 学习笔记 —— 神奇的服务容器 [转]

    容器,字面上理解就是装东西的东西.常见的变量.对象属性等都可以算是容器.一个容器能够装什么,全部取决于你对该容器的定义.当然,有这样一种容器,它存放的不是文本.数值,而是对象.对象的描述(类.接口)或 ...

  5. Swift开发小技巧--自定义Log

    Swift中的自定义Log OC中有宏的定义,可以定义自己的Log,但是Swif中没有宏的定义,想要实现类似OC中的自定义Log,必须实现以下操作 1.在AppDelegate.swift文件中定义一 ...

  6. SharePoint 2013 中自定义WCF服务

    在使用SharePoint2013的时候,如果其他客户端 API 的组合不足,可以通过自定义 Web 服务扩展 SharePoint.默认情况下,SharePoint 2013 不仅支持创建自定义 A ...

  7. 自定义Attribute 服务端校验 客户端校验

    MVC 自定义Attribute 服务端校验 客户端校验/* GitHub stylesheet for MarkdownPad (http://markdownpad.com) *//* Autho ...

  8. 第四十二篇、自定义Log打印

    1.在Xcode 8出来之后,需要我们去关闭多余的日志信息打印 2.在开发的过程中,打印调试日志是一项比不可少的工程,但是在iOS 10中NSLog打印日志被屏蔽了,就不得不使用自定义Log 3.去掉 ...

  9. Swift中自定义Log打印方法

    系统如何调用super方法 系统默认只会在构造函数中,自动调用super.init()方法,而且是在所写方法的尾部进行调用. 在其他函数中,如何需要调用父类的默认实现,都需要手动去实现. 如果在构造函 ...

随机推荐

  1. CentOS7 更改默认启动桌面(或命令行)模式

    centos7以后是这样的,7以前就是别的版本了 1.systemctl get-default命令获取当前模式 2.systemctl set-default graphical.target 修改 ...

  2. 深度学习调参笔记(trick)

    1. Adam 学习率0.00035真香: 2. SGD + Momentum 学习率应当找到合适区间,一般远大于Adam (取1,2,5,10这类数据): 3. 提前终止,防止过拟合; 4. Ens ...

  3. Java 8新的时间日期库,这二十个案例看完你还学不会算我的!!!

    Java对日期,日历及时间的处理一直以来都饱受诟病,尤其是它决定将java.util.Date定义为可修改的以及将SimpleDateFormat实现成非线程安全的.看来Java已经意识到需要为时间及 ...

  4. myblogplus 第二期 慕舲原创 如何删除官方在你博客内设置的所有广告

    问题描述: 文章下方广告渐多了起来,这也无可厚非,原来只有小小一幅的,毕竟博客园团队很卖力,博客园首页不是在更新吗,博问也在推广(虽然解答者不多,提问者很多) 不过无疑很影响美观,那些可以让他设置,不 ...

  5. 使用Apache的反向代理会影响搜索引擎的收录和排名吗

    http://www.wocaoseo.com/thread-292-1-1.html 百度官方观点:Baiduspider对站点的抓取方式和普通用户访问一样,只要普通用户能访问到的内容,我们就能抓取 ...

  6. 30年技术积累,技术流RTC如何成为视频直播领域的黑马?

    摘要:视频业务链的背后,本质是一张视频处理和分发网络.5G+云+AI时代下,实时音视频必然会步入到一个全新的发展期. 2020年这场肆虐全球的新冠疫情让很多企业重新审视自己对数字化的认识,正如 “大潮 ...

  7. Python采集CSDN博客排行榜数据

    文章目录 前言 网络爬虫 搜索引擎 爬虫应用 谨防违法 爬虫实战 网页分析 编写代码 运行效果 反爬技术 前言 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知 ...

  8. 拾色器,可以取出电脑屏幕的任何颜色,ui以及程序员前端等常用软件,文件很小,300K

    作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985,转载请说明出处. 今天给大家介绍一个小软件,挺实用的,叫做拾色器. 用途:取出电脑屏幕的任意颜色,当你 ...

  9. Idea使用方式——创建类模板

    问题:创建类或接口时,要添加自定义的默认注释,比如版本,时间等.每个类修改显然不符合程序员的思路,有没有办法通过定义模板来实现? 使用Idea模板 Idea可听过创建类模板来实现. 功能路径:Sett ...

  10. 在CG/HLSL中访问着色器的内容

    着色器在Properties代码块中声明 材质球的各种特性.如果你想要在着色器程序中使用这些特性,你需要在CG/HLSL中声明一个变量,这个变量需要与你要使用的特性拥有同样的名字和对的上号的类型.比如 ...