From 032ae1e93fdaa1071f84926438d1492eb601422b Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Mon, 13 Apr 2026 21:18:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0log=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.yaml | 6 ++- dashboard/src/utils/api.js | 7 +++- luxx/__init__.py | 5 ++- luxx/config.py | 13 +++++++ luxx/services/llm_client.py | 14 ++++--- run.py | 75 ++++++++++++++++++++++++++++++++++++- 6 files changed, 110 insertions(+), 10 deletions(-) diff --git a/config.yaml b/config.yaml index 27d6136..fc17190 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ # 配置文件 app: secret_key: ${APP_SECRET_KEY} - debug: true + debug: false host: 0.0.0.0 port: 8000 @@ -19,3 +19,7 @@ tools: cache_ttl: 300 max_workers: 4 max_iterations: 10 + + +logging: + level: INFO diff --git a/dashboard/src/utils/api.js b/dashboard/src/utils/api.js index 387f4af..12029cf 100644 --- a/dashboard/src/utils/api.js +++ b/dashboard/src/utils/api.js @@ -99,10 +99,15 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) { onError(data.content) } } catch (e) { - // 忽略解析错误 + console.error('SSE parse error:', e, 'line:', line) } } } + + // 如果没有收到 done 事件,触发错误 + if (!completed && onError) { + onError('stream ended without done event') + } break } diff --git a/luxx/__init__.py b/luxx/__init__.py index b09caac..d1471f5 100644 --- a/luxx/__init__.py +++ b/luxx/__init__.py @@ -1,4 +1,5 @@ """FastAPI application factory""" +import logging from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -7,6 +8,8 @@ from luxx.config import config from luxx.database import init_db from luxx.routes import api_router +logger = logging.getLogger("luxx") + @asynccontextmanager async def lifespan(app: FastAPI): @@ -31,7 +34,7 @@ async def lifespan(app: FastAPI): ) db.add(default_user) db.commit() - print("Default admin user created: admin / admin123") + logger.info("Default admin user created: admin / admin123") finally: db.close() diff --git a/luxx/config.py b/luxx/config.py index bf8297f..6e5d083 100644 --- a/luxx/config.py +++ b/luxx/config.py @@ -111,6 +111,19 @@ class Config: @property def tools_max_iterations(self) -> int: return self.get("tools.max_iterations", 10) + + # Logging configuration + @property + def log_level(self) -> str: + return self.get("logging.level", "INFO") + + @property + def log_format(self) -> str: + return self.get("logging.format", "%(asctime)s | %(levelname)-8s | %(message)s") + + @property + def log_date_format(self) -> str: + return self.get("logging.date_format", "%Y-%m-%d %H:%M:%S") # Global configuration instance diff --git a/luxx/services/llm_client.py b/luxx/services/llm_client.py index 5dcd6de..467379e 100644 --- a/luxx/services/llm_client.py +++ b/luxx/services/llm_client.py @@ -1,10 +1,13 @@ """LLM API client""" import json import httpx +import logging from typing import Dict, Any, Optional, List, AsyncGenerator from luxx.config import config +logger = logging.getLogger("luxx.llm") + class LLMResponse: """LLM response""" @@ -147,19 +150,18 @@ class LLMClient: """ body = self._build_body(model, messages, tools, stream=True, **kwargs) - print(f"[LLM] Starting stream_call for model: {model}") - print(f"[LLM] Messages count: {len(messages)}") + logger.info(f"Starting stream_call for model: {model}, messages count: {len(messages)}") try: async with httpx.AsyncClient(timeout=120.0) as client: - print(f"[LLM] Sending request to {self.api_url}") + logger.info(f"Sending request to {self.api_url}") async with client.stream( "POST", self.api_url, headers=self._build_headers(), json=body ) as response: - print(f"[LLM] Response status: {response.status_code}") + logger.info(f"Response status: {response.status_code}") response.raise_for_status() async for line in response.aiter_lines(): @@ -167,10 +169,10 @@ class LLMClient: yield line + "\n" except httpx.HTTPStatusError as e: status_code = e.response.status_code if e.response else "?" - print(f"[LLM] HTTP error: {status_code}") + logger.error(f"HTTP error: {status_code}") yield f"event: error\ndata: {json.dumps({'content': f'HTTP {status_code}: Request failed'})}\n\n" except Exception as e: - print(f"[LLM] Exception: {type(e).__name__}: {str(e)}") + logger.error(f"Exception: {type(e).__name__}: {str(e)}") yield f"event: error\ndata: {json.dumps({'content': str(e)})}\n\n" diff --git a/run.py b/run.py index 54dc763..9ad36a3 100644 --- a/run.py +++ b/run.py @@ -1,17 +1,90 @@ #!/usr/bin/env python3 """Application entry point""" +import logging +import logging.config +from copy import copy import uvicorn +from uvicorn.logging import ColourizedFormatter from luxx.config import config +# Custom formatter extending uvicorn's ColourizedFormatter +class ModuleFormatter(ColourizedFormatter): + """Add module name after level prefix for non-uvicorn loggers""" + + def formatMessage(self, record: logging.LogRecord) -> str: + # Copy record to avoid modifying the original + recordcopy = copy(record) + + # Get level name with color + levelname = recordcopy.levelname + separator = " " * (8 - len(recordcopy.levelname)) + if self.use_colors: + levelname = self.color_level_name(levelname, recordcopy.levelno) + if "color_message" in recordcopy.__dict__: + recordcopy.msg = recordcopy.__dict__["color_message"] + recordcopy.__dict__["message"] = recordcopy.getMessage() + + # Add module name for all loggers + name = record.name + # For uvicorn.error, show "uvicorn" not "error" + if name.startswith('uvicorn.'): + module = 'uvicorn' + else: + module = name + levelprefix = levelname + ":" + separator + recordcopy.__dict__["levelprefix"] = f"{levelprefix} [{module}]" + + return super(ColourizedFormatter, self).formatMessage(recordcopy) + + +def get_log_config() -> dict: + """Get log configuration from config.yaml""" + log_level = getattr(config, 'log_level', 'INFO') + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": ModuleFormatter, + "fmt": "%(levelprefix)s %(message)s", + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + }, + }, + "loggers": { + "uvicorn": {"handlers": ["default"], "level": log_level, "propagate": False}, + "uvicorn.error": {"handlers": ["default"], "level": log_level, "propagate": False}, + "uvicorn.access": {"handlers": [], "level": "WARNING", "propagate": False}, + "uvicorn.asgi": {"handlers": [], "level": "WARNING", "propagate": False}, + }, + "root": { + "handlers": ["default"], + "level": log_level, + }, + } + + +LOG_CONFIG = get_log_config() + + def main(): """Start the application""" + logging.config.dictConfig(LOG_CONFIG) + uvicorn.run( "luxx:app", host=config.app_host, port=config.app_port, reload=config.debug, - log_level="debug" if config.debug else "info" + log_level=config.log_level.lower(), + log_config=LOG_CONFIG )