├── LICENSE ├── README.md ├── composer.json └── src ├── Install.php ├── Middleware.php └── config └── plugin └── webman └── log ├── app.php └── middleware.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 webman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # log 2 | webman log plugin 3 | https://www.workerman.net/plugin/61 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webman/log", 3 | "type": "library", 4 | "license": "MIT", 5 | "description": "Webman plugin webman/log", 6 | "require": { 7 | "workerman/webman-framework": "^2.1" 8 | }, 9 | "autoload": { 10 | "psr-4": { 11 | "Webman\\Log\\": "src" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Install.php: -------------------------------------------------------------------------------- 1 | 'config/plugin/webman/log', 13 | ); 14 | 15 | /** 16 | * Install 17 | * @return void 18 | */ 19 | public static function install() 20 | { 21 | static::installByRelation(); 22 | $think_orm_config_path = config_path() . '/thinkorm.php'; 23 | if (!is_file($think_orm_config_path)) { 24 | return; 25 | } 26 | $think_orm_config_content = file_get_contents($think_orm_config_path); 27 | $think_orm_config_content = preg_replace('/\'trigger_sql\' *?=> *?false/', "'trigger_sql' => true", $think_orm_config_content); 28 | file_put_contents($think_orm_config_path, $think_orm_config_content); 29 | } 30 | 31 | /** 32 | * Uninstall 33 | * @return void 34 | */ 35 | public static function uninstall() 36 | { 37 | self::uninstallByRelation(); 38 | $think_orm_config_path = config_path() . '/thinkorm.php'; 39 | if (!is_file($think_orm_config_path)) { 40 | return; 41 | } 42 | $think_orm_config_content = file_get_contents($think_orm_config_path); 43 | $think_orm_config_content = preg_replace('/\'trigger_sql\' *?=> *?true/', "'trigger_sql' => false", $think_orm_config_content); 44 | file_put_contents($think_orm_config_path, $think_orm_config_content); 45 | } 46 | 47 | /** 48 | * installByRelation 49 | * @return void 50 | */ 51 | public static function installByRelation() 52 | { 53 | foreach (static::$pathRelation as $source => $dest) { 54 | if ($pos = strrpos($dest, '/')) { 55 | $parent_dir = base_path().'/'.substr($dest, 0, $pos); 56 | if (!is_dir($parent_dir)) { 57 | mkdir($parent_dir, 0777, true); 58 | } 59 | } 60 | //symlink(__DIR__ . "/$source", base_path()."/$dest"); 61 | copy_dir(__DIR__ . "/$source", base_path()."/$dest"); 62 | echo "Create $dest 63 | "; 64 | } 65 | } 66 | 67 | /** 68 | * uninstallByRelation 69 | * @return void 70 | */ 71 | public static function uninstallByRelation() 72 | { 73 | foreach (static::$pathRelation as $source => $dest) { 74 | $path = base_path()."/$dest"; 75 | if (!is_dir($path) && !is_file($path)) { 76 | continue; 77 | } 78 | echo "Remove $dest 79 | "; 80 | if (is_file($path) || is_link($path)) { 81 | unlink($path); 82 | continue; 83 | } 84 | remove_dir($path); 85 | } 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/Middleware.php: -------------------------------------------------------------------------------- 1 | app,$conf['dontReport']['app'],true)){ 41 | return $next($request); 42 | } 43 | 44 | //跳过配置的path 45 | if(!empty($conf['dontReport']['path']) && is_array($conf['dontReport']['path'])){ 46 | $requestPath=$request->path(); 47 | foreach ($conf['dontReport']['path'] as $_path){ 48 | if(strpos($requestPath,$_path)===0){ 49 | return $next($request); 50 | } 51 | } 52 | } 53 | 54 | //跳过配置的控制器日志记录 55 | if(!empty($conf['dontReport']['controller']) && is_array($conf['dontReport']['controller']) && in_array($request->controller,$conf['dontReport']['controller'],true)){ 56 | return $next($request); 57 | } 58 | 59 | //跳过配置的方法 60 | if(!empty($conf['dontReport']['action']) && is_array($conf['dontReport']['action'])){ 61 | foreach ($conf['dontReport']['action'] as $_action){ 62 | if($_action[0]===$request->controller && $_action[1]===$request->action){ 63 | return $next($request); 64 | } 65 | } 66 | } 67 | 68 | // 请求开始时间 69 | $start_time = microtime(true); 70 | 71 | // 记录ip 请求等信息 72 | $logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/'); 73 | Context::get()->webmanLogs = ''; 74 | 75 | // 清理think-orm的日志 76 | if (class_exists(ThinkDb::class, false) && class_exists(Mysql::class, false)) { 77 | ThinkDb::getDbLog(true); 78 | } 79 | 80 | // 初始化数据库监听 81 | if (!$initialized_db) { 82 | $initialized_db = true; 83 | $this->initDbListen(); 84 | } 85 | 86 | // 初始化think-orm日志监听 87 | if (!$initialized_think_orm) { 88 | try { 89 | ThinkDb::setLog(function ($type, $log) { 90 | Context::get()->webmanLogs = (Context::get()->webmanLogs ?? '') . "[SQL]\t" . trim($log) . PHP_EOL; 91 | }); 92 | } catch (Throwable $e) {} 93 | $initialized_think_orm = true; 94 | } 95 | 96 | // 得到响应 97 | $response = $next($request); 98 | $time_diff = substr((microtime(true) - $start_time) * 1000, 0, 7); 99 | $logs .= " [{$time_diff}ms] [webman/log]" . PHP_EOL; 100 | if ($request->method() === 'POST') { 101 | $logs .= "[POST]\t" . var_export($request->post(), true) . PHP_EOL; 102 | } 103 | $logs = $logs . (Context::get()->webmanLogs ?? ''); 104 | 105 | // think-orm如果被使用,则记录think-orm的日志 106 | if ($loaded_think_db = (class_exists(ThinkDb::class, false) && class_exists(Mysql::class, false))) { 107 | $sql_logs = ThinkDb::getDbLog(true); 108 | if (!empty($sql_logs['sql'])) { 109 | foreach ($sql_logs['sql'] as $sql) { 110 | $logs .= "[SQL]\t" . trim($sql) . PHP_EOL; 111 | } 112 | } 113 | } 114 | 115 | // 判断业务是否出现异常 116 | $exception = null; 117 | if (method_exists($response, 'exception')) { 118 | $exception = $response->exception(); 119 | } 120 | 121 | // 尝试记录异常 122 | $method = 'info'; 123 | if ($exception && config('plugin.webman.log.app.exception.enable', true) && !$this->shouldntReport($exception)) { 124 | $logs .= $exception . PHP_EOL; 125 | $method = 'error'; 126 | } 127 | 128 | // 判断Db是否有未提交的事务 129 | $has_uncommitted_transaction = false; 130 | if (class_exists(Connection::class, false)) { 131 | if ($log = $this->checkDbUncommittedTransaction()) { 132 | $has_uncommitted_transaction = true; 133 | $method = 'error'; 134 | $logs .= $log; 135 | } 136 | } 137 | 138 | // 判断think-orm是否有未提交的事务 139 | if ($loaded_think_db) { 140 | if ($log = $this->checkTpUncommittedTransaction()) { 141 | $has_uncommitted_transaction = true; 142 | $method = 'error'; 143 | $logs .= $log; 144 | } 145 | } 146 | 147 | /** 148 | * 初始化redis监听 149 | * 注意:由于redis是延迟监听,所以第一个请求不会记录redis具体日志 150 | */ 151 | $new_names = $this->tryInitRedisListen(); 152 | foreach ($new_names as $name) { 153 | $logs .= "[Redis]\t[connection:{$name}] ..." . PHP_EOL; 154 | } 155 | 156 | call_user_func([Log::channel(config('plugin.webman.log.app.channel', 'default')), $method], $logs); 157 | 158 | if ($has_uncommitted_transaction) { 159 | throw new RuntimeException('Uncommitted transactions found'); 160 | } 161 | 162 | return $response; 163 | } 164 | 165 | /** 166 | * 初始化数据库日志监听 167 | * 168 | * @return void 169 | */ 170 | protected function initDbListen() 171 | { 172 | if (!class_exists(QueryExecuted::class) || !class_exists(Db::class)) { 173 | return; 174 | } 175 | try { 176 | $capsule = $this->getCapsule(); 177 | if (!$capsule) { 178 | return; 179 | } 180 | $dispatcher = $capsule->getEventDispatcher(); 181 | if (!$dispatcher) { 182 | if (!class_exists(Dispatcher::class)) { 183 | return; 184 | } 185 | $dispatcher = new Dispatcher(new Container); 186 | } 187 | $dispatcher->listen(QueryExecuted::class, function (QueryExecuted $query) { 188 | $sql = trim($query->sql); 189 | if (strtolower($sql) === 'select 1') { 190 | return; 191 | } 192 | $sql = str_replace("?", "%s", $sql); 193 | foreach ($query->bindings as $i => $binding) { 194 | if ($binding instanceof \DateTime) { 195 | $query->bindings[$i] = $binding->format("'Y-m-d H:i:s'"); 196 | } else { 197 | if (is_string($binding)) { 198 | $query->bindings[$i] = "'$binding'"; 199 | } 200 | } 201 | } 202 | $log = $sql; 203 | try { 204 | $log = vsprintf($sql, $query->bindings); 205 | } catch (\Throwable $e) {} 206 | Context::get()->webmanLogs = (Context::get()->webmanLogs ?? '') . "[SQL]\t[connection:{$query->connectionName}] $log [{$query->time} ms]" . PHP_EOL; 207 | }); 208 | $capsule->setEventDispatcher($dispatcher); 209 | } catch (\Throwable $e) { 210 | echo $e; 211 | } 212 | } 213 | 214 | /** 215 | * 尝试初始化redis日志监听 216 | * 217 | * @return array 218 | */ 219 | protected function tryInitRedisListen(): array 220 | { 221 | static $listened; 222 | if (!class_exists(CommandExecuted::class) || !class_exists(Redis::class)) { 223 | return []; 224 | } 225 | $new_names = []; 226 | $listened ??= new \WeakMap(); 227 | // Cache 目前无法监听 日志 228 | try { 229 | foreach (Redis::instance()->connections() ?: [] as $connection) { 230 | /* @var \Illuminate\Redis\Connections\Connection $connection */ 231 | $name = $connection->getName(); 232 | if (isset($listened[$connection])) { 233 | continue; 234 | } 235 | $connection->listen(function (CommandExecuted $command) { 236 | foreach ($command->parameters as &$item) { 237 | if (is_array($item)) { 238 | $item = implode('\', \'', $item); 239 | } 240 | } 241 | Context::get()->webmanLogs = (Context::get()->webmanLogs ?? '') . "[Redis]\t[connection:{$command->connectionName}] Redis::{$command->command}('" . implode('\', \'', $command->parameters) . "') ({$command->time} ms)" . PHP_EOL; 242 | }); 243 | $listened[$connection] = $name; 244 | $new_names[] = $name; 245 | } 246 | } catch (Throwable $e) { 247 | } 248 | return $new_names; 249 | } 250 | 251 | 252 | /** 253 | * 获得Db的Manager 254 | * 255 | * @return \Webman\Database\Manager 256 | */ 257 | protected function getCapsule() 258 | { 259 | static $capsule; 260 | if (!$capsule) { 261 | $reflect = new \ReflectionClass(Db::class); 262 | $property = $reflect->getProperty('instance'); 263 | $property->setAccessible(true); 264 | $capsule = $property->getValue(); 265 | } 266 | return $capsule; 267 | } 268 | 269 | /** 270 | * 检查Db是否有未提交的事务 271 | * 272 | * @return string 273 | */ 274 | protected function checkDbUncommittedTransaction(): string 275 | { 276 | $logs = ''; 277 | $context = Context::get(); 278 | foreach ($context as $item) { 279 | if ($item instanceof Connection) { 280 | if ($item->transactionLevel() > 0) { 281 | $item->rollBack(); 282 | $logs .= "[ERROR]\tUncommitted transaction found and try to rollback" . PHP_EOL; 283 | } 284 | } 285 | } 286 | return $logs; 287 | } 288 | 289 | /** 290 | * 检查think-orm是否有未提交的事务 291 | * 292 | * @return string 293 | */ 294 | protected function checkTpUncommittedTransaction(): string 295 | { 296 | static $property, $manager_instance; 297 | $logs = ''; 298 | $context = Context::get(); 299 | foreach ($context as $item) { 300 | if ($item instanceof PDOConnection) { 301 | if (method_exists($item, 'getPdo')) { 302 | $pdo = $item->getPdo(); 303 | if ($pdo && $pdo->inTransaction()) { 304 | $item->rollBack(); 305 | $logs .= "[ERROR]\tUncommitted transaction found and try to rollback" . PHP_EOL; 306 | } 307 | } 308 | } 309 | } 310 | return $logs; 311 | } 312 | 313 | /** 314 | * 判断是否需要记录异常 315 | * 316 | * @param Throwable $e 317 | * @return bool 318 | */ 319 | protected function shouldntReport($e): bool 320 | { 321 | foreach (config('plugin.webman.log.app.exception.dontReport', []) as $type) { 322 | if ($e instanceof $type) { 323 | return true; 324 | } 325 | } 326 | return false; 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /src/config/plugin/webman/log/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | 'exception' => [ 5 | // 是否记录异常到日志 6 | 'enable' => true, 7 | // 不会记录到日志的异常类 8 | 'dontReport' => [ 9 | support\exception\BusinessException::class 10 | ] 11 | ], 12 | 'dontReport' => [ 13 | 'app' => [], 14 | 'controller' => [], 15 | 'action' => [], 16 | 'path' => [] 17 | ], 18 | 'channel' => 'default' // 日志通道(在config/log.php里配置,默认是default) 19 | ]; 20 | -------------------------------------------------------------------------------- /src/config/plugin/webman/log/middleware.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Webman\Log\Middleware; 16 | 17 | return [ 18 | '@' => [ 19 | Middleware::class 20 | ] 21 | ]; 22 | --------------------------------------------------------------------------------