request_logger.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. """
  2. 请求日志记录器 - 记录每个语音对话请求的详细性能数据
  3. 使用JSON Lines格式,按天轮转日志文件
  4. """
  5. import json
  6. import time
  7. import queue
  8. import threading
  9. from datetime import datetime
  10. from pathlib import Path
  11. from typing import Optional, Dict, Any
  12. from dataclasses import dataclass
  13. from config import settings
  14. @dataclass
  15. class PerformanceMetrics:
  16. """性能指标数据类"""
  17. # 用户说话
  18. user_speech_start: Optional[float] = None
  19. user_speech_end: Optional[float] = None
  20. user_text: str = ""
  21. # STT相关
  22. stt_first_audio_received: Optional[float] = None
  23. stt_first_text_generated: Optional[float] = None
  24. stt_last_audio_received: Optional[float] = None
  25. stt_completed: Optional[float] = None
  26. stt_audio_chunks: int = 0 # 音频块数量
  27. stt_total_bytes: int = 0 # 总字节数
  28. # LLM提示词准备
  29. llm_prompt_prepare_start: Optional[float] = None
  30. llm_prompt_prepare_end: Optional[float] = None
  31. llm_prompt_length: int = 0 # 提示词长度
  32. llm_context_round: int = 0 # 上下文轮次
  33. llm_full_prompt: str = "" # 完整prompt(system_prompt + user_input)
  34. # LLM生成
  35. llm_call_start: Optional[float] = None
  36. llm_first_text_output: Optional[float] = None
  37. llm_all_text_completed: Optional[float] = None
  38. llm_call_end: Optional[float] = None
  39. llm_emotion: str = "neutral" # 情感
  40. llm_sentences: int = 0 # 句子数
  41. # TTS生成
  42. tts_call_start: Optional[float] = None
  43. tts_first_audio_output: Optional[float] = None
  44. tts_all_audio_completed: Optional[float] = None
  45. tts_call_end: Optional[float] = None
  46. tts_voice: str = "Cherry" # 音色
  47. tts_audio_chunks: int = 0 # 音频块数量
  48. tts_total_bytes: int = 0 # 总字节数
  49. # AI响应
  50. ai_response_text: str = ""
  51. # 消息ID(数据库关联)
  52. user_message_id: Optional[str] = None # 用户消息的 message_id
  53. assistant_message_id: Optional[str] = None # AI消息的 message_id
  54. def calculate_user_speech_duration_ms(self) -> Optional[float]:
  55. """计算用户说话时长(ms)"""
  56. if self.user_speech_start and self.user_speech_end:
  57. return (self.user_speech_end - self.user_speech_start) * 1000
  58. return None
  59. def calculate_stt_latency_ms(self) -> Optional[float]:
  60. """计算STT延迟(ms): STT完成时间 - 收到最后一个音频时间"""
  61. if self.stt_completed and self.stt_last_audio_received:
  62. return (self.stt_completed - self.stt_last_audio_received) * 1000
  63. return None
  64. def calculate_llm_prompt_latency_ms(self) -> Optional[float]:
  65. """计算LLM提示词准备延迟(ms)"""
  66. if self.llm_prompt_prepare_end and self.llm_prompt_prepare_start:
  67. return (self.llm_prompt_prepare_end - self.llm_prompt_prepare_start) * 1000
  68. return None
  69. def calculate_llm_generation_latency_ms(self) -> Optional[float]:
  70. """计算LLM生成延迟(ms)"""
  71. if self.llm_first_text_output and self.llm_call_start:
  72. return (self.llm_first_text_output - self.llm_call_start) * 1000
  73. return None
  74. def calculate_tts_latency_ms(self) -> Optional[float]:
  75. """计算TTS延迟(ms)"""
  76. if self.tts_first_audio_output and self.tts_call_start:
  77. return (self.tts_first_audio_output - self.tts_call_start) * 1000
  78. return None
  79. def calculate_total_latency_ms(self) -> Optional[float]:
  80. """计算总延迟(ms): 开始输出第一个音频时间 - 用户结束说话时间"""
  81. if self.tts_first_audio_output and self.user_speech_end:
  82. return (self.tts_first_audio_output - self.user_speech_end) * 1000
  83. return None
  84. class RequestLogger:
  85. """请求日志记录器(线程安全,异步写入)"""
  86. def __init__(self, log_dir: str = "logs"):
  87. """
  88. 初始化日志记录器
  89. Args:
  90. log_dir: 日志目录
  91. """
  92. self.log_dir = Path(log_dir)
  93. self.log_dir.mkdir(parents=True, exist_ok=True)
  94. # 当前日志文件路径
  95. self.current_log_file: Optional[Path] = None
  96. self.current_date: Optional[str] = None
  97. # 异步写入队列
  98. self.log_queue: queue.Queue = queue.Queue()
  99. self.writer_thread: Optional[threading.Thread] = None
  100. self.running = True
  101. # 启动后台写入线程
  102. self._start_writer_thread()
  103. def _get_log_file_path(self, date_str: str) -> Path:
  104. """获取指定日期的日志文件路径"""
  105. return self.log_dir / f"request_{date_str}.log"
  106. def _get_current_log_file(self) -> Path:
  107. """获取当前日期的日志文件路径(自动轮转)"""
  108. today = datetime.now().strftime("%Y-%m-%d")
  109. # 如果日期变化,切换日志文件
  110. if self.current_date != today:
  111. self.current_date = today
  112. self.current_log_file = self._get_log_file_path(today)
  113. return self.current_log_file
  114. def _start_writer_thread(self):
  115. """启动后台写入线程"""
  116. def writer_loop():
  117. """后台写入循环"""
  118. while self.running:
  119. try:
  120. # 从队列获取日志条目(超时1秒)
  121. log_entry = self.log_queue.get(timeout=1.0)
  122. # 获取当前日志文件
  123. log_file = self._get_current_log_file()
  124. # 写入JSON格式日志文件(追加模式)
  125. with open(log_file, 'a', encoding='utf-8') as f:
  126. f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
  127. # 如果是请求日志,同时写入conversation_log.txt
  128. if log_entry.get("类型") == "请求":
  129. self._write_conversation_log(log_entry)
  130. self.log_queue.task_done()
  131. except queue.Empty:
  132. continue
  133. except Exception as e:
  134. # 写入失败,记录到stderr(避免循环)
  135. import sys
  136. print(f"[RequestLogger] 写入日志失败: {e}", file=sys.stderr)
  137. self.writer_thread = threading.Thread(target=writer_loop, daemon=True)
  138. self.writer_thread.start()
  139. def _format_timestamp(self, timestamp: Optional[float]) -> Optional[str]:
  140. """格式化时间戳为ISO 8601格式"""
  141. if timestamp is None:
  142. return None
  143. dt = datetime.fromtimestamp(timestamp)
  144. return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
  145. def _format_iso_time_to_conversation_log(self, iso_time: Optional[str]) -> str:
  146. """将ISO时间戳格式化为conversation_log.txt格式 (HH:MM:SS.mmm)"""
  147. if not iso_time:
  148. return "N/A"
  149. try:
  150. # 从ISO格式解析: 2025-11-11T22:42:46.123Z
  151. dt = datetime.fromisoformat(iso_time.replace('Z', '+00:00'))
  152. return dt.strftime("%H:%M:%S.%f")[:-3]
  153. except:
  154. return "N/A"
  155. def _write_conversation_log(self, log_entry: Dict[str, Any]):
  156. """写入conversation_log.txt格式的日志"""
  157. try:
  158. conversation_log_path = Path("conversation_log.txt")
  159. stages = log_entry.get("阶段", {})
  160. summary = log_entry.get("总结", {})
  161. request_id = log_entry.get("请求ID", "unknown")
  162. timestamp = log_entry.get("时间戳", "")
  163. # 解析时间戳获取日期时间
  164. if timestamp:
  165. try:
  166. # 从ISO格式解析: 2025-11-11T22:42:46.123Z
  167. dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
  168. date_str = dt.strftime("%Y-%m-%d %H:%M:%S")
  169. except:
  170. date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  171. else:
  172. date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  173. lines = []
  174. lines.append(f"[{date_str}] [{request_id}]")
  175. # 用户说话
  176. user_speech = stages.get("用户说话", {})
  177. user_text = ""
  178. if "语音识别" in stages and "输出" in stages["语音识别"]:
  179. user_text = stages["语音识别"]["输出"].get("文本", "")
  180. user_start = user_speech.get("开始时间")
  181. user_end = user_speech.get("结束时间")
  182. user_duration = user_speech.get("时长_毫秒")
  183. user_start_str = self._format_iso_time_to_conversation_log(user_start)
  184. user_end_str = self._format_iso_time_to_conversation_log(user_end)
  185. user_duration_str = f"{int(user_duration)}ms" if user_duration else "N/A"
  186. lines.append(f"用户说话: {user_start_str}, {user_text}, {user_end_str}, {user_duration_str}")
  187. # STT
  188. stt = stages.get("语音识别", {})
  189. stt_first_audio = stt.get("首次收到音频")
  190. stt_first_text = stt.get("首次生成文本")
  191. stt_last_audio = stt.get("最后收到音频")
  192. stt_completed = stt.get("完成时间")
  193. stt_latency = stt.get("延迟_毫秒")
  194. stt_first_audio_str = self._format_iso_time_to_conversation_log(stt_first_audio)
  195. stt_first_text_str = self._format_iso_time_to_conversation_log(stt_first_text)
  196. stt_last_audio_str = self._format_iso_time_to_conversation_log(stt_last_audio)
  197. stt_completed_str = self._format_iso_time_to_conversation_log(stt_completed)
  198. stt_latency_str = f"{int(stt_latency)}ms" if stt_latency else "N/A"
  199. lines.append(f"STT: {stt_first_audio_str}, {stt_first_text_str}, {stt_last_audio_str}, {stt_completed_str}, {stt_latency_str}")
  200. # LLM提示词
  201. llm_prompt = stages.get("LLM提示词准备", {})
  202. llm_prompt_start = llm_prompt.get("开始时间")
  203. llm_prompt_end = llm_prompt.get("结束时间")
  204. llm_prompt_duration = llm_prompt.get("时长_毫秒")
  205. llm_prompt_start_str = self._format_iso_time_to_conversation_log(llm_prompt_start)
  206. llm_prompt_end_str = self._format_iso_time_to_conversation_log(llm_prompt_end)
  207. llm_prompt_duration_str = f"{int(llm_prompt_duration)}ms" if llm_prompt_duration else "N/A"
  208. lines.append(f"LLM提示词: {llm_prompt_start_str}, {llm_prompt_end_str}, {llm_prompt_duration_str}")
  209. # LLM生成
  210. llm = stages.get("LLM生成", {})
  211. llm_call_start = llm.get("调用开始")
  212. llm_first_text = llm.get("首次文本输出")
  213. llm_all_completed = llm.get("全部文本完成")
  214. llm_call_end = llm.get("调用结束")
  215. llm_latency = llm.get("延迟_毫秒")
  216. llm_call_start_str = self._format_iso_time_to_conversation_log(llm_call_start)
  217. llm_first_text_str = self._format_iso_time_to_conversation_log(llm_first_text)
  218. llm_all_completed_str = self._format_iso_time_to_conversation_log(llm_all_completed)
  219. llm_call_end_str = self._format_iso_time_to_conversation_log(llm_call_end)
  220. llm_latency_str = f"{int(llm_latency)}ms" if llm_latency else "N/A"
  221. lines.append(f"LLM生成: {llm_call_start_str}, {llm_first_text_str}, {llm_all_completed_str}, {llm_call_end_str}, {llm_latency_str}")
  222. # TTS生成
  223. tts = stages.get("TTS生成", {})
  224. tts_call_start = tts.get("调用开始")
  225. tts_first_audio = tts.get("首次音频输出")
  226. tts_all_completed = tts.get("全部音频完成")
  227. tts_call_end = tts.get("调用结束")
  228. tts_latency = tts.get("延迟_毫秒")
  229. tts_call_start_str = self._format_iso_time_to_conversation_log(tts_call_start)
  230. tts_first_audio_str = self._format_iso_time_to_conversation_log(tts_first_audio)
  231. tts_all_completed_str = self._format_iso_time_to_conversation_log(tts_all_completed)
  232. tts_call_end_str = self._format_iso_time_to_conversation_log(tts_call_end)
  233. tts_latency_str = f"{int(tts_latency)}ms" if tts_latency else "N/A"
  234. lines.append(f"TTS生成: {tts_call_start_str}, {tts_first_audio_str}, {tts_all_completed_str}, {tts_call_end_str}, {tts_latency_str}")
  235. # 总结
  236. ai_response_text = ""
  237. if "LLM生成" in stages and "输出" in stages["LLM生成"]:
  238. ai_response_text = stages["LLM生成"]["输出"].get("文本", "")
  239. total_latency = summary.get("首次音频延迟_毫秒")
  240. total_latency_str = f"{int(total_latency)}ms" if total_latency else "N/A"
  241. lines.append(f"总结: {ai_response_text}, {total_latency_str}")
  242. lines.append("-" * 120)
  243. lines.append("")
  244. # 写入文件
  245. with open(conversation_log_path, 'a', encoding='utf-8') as f:
  246. f.write('\n'.join(lines) + '\n')
  247. except Exception as e:
  248. # 写入失败不影响主流程
  249. import sys
  250. print(f"[RequestLogger] 写入conversation_log.txt失败: {e}", file=sys.stderr)
  251. def _build_request_log(
  252. self,
  253. request_id: str,
  254. user_id: str,
  255. metrics: PerformanceMetrics
  256. ) -> Dict[str, Any]:
  257. """构建请求日志JSON对象"""
  258. # 构建各阶段数据
  259. stages = {}
  260. # 用户说话阶段
  261. if metrics.user_speech_start and metrics.user_speech_end:
  262. stages["用户说话"] = {
  263. "开始时间": self._format_timestamp(metrics.user_speech_start),
  264. "结束时间": self._format_timestamp(metrics.user_speech_end),
  265. "时长_毫秒": metrics.calculate_user_speech_duration_ms()
  266. }
  267. # STT阶段
  268. if metrics.stt_first_audio_received:
  269. stt_stage = {
  270. "首次收到音频": self._format_timestamp(metrics.stt_first_audio_received),
  271. "最后收到音频": self._format_timestamp(metrics.stt_last_audio_received),
  272. "完成时间": self._format_timestamp(metrics.stt_completed),
  273. "输入": {
  274. "音频块数": metrics.stt_audio_chunks,
  275. "总字节数": metrics.stt_total_bytes
  276. },
  277. "输出": {
  278. "文本": metrics.user_text,
  279. "是否最终": True
  280. }
  281. }
  282. if metrics.stt_first_text_generated:
  283. stt_stage["首次生成文本"] = self._format_timestamp(metrics.stt_first_text_generated)
  284. if metrics.stt_completed and metrics.stt_last_audio_received:
  285. stt_stage["延迟_毫秒"] = metrics.calculate_stt_latency_ms()
  286. stages["语音识别"] = stt_stage
  287. # LLM提示词准备阶段
  288. if metrics.llm_prompt_prepare_start and metrics.llm_prompt_prepare_end:
  289. stages["LLM提示词准备"] = {
  290. "开始时间": self._format_timestamp(metrics.llm_prompt_prepare_start),
  291. "结束时间": self._format_timestamp(metrics.llm_prompt_prepare_end),
  292. "时长_毫秒": metrics.calculate_llm_prompt_latency_ms()
  293. }
  294. # LLM生成阶段
  295. if metrics.llm_call_start:
  296. llm_stage = {
  297. "调用开始": self._format_timestamp(metrics.llm_call_start),
  298. "调用结束": self._format_timestamp(metrics.llm_call_end),
  299. "输入": {
  300. "提示词长度": metrics.llm_prompt_length,
  301. "上下文轮次": metrics.llm_context_round,
  302. "完整提示词": metrics.llm_full_prompt if metrics.llm_full_prompt else ""
  303. },
  304. "输出": {
  305. "文本": metrics.ai_response_text,
  306. "情感": metrics.llm_emotion,
  307. "句子数": metrics.llm_sentences
  308. }
  309. }
  310. if metrics.llm_first_text_output:
  311. llm_stage["首次文本输出"] = self._format_timestamp(metrics.llm_first_text_output)
  312. if metrics.llm_all_text_completed:
  313. llm_stage["全部文本完成"] = self._format_timestamp(metrics.llm_all_text_completed)
  314. if metrics.llm_first_text_output and metrics.llm_call_start:
  315. llm_stage["延迟_毫秒"] = metrics.calculate_llm_generation_latency_ms()
  316. stages["LLM生成"] = llm_stage
  317. # TTS阶段
  318. if metrics.tts_call_start:
  319. tts_stage = {
  320. "调用开始": self._format_timestamp(metrics.tts_call_start),
  321. "调用结束": self._format_timestamp(metrics.tts_call_end),
  322. "输入": {
  323. "文本": metrics.ai_response_text,
  324. "情感": metrics.llm_emotion,
  325. "音色": metrics.tts_voice
  326. },
  327. "输出": {
  328. "音频块数": metrics.tts_audio_chunks,
  329. "总字节数": metrics.tts_total_bytes
  330. }
  331. }
  332. if metrics.tts_first_audio_output:
  333. tts_stage["首次音频输出"] = self._format_timestamp(metrics.tts_first_audio_output)
  334. if metrics.tts_all_audio_completed:
  335. tts_stage["全部音频完成"] = self._format_timestamp(metrics.tts_all_audio_completed)
  336. if metrics.tts_first_audio_output and metrics.tts_call_start:
  337. tts_stage["延迟_毫秒"] = metrics.calculate_tts_latency_ms()
  338. stages["TTS生成"] = tts_stage
  339. # 构建总结
  340. summary = {}
  341. if metrics.tts_first_audio_output and metrics.user_speech_end:
  342. summary["首次音频延迟_毫秒"] = metrics.calculate_total_latency_ms()
  343. if metrics.tts_all_audio_completed and metrics.user_speech_start:
  344. total_duration = (metrics.tts_all_audio_completed - metrics.user_speech_start) * 1000
  345. summary["总延迟_毫秒"] = total_duration
  346. # 构建完整日志对象
  347. log_entry = {
  348. "类型": "请求",
  349. "时间戳": self._format_timestamp(time.time()),
  350. "请求ID": request_id,
  351. "用户ID": user_id,
  352. "阶段": stages
  353. }
  354. if summary:
  355. log_entry["总结"] = summary
  356. # 添加消息ID(数据库关联)
  357. if metrics.user_message_id:
  358. log_entry["用户消息ID"] = metrics.user_message_id
  359. if metrics.assistant_message_id:
  360. log_entry["助手消息ID"] = metrics.assistant_message_id
  361. return log_entry
  362. def log_request(
  363. self,
  364. user_id: str,
  365. request_id: str,
  366. metrics: PerformanceMetrics
  367. ):
  368. """
  369. 记录一条对话请求
  370. Args:
  371. user_id: 用户ID
  372. request_id: 请求ID
  373. metrics: 性能指标对象
  374. """
  375. try:
  376. log_entry = self._build_request_log(request_id, user_id, metrics)
  377. self.log_queue.put(log_entry)
  378. except Exception as e:
  379. # 记录失败不影响主流程
  380. import sys
  381. print(f"[RequestLogger] 记录请求日志失败: {e}", file=sys.stderr)
  382. def log_error(
  383. self,
  384. user_id: str,
  385. request_id: str,
  386. error_code: str,
  387. error_message: str,
  388. stage: Optional[str] = None,
  389. user_text: Optional[str] = None
  390. ):
  391. """
  392. 记录错误请求
  393. Args:
  394. user_id: 用户ID
  395. request_id: 请求ID
  396. error_code: 错误代码
  397. error_message: 错误信息
  398. stage: 错误发生的阶段(可选)
  399. user_text: 用户输入文本(可选)
  400. """
  401. try:
  402. log_entry = {
  403. "类型": "错误",
  404. "时间戳": self._format_timestamp(time.time()),
  405. "请求ID": request_id,
  406. "用户ID": user_id,
  407. "错误": {
  408. "代码": error_code,
  409. "消息": error_message
  410. }
  411. }
  412. if stage:
  413. log_entry["错误"]["阶段"] = stage
  414. if user_text:
  415. log_entry["上下文"] = {
  416. "用户文本": user_text,
  417. "阶段": stage or "unknown"
  418. }
  419. self.log_queue.put(log_entry)
  420. except Exception as e:
  421. import sys
  422. print(f"[RequestLogger] 记录错误日志失败: {e}", file=sys.stderr)
  423. def shutdown(self):
  424. """关闭日志记录器(等待队列清空)"""
  425. self.running = False
  426. if self.writer_thread:
  427. self.log_queue.join() # 等待队列清空
  428. self.writer_thread.join(timeout=5.0)
  429. # 全局单例
  430. request_logger = RequestLogger(log_dir=settings.LOG_DIR)