#!/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)