diff --git a/backend/api/main.py b/backend/api/main.py
index 21b6b05..41db2a7 100644
--- a/backend/api/main.py
+++ b/backend/api/main.py
@@ -114,6 +114,16 @@ def setup_logging():
# 让 handler 自己按组筛选(error/warning/info),这里只需要放宽到 INFO
redis_handler.setLevel(logging.INFO)
root_logger.addHandler(redis_handler)
+
+ # 诊断:启动时快速检测一次 Redis 可用性(失败不影响运行)
+ try:
+ client = redis_handler._get_redis() # noqa: SLF001(仅用于诊断)
+ if client is None:
+ logging.getLogger(__name__).warning(f"⚠ Redis 日志写入未启用。REDIS_URL={redis_url}")
+ else:
+ logging.getLogger(__name__).info(f"✓ Redis 日志写入已启用。REDIS_URL={redis_url}")
+ except Exception:
+ pass
except Exception:
pass
diff --git a/backend/api/routes/system.py b/backend/api/routes/system.py
index 658e52c..774be68 100644
--- a/backend/api/routes/system.py
+++ b/backend/api/routes/system.py
@@ -125,6 +125,76 @@ class LogsConfigUpdate(BaseModel):
include_debug_in_info: Optional[bool] = None
+def _beijing_time_str() -> str:
+ from datetime import datetime, timezone, timedelta
+
+ beijing_tz = timezone(timedelta(hours=8))
+ return datetime.now(tz=beijing_tz).strftime("%Y-%m-%d %H:%M:%S")
+
+
+@router.post("/logs/test-write")
+async def logs_test_write(
+ x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token"),
+) -> Dict[str, Any]:
+ """
+ 写入 3 条测试日志到 Redis(error/warning/info),用于验证“是否写入到同一台 Redis、同一组 key”。
+ """
+ _require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
+
+ client = _get_redis_client_for_logs()
+ if client is None:
+ raise HTTPException(status_code=503, detail="Redis 不可用,无法写入测试日志")
+
+ cfg = _read_logs_config(client)
+ now_ms = int(__import__("time").time() * 1000)
+ time_str = _beijing_time_str()
+
+ entries = {
+ "error": {"level": "ERROR", "message": f"[TEST] backend 写入 error 测试日志 @ {time_str}"},
+ "warning": {"level": "WARNING", "message": f"[TEST] backend 写入 warning 测试日志 @ {time_str}"},
+ "info": {"level": "INFO", "message": f"[TEST] backend 写入 info 测试日志 @ {time_str}"},
+ }
+
+ day = _beijing_yyyymmdd()
+ stats_prefix = _logs_stats_prefix()
+
+ pipe = client.pipeline()
+ for g in LOG_GROUPS:
+ key = _logs_key_for_group(g)
+ max_len = int(cfg["max_len"][g])
+ entry = {
+ "ts": now_ms,
+ "time": time_str,
+ "service": "backend",
+ "level": entries[g]["level"],
+ "logger": "api.routes.system",
+ "message": entries[g]["message"],
+ "hostname": os.getenv("HOSTNAME", ""),
+ "signature": f"backend|{entries[g]['level']}|test|{entries[g]['message']}",
+ "count": 1,
+ }
+ pipe.lpush(key, json.dumps(entry, ensure_ascii=False))
+ if max_len > 0:
+ pipe.ltrim(key, 0, max_len - 1)
+ pipe.incr(f"{stats_prefix}:{day}:{g}", 1)
+ pipe.expire(f"{stats_prefix}:{day}:{g}", 14 * 24 * 3600)
+
+ pipe.execute()
+
+ # 返回写入后的 LLEN,便于你确认
+ pipe2 = client.pipeline()
+ for g in LOG_GROUPS:
+ pipe2.llen(_logs_key_for_group(g))
+ llens = pipe2.execute()
+
+ return {
+ "message": "ok",
+ "keys": {g: _logs_key_for_group(g) for g in LOG_GROUPS},
+ "llen": {g: int(llens[i] or 0) for i, g in enumerate(LOG_GROUPS)},
+ "note": "如果你在前端仍看不到,说明前端请求的后端实例/Redis key/筛选条件不一致。",
+ }
+
+
def _get_redis_client_for_logs():
"""
获取 Redis 客户端(优先复用 config_manager 的连接;失败则自行创建)。
diff --git a/frontend/src/components/LogMonitor.jsx b/frontend/src/components/LogMonitor.jsx
index 6bb42a1..c601235 100644
--- a/frontend/src/components/LogMonitor.jsx
+++ b/frontend/src/components/LogMonitor.jsx
@@ -113,6 +113,16 @@ export default function LogMonitor() {
}
}
+ const writeTest = async () => {
+ setError('')
+ try {
+ await api.writeLogsTest()
+ await load()
+ } catch (e) {
+ setError(e?.message || '写入测试日志失败')
+ }
+ }
+
const goFirstPage = () => setPageStart(0)
const goNextPage = () => setPageStart(pageMeta?.next_start || 0)
const goPrevPage = () => setPageStart(Math.max(0, pageStart - Number(limit || 200)))
@@ -130,6 +140,9 @@ export default function LogMonitor() {
+
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 9c32137..f7999c1 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -331,4 +331,16 @@ export const api = {
}
return response.json();
},
+
+ writeLogsTest: async () => {
+ const response = await fetch(buildUrl('/api/system/logs/test-write'), {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ });
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ detail: '写入测试日志失败' }));
+ throw new Error(error.detail || '写入测试日志失败');
+ }
+ return response.json();
+ },
};
diff --git a/trading_system/main.py b/trading_system/main.py
index 28b45fa..77ebae8 100644
--- a/trading_system/main.py
+++ b/trading_system/main.py
@@ -65,10 +65,13 @@ logging.basicConfig(
# 追加:将 ERROR 日志写入 Redis(不影响现有文件/控制台日志)
try:
- if __name__ == '__main__':
- from redis_log_handler import RedisErrorLogHandler, RedisLogConfig
- else:
+ # 兼容两种启动方式:
+ # - 直接运行:python trading_system/main.py
+ # - 模块运行:python -m trading_system.main
+ try:
from .redis_log_handler import RedisErrorLogHandler, RedisLogConfig
+ except Exception:
+ from redis_log_handler import RedisErrorLogHandler, RedisLogConfig
redis_cfg = RedisLogConfig(
redis_url=getattr(config, "REDIS_URL", "redis://localhost:6379"),
@@ -83,6 +86,22 @@ try:
# 让 handler 自己按组筛选(error/warning/info),这里只需要放宽到 INFO
redis_handler.setLevel(logging.INFO)
logging.getLogger().addHandler(redis_handler)
+
+ # 诊断:启动时快速检测一次 Redis 可用性(失败不影响运行)
+ try:
+ client = redis_handler._get_redis() # noqa: SLF001(仅用于诊断)
+ if client is None:
+ logger = logging.getLogger(__name__)
+ logger.warning(
+ f"⚠ Redis 日志写入未启用(无法连接或缺少依赖)。REDIS_URL={getattr(config, 'REDIS_URL', None)}"
+ )
+ else:
+ logger = logging.getLogger(__name__)
+ logger.info(
+ f"✓ Redis 日志写入已启用。REDIS_URL={getattr(config, 'REDIS_URL', None)}"
+ )
+ except Exception:
+ pass
except Exception:
# Redis handler 仅用于增强监控,失败不影响交易系统启动
pass