zoukankan      html  css  js  c++  java
  • Laravel Exception结合自定义Log服务的使用

    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中
        # 执行的是AppExceptionsHandler的report方法
        $this->app[ExceptionHandler::class]->report($e);
    }
    
    # 跳转到AppExceptionsHandler的report方法
    public function report(Exception $exception)
    {	
        # 继续跳转到父类的report方法
        parent::report($exception);
    }
    
    # 只看核心代码
    IlluminateFoundationExceptionsHandler::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服务
        # IlluminateLogLogManager实例
        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阶段

    下面给出简要介绍

    IlluminateFoundationBootstrapHandleExceptions::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 扩展AppExceptionsHandler类
    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本身具有替换性和通用性,其他框架稍加改动也可以使用

    laravel中的服务实现是可以快速切换的,这里使用最有代表性的monolog作为我们本次日志服务的实现

    1 创建契约
    <?php
    
    namespace AppContracts;
    
    interface ExceptionLog
    {      
        // 记录异常
        public function recordEx(Exception $e);
    }
    
    2 创建日志服务 并简单介绍monolog使用
    # 关于monolog的更多使用方法请查看官方文档 https://github.com/Seldaek/monolog
    <?php
    
    namespace AppServicesLogs;
    
    use AppContractsExceptionLog;
    use MonologLogger;
    use MonologHandlerStreamHandler;
    use MonologHandlerRotatingFileHandler;
    use MonologHandlerElasticsearchHandler;
    use ElasticsearchClientBuilder;
    
    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 AppProviders;
    
    use IlluminateSupportServiceProvider;
    use AppContractsExceptionLog;
    use AppServicesLogsMonoException;
    use IlluminateContractsSupportDeferrableProvider;
    
    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
    ...
    AppProvidersAuthServiceProvider::class,
    // AppProvidersBroadcastServiceProvider::class,
    AppProvidersEventServiceProvider::class,
    AppProvidersRouteServiceProvider::class,
    // 注册自定义日志服务
    AppProvidersLogServiceProvider::class,
    
    
    4 创建自定义异常 并在其中使用自定义的日志服务
    <?php
    
    namespace AppExceptions;
    
    use Exception;
    use AppContractsExceptionLog;
    
    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 MonologHandlerNullHandler;
    use MonologHandlerStreamHandler;
    use MonologHandlerSyslogUdpHandler;
    use MonologHandlerElasticsearchHandler;
    use ElasticsearchClientBuilder;
    use MonologFormatterElasticsearchFormatter;
    
    ...
    '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 () {
        // 手动触发一个parse error
        // laravel会将其转换成一个symfony的致命异常
        aaa
    });
    

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

    更多用法请查看laravel组件的日志部分文档,感兴趣的道友可以自行查看laravel log和monolog的源码,两者都提供自定义日志处理器,自由发挥吧各位

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

  • 相关阅读:
    java 取汉字首字母
    详解intellij idea搭建SSM框架(spring+maven+mybatis+mysql+junit)(下)
    详解intellij idea搭建SSM框架(spring+maven+mybatis+mysql+junit)(上)
    IntelliJ IDEA maven项目new里没有package
    IntelliJ IDEA上操作GitHub
    IntelliJ IDEA部署tomcat时Edit Configuration Deployment无artifact选项
    Java开发需掌握的常用Linux命令(持续更新)
    Spring JDBC 示例
    java 获取日期的几天前,几个月前和几年前
    Anaconda3(0)环境基本使用
  • 原文地址:https://www.cnblogs.com/alwayslinger/p/13718504.html
Copyright © 2011-2022 走看看