修改日志配置
- 自定义channel
- 区分哪个环境中使用自定义的channel
- 使用优雅的 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
- 我们可以很方便的通过 Illuminate\Support\Facades\Request 获得请求链接,请求参数,请求头
- 错误信息通过外部传入,默认的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×tamp=".$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中间件:
- 这是网络请求和响应的中间件,左右的网络请求都会到经过这个中间件的处理
- 耗时操作的计算只在测试环境中部署,上线时去掉。(或者通过生产环境或者开发环境的动态变量做判断,这种实现应该更好)
- 有些请求三方的接口不再我们优化的范围内,为了避免频繁接口提醒,可以设置白名单
- 慢查询之所以有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为空,被清理了,导致校验失败。