multi-store/vendor/webman/log/src/Middleware.php
2024-05-31 09:27:37 +08:00

347 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Webman\Log;
use Illuminate\Container\Container;
use Illuminate\Database\Connection;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Events\Dispatcher;
use Illuminate\Redis\Events\CommandExecuted;
use support\Context;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
use Throwable;
use RuntimeException;
use support\Db;
use support\Log;
use support\Redis;
use think\db\connector\Mysql;
use think\DbManager;
use think\facade\Db as ThinkDb;
use think\Container as ThinkContainer;
class Middleware implements MiddlewareInterface
{
/**
* @param Request $request
* @param callable $next
* @return Response
*/
public function process(Request $request, callable $next): Response
{
static $initialized_db, $initialized_think_orm;
$conf=config('plugin.webman.log.app');
//跳过配置的模块
if(!empty($conf['dontReport']['app']) && is_array($conf['dontReport']['app']) && in_array($request->app,$conf['dontReport']['app'],true)){
return $next($request);
}
//跳过配置的path
if(!empty($conf['dontReport']['path']) && is_array($conf['dontReport']['path'])){
$requestPath=$request->path();
foreach ($conf['dontReport']['path'] as $_path){
if(strpos($requestPath,$_path)===0){
return $next($request);
}
}
}
//跳过配置的控制器日志记录
if(!empty($conf['dontReport']['controller']) && is_array($conf['dontReport']['controller']) && in_array($request->controller,$conf['dontReport']['controller'],true)){
return $next($request);
}
//跳过配置的方法
if(!empty($conf['dontReport']['action']) && is_array($conf['dontReport']['action'])){
foreach ($conf['dontReport']['action'] as $_action){
if($_action[0]===$request->controller && $_action[1]===$request->action){
return $next($request);
}
}
}
// 请求开始时间
$start_time = microtime(true);
// 记录ip 请求等信息
$logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/');
Context::get()->webmanLogs = '';
// 清理think-orm的日志
if (class_exists(ThinkDb::class, false) && class_exists(Mysql::class, false)) {
ThinkDb::getDbLog(true);
}
// 初始化数据库监听
if (!$initialized_db) {
$initialized_db = true;
$this->initDbListen();
}
// 初始化think-orm日志监听
if (!$initialized_think_orm) {
try {
ThinkDb::setLog(function ($type, $log) {
Context::get()->webmanLogs = (Context::get()->webmanLogs ?? '') . "[SQL]\t" . trim($log) . PHP_EOL;
});
} catch (Throwable $e) {}
$initialized_think_orm = true;
}
// 得到响应
$response = $next($request);
$time_diff = substr((microtime(true) - $start_time) * 1000, 0, 7);
$logs .= " [{$time_diff}ms] [webman/log]" . PHP_EOL;
if ($request->method() === 'POST') {
$logs .= "[POST]\t" . var_export($request->post(), true) . PHP_EOL;
}
$logs = $logs . (Context::get()->webmanLogs ?? '');
// think-orm如果被使用则记录think-orm的日志
if ($loaded_think_db = (class_exists(ThinkDb::class, false) && class_exists(Mysql::class, false))) {
$sql_logs = ThinkDb::getDbLog(true);
if (!empty($sql_logs['sql'])) {
foreach ($sql_logs['sql'] as $sql) {
$logs .= "[SQL]\t" . trim($sql) . PHP_EOL;
}
}
}
// 判断业务是否出现异常
$exception = null;
if (method_exists($response, 'exception')) {
$exception = $response->exception();
}
// 尝试记录异常
$method = 'info';
if ($exception && config('plugin.webman.log.app.exception.enable', true) && !$this->shouldntReport($exception)) {
$logs .= $exception . PHP_EOL;
$method = 'error';
}
// 判断Db是否有未提交的事务
$has_uncommitted_transaction = false;
if (class_exists(Connection::class, false)) {
if ($log = $this->checkDbUncommittedTransaction()) {
$has_uncommitted_transaction = true;
$method = 'error';
$logs .= $log;
}
}
// 判断think-orm是否有未提交的事务
if ($loaded_think_db) {
if ($log = $this->checkTpUncommittedTransaction()) {
$has_uncommitted_transaction = true;
$method = 'error';
$logs .= $log;
}
}
/**
* 初始化redis监听
* 注意由于redis是延迟监听所以第一个请求不会记录redis具体日志
*/
$new_names = $this->tryInitRedisListen();
foreach ($new_names as $name) {
$logs .= "[Redis]\t[connection:{$name}] ..." . PHP_EOL;
}
call_user_func([Log::class, $method], $logs);
if ($has_uncommitted_transaction) {
throw new RuntimeException('Uncommitted transactions found');
}
return $response;
}
/**
* 初始化数据库日志监听
*
* @return void
*/
protected function initDbListen()
{
if (!class_exists(QueryExecuted::class)) {
return;
}
try {
$capsule = $this->getCapsule();
if (!$capsule) {
return;
}
$dispatcher = $capsule->getEventDispatcher();
if (!$dispatcher) {
$dispatcher = new Dispatcher(new Container);
}
$dispatcher->listen(QueryExecuted::class, function (QueryExecuted $query) {
$sql = trim($query->sql);
if (strtolower($sql) === 'select 1') {
return;
}
$sql = str_replace("?", "%s", $sql);
foreach ($query->bindings as $i => $binding) {
if ($binding instanceof \DateTime) {
$query->bindings[$i] = $binding->format("'Y-m-d H:i:s'");
} else {
if (is_string($binding)) {
$query->bindings[$i] = "'$binding'";
}
}
}
$log = $sql;
try {
$log = vsprintf($sql, $query->bindings);
} catch (\Throwable $e) {}
Context::get()->webmanLogs = (Context::get()->webmanLogs ?? '') . "[SQL]\t[connection:{$query->connectionName}] $log [{$query->time} ms]" . PHP_EOL;
});
$capsule->setEventDispatcher($dispatcher);
} catch (\Throwable $e) {
echo $e;
}
}
/**
* 尝试初始化redis日志监听
*
* @return array
*/
protected function tryInitRedisListen(): array
{
static $listened_names = [];
if (!class_exists(CommandExecuted::class)) {
return [];
}
$new_names = [];
try {
foreach (Redis::instance()->connections() ?: [] as $connection) {
/* @var \Illuminate\Redis\Connections\Connection $connection */
$name = $connection->getName();
if (isset($listened_names[$name])) {
continue;
}
$connection->listen(function (CommandExecuted $command) {
foreach ($command->parameters as &$item) {
if (is_array($item)) {
$item = implode('\', \'', $item);
}
}
Context::get()->webmanLogs = (Context::get()->webmanLogs ?? '') . "[Redis]\t[connection:{$command->connectionName}] Redis::{$command->command}('" . implode('\', \'', $command->parameters) . "') ({$command->time} ms)" . PHP_EOL;
});
$listened_names[$name] = $name;
$new_names[] = $name;
}
} catch (Throwable $e) {
echo $e;
}
return $new_names;
}
/**
* 获得Db的Manager
*
* @return mixed
*/
protected function getCapsule()
{
static $capsule;
if (!$capsule) {
$reflect = new \ReflectionClass(Db::class);
$property = $reflect->getProperty('instance');
$property->setAccessible(true);
$capsule = $property->getValue();
}
return $capsule;
}
/**
* 检查Db是否有未提交的事务
*
* @return string
*/
protected function checkDbUncommittedTransaction(): string
{
$logs = '';
try {
foreach ($this->getCapsule()->getDatabaseManager()->getConnections() as $connection) {
/* @var \Illuminate\Database\MySqlConnection $connection * */
if (\in_array($connection->getConfig('driver'), ['mysql', 'pgsql', 'sqlite', 'sqlsrv'])) {
$pdo = $connection->getPdo();
if ($pdo && $pdo->inTransaction()) {
$connection->rollBack();
$logs .= "[ERROR]\tUncommitted transaction found and try to rollback" . PHP_EOL;
}
}
}
} catch (Throwable $e) {
echo $e;
}
return $logs;
}
/**
* 检查think-orm是否有未提交的事务
*
* @return string
*/
protected function checkTpUncommittedTransaction(): string
{
static $property, $manager_instance;
$logs = '';
try {
if (!$property) {
if (class_exists(ThinkContainer::class, false)) {
$manager_instance = ThinkContainer::getInstance()->make(DbManager::class);
} else {
$reflect = new \ReflectionClass(ThinkDb::class);
$property = $reflect->getProperty('instance');
$property->setAccessible(true);
$manager_instance = $property->getValue();
}
$reflect = new \ReflectionClass($manager_instance);
$property = $reflect->getProperty('instance');
$property->setAccessible(true);
}
$instances = $property->getValue($manager_instance);
foreach ($instances as $connection) {
/* @var \think\db\connector\Mysql $connection */
if (method_exists($connection, 'getPdo')) {
$pdo = $connection->getPdo();
if ($pdo && $pdo->inTransaction()) {
$connection->rollBack();
$logs .= "[ERROR]\tUncommitted transaction found and try to rollback" . PHP_EOL;
}
}
}
} catch (Throwable $e) {
echo $e;
}
return $logs;
}
/**
* 判断是否需要记录异常
*
* @param Throwable $e
* @return bool
*/
protected function shouldntReport($e): bool
{
foreach (config('plugin.webman.log.app.exception.dontReport', []) as $type) {
if ($e instanceof $type) {
return true;
}
}
return false;
}
}