From 03f5b8a6c4eaf06d7eceadf550ba6ff7dc2662ca Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 12 Aug 2026 16:29:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=97=A5=E5=BF=97=E4=B8=8E?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E8=B0=83=E8=AF=95=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev.sh | 53 +++++++++++++++++++++++++++++-------------------- utils/logger.py | 27 +++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/dev.sh b/dev.sh index 4062d42..a7ad737 100755 --- a/dev.sh +++ b/dev.sh @@ -40,9 +40,15 @@ YYB_WORKER_IMAGE="${YYB_WORKER_IMAGE:-douyu-yyb-worker:local}" DEV_YYB_WORKER_URL="${DEV_YYB_WORKER_URL:-http://127.0.0.1:${YYB_WORKER_PORT}}" YYB_WORKER_CONTAINER="${YYB_WORKER_CONTAINER:-douyu-yyb-worker-dev}" YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}" +LOG_DIR="$ROOT_DIR/logs" +LOG_DATE="$(date +%Y-%m-%d)" +BACKEND_LOG="$LOG_DIR/backend-$LOG_DATE.log" +FRONTEND_LOG="$LOG_DIR/frontend-$LOG_DATE.log" +WORKER_LOG="$LOG_DIR/yyb-worker-$LOG_DATE.log" BACKEND_PID="" FRONTEND_PID="" +WORKER_LOG_PID="" WORKER_STARTED_BY_DEV=false stop_tree() { @@ -62,6 +68,7 @@ cleanup() { trap - EXIT INT TERM stop_tree "$FRONTEND_PID" stop_tree "$BACKEND_PID" + stop_tree "$WORKER_LOG_PID" if [ "$WORKER_STARTED_BY_DEV" = true ]; then docker stop "$YYB_WORKER_CONTAINER" >/dev/null 2>&1 || true fi @@ -130,7 +137,7 @@ fi BACKEND_PROXY_HOST="${BACKEND_PROXY_HOST:-$(detect_backend_proxy_host)}" BACKEND_PROXY_TARGET="${VITE_BACKEND_TARGET:-http://${BACKEND_PROXY_HOST}:${BACKEND_PORT}}" -mkdir -p data logs +mkdir -p data "$LOG_DIR" mkdir -p "$YYB_WORKER_DATA_DIR" trap cleanup EXIT INT TERM @@ -172,7 +179,14 @@ start_yyb_worker() { done } +stream_yyb_worker_logs() { + # docker logs 默认只输出到终端;持续转存后保留 Worker 的异常和协议脚本输出。 + (docker logs --tail 100 --follow "$YYB_WORKER_CONTAINER" 2>&1 | tee -a "$WORKER_LOG") & + WORKER_LOG_PID=$! +} + start_yyb_worker +stream_yyb_worker_logs echo "正在启动本地 MySQL..." MYSQL_IMAGE="$MYSQL_IMAGE" \ @@ -203,35 +217,32 @@ echo " 前端: http://localhost:${FRONTEND_PORT}" echo " API 代理: ${BACKEND_PROXY_TARGET}" echo " MySQL: ${DB_HOST}:${DB_PORT}/${DB_NAME}" echo " YYB Worker: ${DEV_YYB_WORKER_URL}" +echo " 后端日志: ${BACKEND_LOG}" +echo " 前端日志: ${FRONTEND_LOG}" +echo " Worker 日志: ${WORKER_LOG}" echo " 退出: Ctrl+C" echo "==============================" echo "" -DB_HOST="$DB_HOST" \ -DB_PORT="$DB_PORT" \ -DB_NAME="$DB_NAME" \ -DB_USER="$DB_USER" \ -DB_PASSWORD="$DB_PASSWORD" \ -DB_POOL_SIZE="$DB_POOL_SIZE" \ -DB_MAX_OVERFLOW="$DB_MAX_OVERFLOW" \ -DB_POOL_TIMEOUT="$DB_POOL_TIMEOUT" \ -DB_POOL_RECYCLE="$DB_POOL_RECYCLE" \ -YYB_WORKER_URL="$DEV_YYB_WORKER_URL" \ -YYB_WORKER_KEY="$YYB_WORKER_KEY" \ -LOG_LEVEL="$LOG_LEVEL" \ -uv run uvicorn web.backend.main:app \ - --host "$BACKEND_HOST" \ - --port "$BACKEND_PORT" \ - --reload \ - --reload-dir core \ - --reload-dir utils \ - --reload-dir web/backend & +( + export DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD + export DB_POOL_SIZE DB_MAX_OVERFLOW DB_POOL_TIMEOUT DB_POOL_RECYCLE + export YYB_WORKER_KEY LOG_LEVEL + export YYB_WORKER_URL="$DEV_YYB_WORKER_URL" + uv run uvicorn web.backend.main:app \ + --host "$BACKEND_HOST" \ + --port "$BACKEND_PORT" \ + --reload \ + --reload-dir core \ + --reload-dir utils \ + --reload-dir web/backend 2>&1 | tee -a "$BACKEND_LOG" +) & BACKEND_PID=$! ( cd web/frontend VITE_BACKEND_TARGET="$BACKEND_PROXY_TARGET" \ - npm run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" + npm run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" 2>&1 | tee -a "$FRONTEND_LOG" ) & FRONTEND_PID=$! diff --git a/utils/logger.py b/utils/logger.py index 8c32806..dff8f86 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -2,7 +2,9 @@ import os import sys +import logging from datetime import date +from logging.handlers import TimedRotatingFileHandler from pathlib import Path from loguru import logger @@ -24,6 +26,30 @@ def _should_rotate(message, file): return False +def _configure_standard_logging(level: str, log_dir: Path) -> None: + """将 Uvicorn/FastAPI 的标准日志同步到控制台和文件。""" + log_level = getattr(logging, level.upper(), logging.INFO) + formatter = logging.Formatter( + "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s" + ) + + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(log_level) + console_handler.setFormatter(formatter) + + file_handler = TimedRotatingFileHandler( + log_dir / "server.log", when="midnight", backupCount=7, encoding="utf-8" + ) + file_handler.setLevel(log_level) + file_handler.setFormatter(formatter) + + for name in ("uvicorn", "uvicorn.error", "uvicorn.access", "fastapi", "starlette"): + standard_logger = logging.getLogger(name) + standard_logger.handlers = [console_handler, file_handler] + standard_logger.setLevel(log_level) + standard_logger.propagate = False + + def setup_logger(level: str = "INFO", log_dir: str = None, log_file: str = None) -> None: """ 配置日志 @@ -52,6 +78,7 @@ def setup_logger(level: str = "INFO", log_dir: str = None, log_file: str = None) # 新接口:按目录 + 日期命名 dir_path = Path(log_dir) dir_path.mkdir(parents=True, exist_ok=True) + _configure_standard_logging(level, dir_path) file_path = str(dir_path / "app-{time:YYYY-MM-DD}.log") elif log_file: # 兼容旧接口