如何及时定位到非必现问题?接口慢查询和接口报错的及时追踪。

如何及时定位到非必现问题?接口慢查询和接口报错的及时追踪。
非常高兴能参加【CSDN1024程序员节】的活动

文章目录

之前写过一篇类似的文章,反馈还不错,本篇做进一步升级,又添加了几个应用场景。

  1. 接口报错的详细信息(请求参数、请求体、响应时间、错误信息等)实时同步到钉钉群
  2. 慢查询接口信息同步到钉钉群(支持添加接口白名单)
  3. 非必现问题错误信息同步到钉钉群,及时追踪定位

基于上述场景,抛转引玉,大家可以借鉴这个思路,在评论区讨论更好的实现方式和应用场景。

首先,说明一下我的代码语言:基于PHP的Laravel框架

下面展示核心代码,关键代码会注释,无关代码会省略。

修改日志配置

  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
                ]
            ]);
    }
}

效果图:
如何及时定位到非必现问题?接口慢查询和接口报错的及时追踪。

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

实现慢查询的核心思路:

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");
            }
        }
    }
}

效果图

如何及时定位到非必现问题?接口慢查询和接口报错的及时追踪。

排查非必现的问题

原理:我们可以在出问题的代码段中,通过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();
        .
        .
        .
    }

效果图
如何及时定位到非必现问题?接口慢查询和接口报错的及时追踪。

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

总结

以上就是我基于钉钉和laravel的服务搭建的实时接口报警系统。
能够实现接口慢查询和接口报错的及时追踪。

欢迎大家在评论区赐教

上一篇:Python 使用skimage实现将彩色图像转换为灰度图像并保存


下一篇:2021-10-20