如何及时定位到非必现问题?

简介: 自定义channel;区分哪个环境中使用自定义的channel;使用优雅的 monolog 驱动

修改日志配置


  1. 自定义channel
  2. 区分哪个环境中使用自定义的channel
  3. 使用优雅的 monolog 驱动


<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            //测试环境除了使用daily保存每天日志到logs/laravel.log,还使用’dingding‘channel
            'channels' => env("APP_ENV") == 'test' ? ['daily', 'dingding'] : ['daily'],
            'ignore_exceptions' => false,
        ],
        //配置钉钉 驱动选择 monolog 
        'dingding' => [
            'driver' => 'monolog',
            'level' => 'error',
            'handler' => \App\Handler\DingdingLogHandler::class,  //自定义handler
        ],
        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 14,
        ],
        .
        .
        .
    ],
];


上面不重要的代码使用3个竖向排列的.省略显示,下面的代码段非重要的也用3个竖向排列的.省略。


自定义钉钉Handler


  1. 我们可以很方便的通过 Illuminate\Support\Facades\Request 获得请求链接,请求参数,请求头
  2. 错误信息通过外部传入,默认的Error报错都会运行到send()方法中


<?php
namespace App\Handler;
use App\Library\CurlRequest;
use App\Library\Utility;
use Monolog\Logger;
use Monolog\Handler;
class DingdingLogHandler extends Handler\AbstractProcessingHandler
{
    private $apiKey;
    private $channel;
    public function __construct(
        $level = Logger::ERROR,
        bool $bubble = true
    ) {
        parent::__construct($level, $bubble);
    }
    protected function write(array $record): void
    {
        $this->send($record['formatted']);
    }
    protected function send(string $message): void
    {
        $microSecond = Utility::getMicroSecond();
        $key = "xxxx";
        $hashString = hash_hmac("sha256", $microSecond ."\n" . $key, $key, true);
        $sign = urlencode(base64_encode($hashString));
        CurlRequest::post("https://oapi.dingtalk.com/robot/send?access_token=xxxxx&timestamp=".$microSecond."&sign=".$sign,
            [
                "msgtype" => "text",
                "at" => [
                    "atMobiles" => [
                        "xxxx",
                        "xxxx"
                    ]
                ],
                "text" => [
                       "content" => "请求链接:\n" . Request::path() . "\n请求参数:\n" . \GuzzleHttp\json_encode(Request::toArray()) . "\n请求头:\n" . \GuzzleHttp\json_encode(Request::header()) . "\n错误信息:\n" . $message
                ]
            ]);
    }
}


效果图:


微信图片_20221111215100.jpg

上述两段代码是实现错误信息同步到钉钉群的核心代码,下面介绍各种场景的实现思路:


实现慢查询的核心思路:


response中间件:



  1. 这是网络请求和响应的中间件,左右的网络请求都会到经过这个中间件的处理
  2. 耗时操作的计算只在测试环境中部署,上线时去掉。(或者通过生产环境或者开发环境的动态变量做判断,这种实现应该更好)
  3. 有些请求三方的接口不再我们优化的范围内,为了避免频繁接口提醒,可以设置白名单
  4. 慢查询之所以有Log::info()和Log::error()的区别是:前者只存储到日志;后者不仅存储到日志,还同步信息到钉钉(我们定义钉钉channel的日志级别为error)


<?php
namespace App\Http\Middleware;
use App\Library\Utility;
use App\Model\ErrorCode;
use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
class ApiResponse
{
    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $requestMicroSecond = Utility::getMicroSecond();
        $response = $next($request);
        if (!is_array($response->original) && (int)($response->original) != 0) {
            $errCode = (int)$response->original;
            //TODO 上线删除 避免资源浪费
            self::calcTime($requestMicroSecond);
            return response()->json(['ret' => $errCode, 'msg' => ErrorCode::getErrorMsg($errCode)])
                ->header("Timestamp", Utility::getMicroSecond());
        } else {
            $ret = ['ret' => 0, 'msg' => ErrorCode::getErrorMsg(0)];
            if (!empty($response->original) && is_array($response->original)) {
                $ret['data'] = $response->original;
            } else if ($response->original != Null) {
                echo $response->original;
                exit;
            }
            //TODO 上线删除
            self::calcTime($requestMicroSecond);
            return response()->json($ret)->header("Timestamp", Utility::getMicroSecond());
        }
    }
    function calcTime($requestTime)
    {
        //白名单
        $exceptApis = [
            'api/pay/order/status',
            'api/pay/order/repair',
            'api/log/token/get',
        ];
        $responseTime = Utility::getMicroSecond();
        $consumeTime = $responseTime - $requestTime;
        if ($consumeTime >= 1000) {
            if (in_array(Request::path(), $exceptApis)) {
                //只记录 不发到钉钉报警
                Log::info("耗时操作(调用三方):" . $consumeTime . "ms");
            } else {
                Log::error("耗时操作:" . $consumeTime . "ms");
            }
        }
    }
}


效果图


微信图片_20221111215105.jpg


排查非必现的问题


原理:我们可以在出问题的代码段中,通过Log::error()打印错误日志,当被触发是就会同步到钉钉群消息中。

比如我们的错误场景是: 安卓客户端发短信验证码登录的时候偶尔会出现验证码验证失败的问题,客户端确信给服务端传了正确的验证码,服务端反击说:你如果传了正常的验证码不可能验证不通过。


这种扯皮是没有用的,加上这种是非必现问题,打印Log定位问题吧,要爬好久log,且不能及时知道什么时候出错的,所以同步错误信息到钉钉群是一个非常典型的应用场景。


上代码


public function validateMsg(Request $request)
    {
        $authCode = new AuthCode($request->phone);
        $code = $authCode->get();
        if (empty($code) || $code != $request->code) {
            Log::error("短信验证码校验问题: AuthCode:" . $code . " 传入code:" . $request->code);
            return ErrorCode::TYPE_CODE_INCORRECT;
        }
        $authCode->delCode();
        .
        .
        .
    }


效果图


微信图片_20221111215110.jpg

最终发现是服务端的问题,本地的authCode为空,被清理了,导致校验失败。


相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
相关文章
|
Java Maven 开发者
入职必会-开发环境搭建14-IDEA配置Maven
在 IDEA 中配置 Maven 可以帮助开发者更方便地管理项目依赖、构建项目和部署应用程序。要在 IDEA 中配置 Maven,可以按照以下步骤进行。
551 1
入职必会-开发环境搭建14-IDEA配置Maven
|
关系型数据库 MySQL 数据库
MySQL数据库事务的概念和应用场景
MySQL数据库事务的概念和应用场景
|
设计模式 Java
常用设计模式介绍~~~ Java实现 【概念+案例+代码】
文章提供了一份常用设计模式的全面介绍,包括创建型模式、结构型模式和行为型模式。每种设计模式都有详细的概念讲解、案例说明、代码实例以及运行截图。作者通过这些模式的介绍,旨在帮助读者更好地理解源码、编写更优雅的代码,并进行系统重构。同时,文章还提供了GitHub上的源码地址,方便读者直接访问和学习。
常用设计模式介绍~~~ Java实现 【概念+案例+代码】
|
Java Spring
自定义注解+AOP
自定义注解+AOP
260 1
|
存储 Java Spring
Spring Boot中的配置中心实现
Spring Boot中的配置中心实现
|
Java
深入理解 Java 8 函数式接口:定义、用法与示例详解
深入理解 Java 8 函数式接口:定义、用法与示例详解
899 2
Java并发编程:理解并使用Future和Callable接口
【2月更文挑战第25天】 在Java中,多线程编程是一个重要的概念,它允许我们同时执行多个任务。然而,有时候我们需要等待一个或多个线程完成,然后才能继续执行其他任务。这就需要使用到Future和Callable接口。本文将深入探讨这两个接口的用法,以及它们如何帮助我们更好地管理多线程。
|
存储 自然语言处理 搜索推荐
mysql中utf8、utf8mb4和utf8mb4_unicode_ci、utf8mb4_general_ci
mysql中utf8、utf8mb4和utf8mb4_unicode_ci、utf8mb4_general_ci
729 0
|
存储 缓存 Java
从浏览器发送请求给SpringBoot后端时,是如何准确找到哪个接口的?(下篇)
从浏览器发送请求给SpringBoot后端时,是如何准确找到哪个接口的?(下篇)
603 1
|
前端开发 Java API
如何使用线上环境进行本地代码调试
如何使用线上环境进行本地代码调试
802 0

热门文章

最新文章