diff --git a/deploy/chongming.service b/deploy/chongming.service index 8ac489e..6ed5fb5 100644 --- a/deploy/chongming.service +++ b/deploy/chongming.service @@ -43,6 +43,10 @@ UMask=0027 # 每机可选覆盖(如 APP_WORKERS),文件不存在时忽略(- 前缀) EnvironmentFile=-/etc/chongming.env Environment=PYTHONUNBUFFERED=1 +# ffmpeg 位于非标准路径(AI Box 的 sophon-ffmpeg):覆盖默认 PATH 才能被 +# shutil.which / create_subprocess_exec 找到(RTSP 拉流依赖);若 ffmpeg 还依赖 +# 私有动态库,再补一条 Environment=LD_LIBRARY_PATH=/opt/sophon/sophon-ffmpeg-latest/lib +Environment=PATH=/opt/sophon/sophon-ffmpeg-latest/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # 优雅停机:SIGTERM 由 gunicorn 排空 worker,等在途请求/告警上传完成 KillSignal=SIGTERM diff --git a/src/chongming/app/core/logger.py b/src/chongming/app/core/logger.py index 617b049..1c62671 100644 --- a/src/chongming/app/core/logger.py +++ b/src/chongming/app/core/logger.py @@ -67,6 +67,80 @@ class ColoredFormatter(logging.Formatter): return result +class DailyRotatingFileHandler(RotatingFileHandler): + """ + 按自然日切分 + 大小轮转的文件处理器 + + 日志文件名形如 {log_name}-2026-08-19.log:进程启动时按当天日期命名, + 跨天后写入时自动切换到新日期文件,避免长时间运行的进程把跨天日志 + 全部堆积到启动当天的文件里。同时保留 RotatingFileHandler 的大小轮转 + (单文件超限追加 .1/.2 后缀)。 + + 历史文件(跨天文件与大小备份)按修改时间保留最近 backup_count 个, + 超出部分在切换日期的瞬间清理。 + """ + + def __init__( + self, + log_dir: str, + log_name: str, + max_bytes: int, + backup_count: int, + encoding: str = "utf-8", + ): + self.log_dir = Path(log_dir) + self.log_name = log_name + self._current_date = datetime.date.today() + super().__init__( + str(self.log_dir / f"{log_name}-{self._current_date}.log"), + encoding=encoding, + maxBytes=max_bytes, + backupCount=backup_count, + ) + + def _build_filename(self, day: datetime.date) -> str: + return str(self.log_dir / f"{self.log_name}-{day}.log") + + def _cleanup_old_files(self) -> None: + """保留最近 backup_count 个日志文件,删除更早的历史文件""" + # 按 (日期, 轮转序号) 排序,不依赖 mtime(同日备份可能同时落盘): + # 日期越大越新;同日内主文件(.log)最新,.N 备份按 N 递减更旧 + pattern = re.compile( + rf"^{re.escape(self.log_name)}-(\d{{4}}-\d{{2}}-\d{{2}})\.log(?:\.(\d+))?$" + ) + + def sort_key(path: Path): + match = pattern.match(path.name) + if not match: + return (datetime.date.min, 0) + return ( + datetime.date.fromisoformat(match.group(1)), + -int(match.group(2) or 0), + ) + + files = sorted( + self.log_dir.glob(f"{self.log_name}-*.log*"), + key=sort_key, + reverse=True, + ) + for old in files[self.backupCount :]: + try: + old.unlink() + except OSError: + pass + + def emit(self, record): + # 跨天:关闭旧文件流,切换到新日期文件,并清理过期历史文件 + today = datetime.date.today() + if today != self._current_date: + self._current_date = today + self.close() + self.baseFilename = self._build_filename(today) + self.stream = self._open() + self._cleanup_old_files() + super().emit(record) + + class ExcludeWatchFilesFilter(logging.Filter): """ 日志过滤器,用于排除第三方库的日志输出 @@ -112,7 +186,7 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"): 为指定的 logger 配置以下功能: 1. 设置日志级别和格式 - 2. 创建按日期命名的轮转文件处理器(支持文件大小限制和备份数量控制) + 2. 创建按自然日切分的轮转文件处理器(跨天自动切换文件,支持文件大小限制和备份数量控制) 3. 创建带颜色格式化的控制台处理器 4. 应用过滤器排除第三方库日志 @@ -145,15 +219,15 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"): plain_formatter = logging.Formatter(log_format) color_formatter = ColoredFormatter(log_format) - # 创建轮转文件处理器 - 限制文件大小为 10MB,保留 5 个备份文件 + # 创建按自然日切分的轮转文件处理器(跨天自动切文件,单文件超限再按大小轮转) max_bytes = log_config.get("max_size", 10 * 1024 * 1024) # 默认 10MB - backup_count = log_config.get("backup_count", 5) # 默认保留 5 个备份 + backup_count = log_config.get("backup_count", 5) # 默认保留 5 个历史文件 - file_handler = RotatingFileHandler( - f"{log_config['file']}/{log_name}-{datetime.date.today()}.log", - encoding="utf-8", - maxBytes=max_bytes, - backupCount=backup_count, + file_handler = DailyRotatingFileHandler( + log_config["file"], + log_name, + max_bytes=max_bytes, + backup_count=backup_count, ) file_handler.setFormatter(plain_formatter) file_handler.addFilter(ExcludeWatchFilesFilter())