feat: 增加log 配置

This commit is contained in:
ViperEkura 2026-04-13 21:25:27 +08:00
parent 85619c0d97
commit 30fc1779f4
7 changed files with 112 additions and 11 deletions

View File

@ -1,7 +1,7 @@
# 配置文件 # 配置文件
app: app:
secret_key: ${APP_SECRET_KEY} secret_key: ${APP_SECRET_KEY}
debug: true debug: false
host: 0.0.0.0 host: 0.0.0.0
port: 8000 port: 8000
@ -19,3 +19,7 @@ tools:
cache_ttl: 300 cache_ttl: 300
max_workers: 4 max_workers: 4
max_iterations: 10 max_iterations: 10
logging:
level: INFO

View File

@ -323,7 +323,8 @@ body {
.message-bubble.assistant .message-container { .message-bubble.assistant .message-container {
align-items: flex-start; align-items: flex-start;
flex: 1 1 auto; flex: 1 1 auto;
width: 100%; width: 80%;
max-width: 80%;
min-width: 0; min-width: 0;
} }

View File

@ -99,10 +99,15 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) {
onError(data.content) onError(data.content)
} }
} catch (e) { } catch (e) {
// 忽略解析错误 console.error('SSE parse error:', e, 'line:', line)
} }
} }
} }
// 如果没有收到 done 事件,触发错误
if (!completed && onError) {
onError('stream ended without done event')
}
break break
} }

View File

@ -1,4 +1,5 @@
"""FastAPI application factory""" """FastAPI application factory"""
import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -7,6 +8,8 @@ from luxx.config import config
from luxx.database import init_db from luxx.database import init_db
from luxx.routes import api_router from luxx.routes import api_router
logger = logging.getLogger(__name__)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@ -31,7 +34,7 @@ async def lifespan(app: FastAPI):
) )
db.add(default_user) db.add(default_user)
db.commit() db.commit()
print("Default admin user created: admin / admin123") logger.info("Default admin user created: admin / admin123")
finally: finally:
db.close() db.close()

View File

@ -111,6 +111,19 @@ class Config:
@property @property
def tools_max_iterations(self) -> int: def tools_max_iterations(self) -> int:
return self.get("tools.max_iterations", 10) 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 # Global configuration instance

View File

@ -1,10 +1,13 @@
"""LLM API client""" """LLM API client"""
import json import json
import httpx import httpx
import logging
from typing import Dict, Any, Optional, List, AsyncGenerator from typing import Dict, Any, Optional, List, AsyncGenerator
from luxx.config import config from luxx.config import config
logger = logging.getLogger(__name__)
class LLMResponse: class LLMResponse:
"""LLM response""" """LLM response"""
@ -147,19 +150,18 @@ class LLMClient:
""" """
body = self._build_body(model, messages, tools, stream=True, **kwargs) body = self._build_body(model, messages, tools, stream=True, **kwargs)
print(f"[LLM] Starting stream_call for model: {model}") logger.info(f"Starting stream_call for model: {model}, messages count: {len(messages)}")
print(f"[LLM] Messages count: {len(messages)}")
try: try:
async with httpx.AsyncClient(timeout=120.0) as client: 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( async with client.stream(
"POST", "POST",
self.api_url, self.api_url,
headers=self._build_headers(), headers=self._build_headers(),
json=body json=body
) as response: ) as response:
print(f"[LLM] Response status: {response.status_code}") logger.info(f"Response status: {response.status_code}")
response.raise_for_status() response.raise_for_status()
async for line in response.aiter_lines(): async for line in response.aiter_lines():
@ -167,10 +169,10 @@ class LLMClient:
yield line + "\n" yield line + "\n"
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
status_code = e.response.status_code if e.response else "?" 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" yield f"event: error\ndata: {json.dumps({'content': f'HTTP {status_code}: Request failed'})}\n\n"
except Exception as e: 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" yield f"event: error\ndata: {json.dumps({'content': str(e)})}\n\n"

75
run.py
View File

@ -1,17 +1,90 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Application entry point""" """Application entry point"""
import logging
import logging.config
from copy import copy
import uvicorn import uvicorn
from uvicorn.logging import ColourizedFormatter
from luxx.config import config 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(): def main():
"""Start the application""" """Start the application"""
logging.config.dictConfig(LOG_CONFIG)
uvicorn.run( uvicorn.run(
"luxx:app", "luxx:app",
host=config.app_host, host=config.app_host,
port=config.app_port, port=config.app_port,
reload=config.debug, reload=config.debug,
log_level="debug" if config.debug else "info" log_level=config.log_level.lower(),
log_config=LOG_CONFIG
) )