Browse Source

feat:完善日志工具,方便一键修复bug

FelixYinMac 4 months ago
parent
commit
d25edd04e0

BIN
.DS_Store


+ 1 - 0
.gitignore

@@ -29,3 +29,4 @@ public/admin-assets
 /字典规则.md
 .htaccess
 /owl-admin.code-workspace
+.DS_Store

+ 11 - 0
.vscode/settings.json

@@ -84,6 +84,17 @@
     "terminal.integrated.env.linux": {
         "PATH": "${env:PATH}:${workspaceFolder}/vendor/bin:${workspaceFolder}/script/ansible/bin:${workspaceFolder}/script/bin"
     },
+    "terminal.integrated.defaultProfile.osx": "zsh",
+    "terminal.integrated.profiles.osx": {
+        "zsh": {
+            "path": "zsh",
+            "icon": "terminal",
+            "args": ["-l"]
+        }
+    },
+    "terminal.integrated.env.osx": {
+        "PATH": "${env:PATH}:${workspaceFolder}/vendor/bin:${workspaceFolder}/script/ansible/bin:${workspaceFolder}/script/bin"
+    },
     "git.confirmSync": false,
     "git.fetchOnPull": true,
     "git.showUnpublishedCommitsButton": "always",

+ 67 - 64
app/Admin/Controllers/CoachUserController.php

@@ -12,71 +12,74 @@ use Slowlyo\OwlAdmin\Controllers\AdminController;
  */
 class CoachUserController extends AdminController
 {
-	protected string $serviceName = CoachUserService::class;
+    protected string $serviceName = CoachUserService::class;
 
-	public function list()
-	{
-		$crud = $this->baseCRUD()
-			->filterTogglable(false)
-			->headerToolbar([
-				$this->createButton('dialog'),
-				...$this->baseHeaderToolBar()
-			])
-			->columns([
-				amis()->TableColumn('id', 'ID')->sortable(),
-				amis()->TableColumn('user_id', '用户编号'),
-				amis()->TableColumn('info_record_id', '技师信息记录编号'),
-				amis()->TableColumn('real_auth_record_id', '技师实名认证记录编号'),
-				amis()->TableColumn('qualification_record_id', '技师资质认证记录编号'),
-				amis()->TableColumn('shop_id', '店铺编号'),
-				amis()->TableColumn('level', '技师等级'),
-				amis()->TableColumn('virtual_order', '虚拟订单数')->sortable(),
-				amis()->TableColumn('score', '评分'),
-				amis()->TableColumn('work_status', '工作状态'),
-				amis()->TableColumn('virtual_status', '虚拟状态'),
-				amis()->TableColumn('state', '状态'),
-				amis()->TableColumn('created_at', admin_trans('admin.created_at'))->type('datetime')->sortable(),
-				amis()->TableColumn('updated_at', admin_trans('admin.updated_at'))->type('datetime')->sortable(),
-				$this->rowActions('dialog')
-			]);
+    public function list()
+    {
+        $crud = $this->baseCRUD()
+            ->filterTogglable(false)
+            ->headerToolbar([
+                $this->createButton('dialog'),
+                ...$this->baseHeaderToolBar()
+            ])
+            ->columns([
+                amis()->TableColumn('id', 'ID')->sortable(),
+                amis()->TableColumn('user_id', '用户编号'),
+                amis()->TableColumn('info_record_id', '技师信息记录编号'),
+                amis()->TableColumn('real_auth_record_id', '技师实名认证记录编号'),
+                amis()->TableColumn('qualification_record_id', '技师资质认证记录编号'),
+                amis()->TableColumn('shop_id', '店铺编号'),
+                amis()->TableColumn('level', '技师等级'),
+                amis()->TableColumn('virtual_order', '虚拟订单数')->sortable(),
+                amis()->TableColumn('score', '评分'),
+                amis()->TableColumn('work_status', '工作状态'),
+                amis()->TableColumn('virtual_status', '虚拟状态'),
+                amis()->TableColumn('state', '状态'),
+                amis()->TableColumn('created_at', admin_trans('admin.created_at'))->type('datetime')->sortable(),
+                amis()->TableColumn('updated_at', admin_trans('admin.updated_at'))->type('datetime')->sortable(),
+                $this->rowActions('dialog')
+            ]);
 
-		return $this->baseList($crud);
-	}
+        throw new \Exception('测试');
+        $xx = 1 / 0;
 
-	public function form($isEdit = false)
-	{
-		return $this->baseForm()->body([
-			amis()->TextControl('user_id', '用户编号'),
-			amis()->TextControl('info_record_id', '技师信息记录编号'),
-			amis()->TextControl('real_auth_record_id', '技师实名认证记录编号'),
-			amis()->TextControl('qualification_record_id', '技师资质认证记录编号'),
-			amis()->TextControl('shop_id', '店铺编号'),
-			amis()->TextControl('level', '技师等级'),
-			amis()->TextControl('virtual_order', '虚拟订单数'),
-			amis()->TextControl('score', '评分'),
-			amis()->TextControl('work_status', '工作状态'),
-			amis()->TextControl('virtual_status', '虚拟状态'),
-			amis()->TextControl('state', '状态'),
-		]);
-	}
+        return $this->baseList($crud);
+    }
 
-	public function detail()
-	{
-		return $this->baseDetail()->body([
-			amis()->TextControl('id', 'ID')->static(),
-			amis()->TextControl('user_id', '用户编号')->static(),
-			amis()->TextControl('info_record_id', '技师信息记录编号')->static(),
-			amis()->TextControl('real_auth_record_id', '技师实名认证记录编号')->static(),
-			amis()->TextControl('qualification_record_id', '技师资质认证记录编号')->static(),
-			amis()->TextControl('shop_id', '店铺编号')->static(),
-			amis()->TextControl('level', '技师等级')->static(),
-			amis()->TextControl('virtual_order', '虚拟订单数')->static(),
-			amis()->TextControl('score', '评分')->static(),
-			amis()->TextControl('work_status', '工作状态')->static(),
-			amis()->TextControl('virtual_status', '虚拟状态')->static(),
-			amis()->TextControl('state', '状态')->static(),
-			amis()->TextControl('created_at', admin_trans('admin.created_at'))->static(),
-			amis()->TextControl('updated_at', admin_trans('admin.updated_at'))->static(),
-		]);
-	}
-}
+    public function form($isEdit = false)
+    {
+        return $this->baseForm()->body([
+            amis()->TextControl('user_id', '用户编号'),
+            amis()->TextControl('info_record_id', '技师信息记录编号'),
+            amis()->TextControl('real_auth_record_id', '技师实名认证记录编号'),
+            amis()->TextControl('qualification_record_id', '技师资质认证记录编号'),
+            amis()->TextControl('shop_id', '店铺编号'),
+            amis()->TextControl('level', '技师等级'),
+            amis()->TextControl('virtual_order', '虚拟订单数'),
+            amis()->TextControl('score', '评分'),
+            amis()->TextControl('work_status', '工作状态'),
+            amis()->TextControl('virtual_status', '虚拟状态'),
+            amis()->TextControl('state', '状态'),
+        ]);
+    }
+
+    public function detail()
+    {
+        return $this->baseDetail()->body([
+            amis()->TextControl('id', 'ID')->static(),
+            amis()->TextControl('user_id', '用户编号')->static(),
+            amis()->TextControl('info_record_id', '技师信息记录编号')->static(),
+            amis()->TextControl('real_auth_record_id', '技师实名认证记录编号')->static(),
+            amis()->TextControl('qualification_record_id', '技师资质认证记录编号')->static(),
+            amis()->TextControl('shop_id', '店铺编号')->static(),
+            amis()->TextControl('level', '技师等级')->static(),
+            amis()->TextControl('virtual_order', '虚拟订单数')->static(),
+            amis()->TextControl('score', '评分')->static(),
+            amis()->TextControl('work_status', '工作状态')->static(),
+            amis()->TextControl('virtual_status', '虚拟状态')->static(),
+            amis()->TextControl('state', '状态')->static(),
+            amis()->TextControl('created_at', admin_trans('admin.created_at'))->static(),
+            amis()->TextControl('updated_at', admin_trans('admin.updated_at'))->static(),
+        ]);
+    }
+}

+ 73 - 42
app/Exceptions/Handler.php

@@ -7,61 +7,92 @@ use Exception;
 use Illuminate\Auth\AuthenticationException;
 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
 use Illuminate\Session\TokenMismatchException;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Validation\ValidationException;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Throwable;
 
-class Handler
+class Handler extends ExceptionHandler
 {
-    public function handle($request, Closure $next)
-    {
-        $response = $next($request);
-        if ($response->exception)
-            return $this->render($request, $response->exception);
-        return $response;
-    }
+    /**
+     * The list of the inputs that are never flashed to the session on validation exceptions.
+     *
+     * @var array<int, string>
+     */
+    protected $dontFlash = [
+        'current_password',
+        'password',
+        'password_confirmation',
+    ];
 
-    public function render($request, Throwable $e): JsonResponse
+    /**
+     * Register the exception handling callbacks for the application.
+     */
+    public function register(): void
     {
-        $code = $e->getCode();
-        $message = $e->getMessage();
-        $status = Response::HTTP_INTERNAL_SERVER_ERROR;
+        $this->reportable(function (Throwable $e) {
+            $context = [
+                'exception' => get_class($e),
+                'file' => $e->getFile(),
+                'line' => $e->getLine(),
+                'trace' => $e->getTraceAsString(),
+                'url' => request()->fullUrl(),
+                'method' => request()->method(),
+                'ip' => request()->ip(),
+                'user_agent' => request()->userAgent(),
+            ];
+            
+            if ($e instanceof NotFoundHttpException) {
+                Log::error('Route not found: ' . request()->fullUrl(), $context);
+            } else {
+                Log::error($e->getMessage(), $context);
+            }
+        });
 
-        // CSRF 跨站请求伪造
-        if ($e instanceof TokenMismatchException) {
-            $status = 419;
-        }
+        $this->renderable(function (NotFoundHttpException $e) {
+            return response()->json([
+                'code' => 404,
+                'msg' => 'Route not found: ' . request()->fullUrl()
+            ], Response::HTTP_NOT_FOUND);
+        });
 
-        if ($e instanceof AuthenticationException) {
-            //Unauthenticated.
-            $status = Response::HTTP_UNAUTHORIZED;
-            $message = '登录超时!';
-        }
+        $this->renderable(function (TokenMismatchException $e) {
+            return response()->json([
+                'code' => $e->getCode() ?: 419,
+                'msg' => $e->getMessage()
+            ], 419);
+        });
 
-        // 表单验证
-        if ($e instanceof ValidationException) {
-            $status = Response::HTTP_UNPROCESSABLE_ENTITY;
-//            $status = 200;
-        }
+        $this->renderable(function (AuthenticationException $e) {
+            return response()->json([
+                'code' => $e->getCode() ?: 401,
+                'msg' => '登录超时!'
+            ], Response::HTTP_UNAUTHORIZED);
+        });
 
-        if ($e instanceof ApiException) {
-//            $status = match ($code) {
-//                Response::HTTP_UNPROCESSABLE_ENTITY => 200, // 表单验证
-//                default => $code,
-//            };
-            $status = 200;
-            // 400 错误请求
-            // 401 未授权
-            // 403 禁止访问
-            // 409 数据冲突
-            // 419 CSRF跨域伪造
-            // 422 验证规则
-            // 500 服务器内部错误
+        $this->renderable(function (ValidationException $e) {
+            return response()->json([
+                'code' => $e->getCode() ?: 422,
+                'msg' => $e->getMessage()
+            ], Response::HTTP_UNPROCESSABLE_ENTITY);
+        });
 
-        }
-        return response()->json(['code' => $code, 'msg' => $message], $status);
-    }
+        $this->renderable(function (ApiException $e) {
+            return response()->json([
+                'code' => $e->getCode() ?: 200,
+                'msg' => $e->getMessage()
+            ], 200);
+        });
 
+        $this->renderable(function (Throwable $e) {
+            return response()->json([
+                'code' => $e->getCode() ?: 500,
+                'msg' => $e->getMessage()
+            ], Response::HTTP_INTERNAL_SERVER_ERROR);
+        });
+    }
 }

+ 0 - 60
app/Logging/CustomizeFormatter.php

@@ -1,60 +0,0 @@
-<?php
-
-namespace App\Logging;
-
-use Illuminate\Log\Logger;
-use Monolog\Formatter\LineFormatter;
-use Monolog\LogRecord;
-
-class CustomizeFormatter
-{
-    /**
-     * 自定义给定的日志实例。
-     */
-    public function __invoke(Logger $logger): void
-    {
-        foreach ($logger->getHandlers() as $handler) {
-            $handler->setFormatter(new LineFormatter(
-                "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
-                'Y-m-d H:i:s',
-                true,
-                true
-            ));
-
-            // 添加更严格的过滤器
-            // $handler->pushProcessor(function (LogRecord $record): LogRecord {
-            //     // 1. 允许SQL日志
-            //     if (str_contains($record->message, 'sql:')) {
-            //         return $record;
-            //     }
-
-            //     // 2. 过滤所有来自vendor目录的日志
-            //     if (isset($record->extra['file']) && str_contains($record->extra['file'], '/vendor/')) {
-            //         $record->extra['filtered'] = true;
-            //         return $record;
-            //     }
-
-            //     // 3. 过滤框架的默认日志通道
-            //     if (in_array($record->channel, [
-            //         'laravel',
-            //         'framework',
-            //         'security',
-            //         'request',
-            //         'schedule',
-            //         'queue',
-            //     ])) {
-            //         $record->extra['filtered'] = true;
-            //         return $record;
-            //     }
-
-            //     // 4. 只允许来自app目录的日志
-            //     if (isset($record->extra['file']) && str_contains($record->extra['file'], '/app/')) {
-            //         return $record;
-            //     }
-
-            //     $record->extra['filtered'] = true;
-            //     return $record;
-            // });
-        }
-    }
-}

+ 11 - 5
bootstrap/app.php

@@ -4,7 +4,7 @@ use Illuminate\Foundation\Application;
 use Illuminate\Foundation\Configuration\Exceptions;
 use Illuminate\Foundation\Configuration\Middleware;
 
-return Application::configure(basePath: dirname(__DIR__))
+$app = Application::configure(basePath: dirname(__DIR__))
     ->withRouting(
         web: __DIR__.'/../routes/web.php',
         api: __DIR__.'/../routes/api.php',
@@ -13,10 +13,16 @@ return Application::configure(basePath: dirname(__DIR__))
     )
     ->withMiddleware(function (Middleware $middleware) {
         //
-        $middleware->use([
-            \App\Exceptions\Handler::class
-        ]);
     })
     ->withExceptions(function (Exceptions $exceptions) {
-        //
+        $exceptions->dontReport([
+            //
+        ]);
     })->create();
+
+$app->singleton(
+    Illuminate\Contracts\Debug\ExceptionHandler::class,
+    App\Exceptions\Handler::class
+);
+
+return $app;

+ 45 - 6
config/logging.php

@@ -10,27 +10,45 @@ return [
     'channels' => [
         'stack' => [
             'driver' => 'stack',
-            'channels' => ['daily'],
+            'channels' => ['single'],
             'ignore_exceptions' => false,
         ],
 
+        'single' => [
+            'driver' => 'single',
+            'path' => storage_path('logs/laravel.log'),
+            'level' => env('LOG_LEVEL', 'debug'),
+            'replace_placeholders' => true,
+        ],
+
         'daily' => [
             'driver' => 'daily',
             'path' => storage_path('logs/laravel.log'),
             'level' => env('LOG_LEVEL', 'debug'),
             'days' => 14,
-            'tap' => [App\Logging\CustomizeFormatter::class],
+            'replace_placeholders' => true,
         ],
 
         'slack' => [
             'driver' => 'slack',
             'url' => env('LOG_SLACK_WEBHOOK_URL'),
-            'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
-            'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
+            'username' => 'Laravel Log',
+            'emoji' => ':boom:',
             'level' => env('LOG_LEVEL', 'critical'),
             'replace_placeholders' => true,
         ],
 
+        'papertrail' => [
+            'driver' => 'monolog',
+            'level' => env('LOG_LEVEL', 'debug'),
+            'handler' => env('LOG_PAPERTRAIL_HANDLER', Monolog\Handler\SyslogUdpHandler::class),
+            'handler_with' => [
+                'host' => env('PAPERTRAIL_URL'),
+                'port' => env('PAPERTRAIL_PORT'),
+                'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
+            ],
+        ],
+
         'stderr' => [
             'driver' => 'monolog',
             'level' => env('LOG_LEVEL', 'debug'),
@@ -42,13 +60,34 @@ return [
             'processors' => [PsrLogMessageProcessor::class],
         ],
 
-        'emergency' => [
-            'path' => storage_path('logs/laravel.log'),
+        'syslog' => [
+            'driver' => 'syslog',
+            'level' => env('LOG_LEVEL', 'debug'),
+            'facility' => LOG_USER,
+            'replace_placeholders' => true,
+        ],
+
+        'errorlog' => [
+            'driver' => 'errorlog',
+            'level' => env('LOG_LEVEL', 'debug'),
+            'replace_placeholders' => true,
         ],
 
         'null' => [
             'driver' => 'monolog',
             'handler' => NullHandler::class,
         ],
+
+        'emergency' => [
+            'path' => storage_path('logs/laravel.log'),
+        ],
+
+        'sql' => [
+            'driver' => 'daily',
+            'path' => storage_path('logs/sql.log'),
+            'level' => env('LOG_LEVEL', 'debug'),
+            'days' => 14,
+            'replace_placeholders' => true,
+        ],
     ],
 ];

+ 36 - 40
script/bin/mylog

@@ -3,61 +3,57 @@
 # 交互式命令行模式
 interactive_mode() {
     while true; do
-        read -p "Enter command (c: clear log, p: print log, l: less log, t: tail log, q: quit): " cmd
+        read -p "请输入命令 (c: 清空日志, p: 打印日志, l: 查看日志, t: 实时查看日志, m: 复制错误信息, q: 退出): " cmd
         case $cmd in
             c)
-                echo "Clearing log..."
-                > storage/logs/laravel.log
+                > storage/logs/laravel-$(date +%Y-%m-%d).log
+                echo "日志已清空。"
                 ;;
             p)
-                cat storage/logs/laravel.log
+                errors=$(cat storage/logs/laravel-$(date +%Y-%m-%d).log | grep "ERROR")
+                if [ -n "$errors" ]; then
+                    echo
+                    cat storage/logs/laravel-$(date +%Y-%m-%d).log | grep "ERROR"
+                    echo
+                else
+                    echo "未发现错误信息。"
+                fi
                 ;;
             l)
-                less storage/logs/laravel.log
+                less storage/logs/laravel-$(date +%Y-%m-%d).log
                 ;;
             t)
-                tail -f -n 1000 storage/logs/laravel.log
+                tail -f -n 1000 storage/logs/laravel-$(date +%Y-%m-%d).log
                 ;;
             q)
                 exit 0
                 ;;
+            m)
+                if [ "$(uname)" == "Darwin" ]; then
+                    errors=$(cat storage/logs/laravel-$(date +%Y-%m-%d).log | grep "ERROR")
+                    if [ -n "$errors" ]; then
+                        cat storage/logs/laravel-$(date +%Y-%m-%d).log | grep -m1 "ERROR" | \
+                        awk '{print $0,"\n\n","上面是错误日志,请帮我修复这个bug,并始终用中文语言回答。"}' | pbcopy
+                        echo "已复制错误信息到剪贴板,你可以问AI助手,Ta会给你解决错误。"
+                    else
+                        echo "未发现错误信息。"
+                    fi
+                else
+                    errors=$(cat storage/logs/laravel-$(date +%Y-%m-%d).log | grep "ERROR")
+                    if [ -n "$errors" ]; then
+                        echo "正在复制错误信息到剪贴板..."
+                        cat storage/logs/laravel-$(date +%Y-%m-%d).log | grep "ERROR" | pbcopy
+                        echo "已复制错误信息到剪贴板,你可以问AI助手,他会给你解决错误。"
+                    else
+                        echo "未发现错误信息。"
+                    fi
+                fi
+                ;;
             *)
-                echo "Invalid command. Use 'c' to clear, 'p' to print, 'l' to less, 't' to tail, or 'q' to quit."
+                echo "无效命令。使用 'c' 清空日志, 'p' 打印日志, 'l' 查看日志, 't' 实时查看日志, 'm' 复制错误信息, 'q' 退出。"
                 ;;
         esac
     done
 }
 
-# 处理命令行参数
-if [ $# -eq 0 ]; then
-    interactive_mode
-else
-    case "$1" in
-        -c)
-            echo "Clearing log..."
-            > storage/logs/laravel.log
-            ;;
-        -p)
-            cat storage/logs/laravel.log
-            ;;
-        -l)
-            less storage/logs/laravel.log
-            ;;
-        -t)
-            tail -f -n 1000 storage/logs/laravel.log
-            ;;
-        -h)
-            echo "Usage: $0 [-c|-p|-l|-t|-h]"
-            echo "  -c: clear log"
-            echo "  -p: print log"
-            echo "  -l: less log"
-            echo "  -t: tail log (last 1000 lines, follow mode)"
-            echo "  -h: show this help message"
-            echo "  No arguments: enter interactive mode"
-            ;;
-        *)
-            echo "Invalid option. Use '$0 -h' for help."
-            exit 1
-            ;;
-    esac
-fi
+interactive_mode