child-psycho-companion/xiaozhi_log_monitor.py

201 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env python3
"""
xiaozhi-esp32-server 实时日志监控器
用法:
python xiaozhi_log_monitor.py # 全部日志
python xiaozhi_log_monitor.py --filter llm # 只看 LLM
python xiaozhi_log_monitor.py --filter mcp # 只看 MCP
python xiaozhi_log_monitor.py --filter device # 只看设备连接
python xiaozhi_log_monitor.py --filter error # 只看错误
按 Ctrl+C 退出
"""
import subprocess
import re
import sys
import argparse
import time
from datetime import datetime
# ANSI 颜色码
COLORS = {
"reset": "\033[0m",
"bold": "\033[1m",
"red": "\033[91m",
"green": "\033[92m",
"yellow": "\033[93m",
"blue": "\033[94m",
"magenta": "\033[95m",
"cyan": "\033[96m",
"gray": "\033[90m",
"white": "\033[97m",
}
# 日志类型配色
LOG_COLORS = {
"ERROR": COLORS["red"] + COLORS["bold"],
"error": COLORS["red"],
"WARN": COLORS["yellow"] + COLORS["bold"],
"warn": COLORS["yellow"],
"LLM": COLORS["cyan"] + COLORS["bold"],
"llm": COLORS["cyan"],
"MCP": COLORS["magenta"] + COLORS["bold"],
"mcp": COLORS["magenta"],
"device": COLORS["green"],
"DEVICE": COLORS["green"] + COLORS["bold"],
"TTS": COLORS["blue"] + COLORS["bold"],
"tts": COLORS["blue"],
"STT": COLORS["blue"],
"ASR": COLORS["blue"],
"VAD": COLORS["gray"],
"websocket": COLORS["yellow"],
"MCP接入点": COLORS["magenta"],
"连接成功": COLORS["green"],
"启动": COLORS["cyan"],
"初始化": COLORS["cyan"],
"function_call": COLORS["yellow"],
}
# 过滤器模式
FILTER_PATTERNS = {
"llm": re.compile(r"(LLM|llm|大模型|MiniMax|abab|chatglm|openai_api|function_call|psycho_screen)", re.IGNORECASE),
"mcp": re.compile(r"(MCP|mcp|接入点|tool|call|psycho_screen)", re.IGNORECASE),
"device": re.compile(r"(device|设备|连接|断开|认证|websocket)", re.IGNORECASE),
"error": re.compile(r"(ERROR|error|错误|失败|异常|401|400|500|exception)", re.IGNORECASE),
"tts": re.compile(r"(TTS|tts|语音|音频|EdgeTTS|生成)", re.IGNORECASE),
"stt": re.compile(r"(STT|ASR|stt|识别|转文字)", re.IGNORECASE),
"all": re.compile(r".*"),
}
# 时间戳提取(格式: 20260406 22:50:12
TIMESTAMP_RE = re.compile(r"(\d{8}\s+\d{2}:\d{2}:\d{2})")
def colorize(line: str) -> str:
"""为日志行上色"""
for keyword, color in LOG_COLORS.items():
if keyword in line:
return color + line + COLORS["reset"]
return line
def format_line(line: str) -> str:
"""格式化单行日志"""
line = line.rstrip("\n\r")
if not line:
return ""
# 提取时间戳
ts_match = TIMESTAMP_RE.search(line)
ts = ts_match.group(1) if ts_match else ""
# 上色
colored = colorize(line)
# 高亮时间戳
if ts:
return f"{COLORS['gray']}{ts}{COLORS['reset']} {colored[len(ts):]}"
return colored
def matches_filter(line: str, filter_name: str) -> bool:
"""检查日志行是否匹配过滤器"""
pattern = FILTER_PATTERNS.get(filter_name, FILTER_PATTERNS["all"])
return bool(pattern.search(line))
def monitor(filter_name: str = "all", container: str = "xiaozhi-esp32-server"):
"""主监控循环"""
print(f"\n{COLORS['cyan']}{COLORS['bold']}=== xiaozhi-esp32-server 实时日志监控 ==={COLORS['reset']}")
print(f"过滤器: {filter_name} | 容器: {container}")
print(f"{COLORS['yellow']}Ctrl+C{COLORS['reset']} 退出\n")
try:
proc = subprocess.Popen(
["docker", "logs", "-f", "--since", "0s", container],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=False,
bufsize=1,
)
except Exception as e:
print(f"{COLORS['red']}启动日志失败: {e}{COLORS['reset']}")
sys.exit(1)
line_count = 0
shown_lines = []
try:
for raw_line in iter(proc.stdout.readline, b""):
try:
line = raw_line.decode("utf-8", errors="replace")
except Exception:
line = raw_line.decode("latin-1", errors="replace")
if not matches_filter(line, filter_name):
continue
line_count += 1
shown_lines.append(line)
# 只保留最后 5000 行
if len(shown_lines) > 5000:
shown_lines.pop(0)
print(format_line(line))
sys.stdout.flush()
except KeyboardInterrupt:
print(f"\n\n{COLORS['gray']}已停止。共显示了 {line_count} 条日志。{COLORS['reset']}")
finally:
proc.terminate()
proc.wait(timeout=5)
def replay_recent(filter_name: str = "all", container: str = "xiaozhi-esp32-server", lines: int = 200):
"""回放最近 N 条日志"""
print(f"\n{COLORS['cyan']}=== 回放最近 {lines} 条日志 ==={COLORS['reset']}\n")
try:
proc = subprocess.run(
["docker", "logs", "--tail", str(lines), container],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=False,
timeout=10,
)
for raw_line in iter(proc.stdout.readline, b""):
line = raw_line.decode("utf-8", errors="replace")
if matches_filter(line, filter_name):
print(format_line(line))
except Exception as e:
print(f"{COLORS['red']}回放失败: {e}{COLORS['reset']}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="xiaozhi-esp32-server 实时日志监控器")
parser.add_argument(
"--filter", "-f",
choices=["all", "llm", "mcp", "device", "error", "tts", "stt"],
default="all",
help="日志过滤器 (default: all)"
)
parser.add_argument(
"--container", "-c",
default="xiaozhi-esp32-server",
help="Docker 容器名 (default: xiaozhi-esp32-server)"
)
parser.add_argument(
"--replay", "-r",
type=int,
metavar="N",
help="回放最近 N 条日志后退出(不持续监控)"
)
args = parser.parse_args()
if args.replay:
replay_recent(filter_name=args.filter, container=args.container, lines=args.replay)
else:
monitor(filter_name=args.filter, container=args.container)