Download apps/core/infrastructure/logging.py from jun2024/fc: direct link, hf CLI and curl.
- Browser
- Download file 12.6 kB
-
https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/core/infrastructure/logging.py
- Command line
-
hf download hf://spaces/jun2024/fc/apps/core/infrastructure/logging.py
-
curl -L -o logging.py https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/core/infrastructure/logging.py
12.6 kB
| """ | |
| Django 日志配置模块 | |
| 提供结构化日志配置 | |
| Docker 环境支持: | |
| - 自动检测 Docker 环境 | |
| - 优先输出到 stdout/stderr(Docker 日志收集) | |
| - 同时保留文件日志(持久化到 Volume) | |
| """ | |
| import os | |
| import sys | |
| from typing import Any | |
| def _safe_get_config(key: str, default: Any = None) -> Any: | |
| """安全获取配置,避免循环导入""" | |
| try: | |
| from .config import get_config | |
| return get_config(key, default) | |
| except (ImportError, AttributeError, KeyError): | |
| return default | |
| def _is_docker_environment() -> bool: | |
| """ | |
| 检测是否在 Docker 环境中运行 | |
| 检测方法: | |
| 1. 检查 /.dockerenv 文件 | |
| 2. 检查 /proc/1/cgroup 中是否包含 docker | |
| 3. 检查环境变量 DOCKER_CONTAINER | |
| """ | |
| # 方法1: 检查 .dockerenv 文件 | |
| if os.path.exists("/.dockerenv"): | |
| return True | |
| # 方法2: 检查 cgroup | |
| try: | |
| with open("/proc/1/cgroup") as f: | |
| if "docker" in f.read(): | |
| return True | |
| except (FileNotFoundError, PermissionError): | |
| pass | |
| # 方法3: 检查环境变量 | |
| if os.environ.get("DOCKER_CONTAINER") == "true": | |
| return True | |
| # 方法4: 检查常见容器内数据库主机标识 | |
| db_host = os.environ.get("DB_HOST", "") | |
| if db_host in {"postgres", "mysql", "mariadb"}: | |
| return True | |
| return False | |
| def get_logging_config(base_dir: Any, debug: bool = True) -> dict[str, Any]: | |
| """ | |
| 获取日志配置 | |
| 从统一配置管理系统获取日志配置参数 | |
| Docker 环境特性: | |
| - 优先输出到 stdout(Docker 日志收集) | |
| - 使用 JSON 格式便于日志聚合 | |
| - 同时保留文件日志到 Volume | |
| Args: | |
| base_dir: 项目根目录 | |
| debug: 是否为调试模式 | |
| Returns: | |
| Django LOGGING 配置字典 | |
| """ | |
| is_docker = _is_docker_environment() | |
| log_dir = os.path.join(base_dir, "logs") | |
| os.makedirs(log_dir, exist_ok=True) | |
| # 从配置系统获取日志参数 | |
| file_max_size = _safe_get_config("logging.file_max_size", 10 * 1024 * 1024) # 10MB | |
| api_backup_count = _safe_get_config("logging.api_backup_count", 5) | |
| error_backup_count = _safe_get_config("logging.error_backup_count", 10) | |
| sql_backup_count = _safe_get_config("logging.sql_backup_count", 3) | |
| # 获取日志级别配置 | |
| console_level = _safe_get_config("logging.console_level", "DEBUG" if debug else "INFO") | |
| file_level = _safe_get_config("logging.file_level", "INFO") | |
| error_level = _safe_get_config("logging.error_level", "ERROR") | |
| django_level = _safe_get_config("logging.django_level", "INFO") | |
| request_level = _safe_get_config("logging.request_level", "WARNING") | |
| apps_level = _safe_get_config("logging.apps_level", "DEBUG" if debug else "INFO") | |
| root_level = _safe_get_config("logging.root_level", "WARNING") | |
| # Docker 环境使用 JSON 格式,便于日志聚合 | |
| console_formatter = "json" if is_docker and not debug else "simple" | |
| config = { | |
| "version": 1, | |
| "disable_existing_loggers": False, | |
| "formatters": { | |
| "verbose": { | |
| "format": "[{asctime}] {levelname} {name} {module}.{funcName}:{lineno} - {message}", | |
| "style": "{", | |
| "datefmt": "%Y-%m-%d %H:%M:%S", | |
| }, | |
| "simple": { | |
| "format": "[{asctime}] {levelname} - {message}", | |
| "style": "{", | |
| "datefmt": "%Y-%m-%d %H:%M:%S", | |
| }, | |
| "json": { | |
| "()": "apps.core.infrastructure.logging.JsonFormatter", | |
| }, | |
| "docker": { | |
| # Docker 友好格式:包含时间戳、级别、模块信息 | |
| "format": "{asctime} | {levelname:8} | {name} | {message}", | |
| "style": "{", | |
| "datefmt": "%Y-%m-%d %H:%M:%S", | |
| }, | |
| }, | |
| "filters": { | |
| "require_debug_true": { | |
| "()": "django.utils.log.RequireDebugTrue", | |
| }, | |
| "require_debug_false": { | |
| "()": "django.utils.log.RequireDebugFalse", | |
| }, | |
| }, | |
| "handlers": { | |
| # 控制台输出(Docker 环境下输出到 stdout) | |
| "console": { | |
| "level": console_level, | |
| "class": "logging.StreamHandler", | |
| "stream": sys.stdout, # 明确指定 stdout | |
| "formatter": console_formatter, | |
| }, | |
| # 错误输出到 stderr(Docker 环境下便于区分) | |
| "console_error": { | |
| "level": "ERROR", | |
| "class": "logging.StreamHandler", | |
| "stream": sys.stderr, # 错误输出到 stderr | |
| "formatter": console_formatter, | |
| }, | |
| "file_api": { | |
| "level": file_level, | |
| "class": "logging.handlers.RotatingFileHandler", | |
| "filename": os.path.join(log_dir, "api.log"), | |
| "maxBytes": file_max_size, | |
| "backupCount": api_backup_count, | |
| "formatter": "verbose", | |
| "encoding": "utf-8", | |
| }, | |
| "file_error": { | |
| "level": error_level, | |
| "class": "logging.handlers.RotatingFileHandler", | |
| "filename": os.path.join(log_dir, "error.log"), | |
| "maxBytes": file_max_size, | |
| "backupCount": error_backup_count, | |
| "formatter": "verbose", | |
| "encoding": "utf-8", | |
| }, | |
| "file_sql": { | |
| "level": "DEBUG", | |
| "class": "logging.handlers.RotatingFileHandler", | |
| "filename": os.path.join(log_dir, "sql.log"), | |
| "maxBytes": file_max_size, | |
| "backupCount": sql_backup_count, | |
| "formatter": "simple", | |
| "encoding": "utf-8", | |
| "filters": ["require_debug_true"], | |
| }, | |
| }, | |
| "loggers": { | |
| "django": { | |
| "handlers": ["console", "console_error", "file_error"], | |
| "level": django_level, | |
| "propagate": True, | |
| }, | |
| "django.request": { | |
| "handlers": ["console", "console_error", "file_error"], | |
| "level": request_level, | |
| "propagate": False, | |
| }, | |
| "django.db.backends": { | |
| "handlers": ["file_sql"] if debug else [], | |
| "level": "DEBUG" if debug else "INFO", | |
| "propagate": False, | |
| }, | |
| "api": { | |
| "handlers": ["console", "console_error", "file_api", "file_error"], | |
| "level": apps_level, | |
| "propagate": False, | |
| }, | |
| "apps": { | |
| "handlers": ["console", "console_error", "file_api", "file_error"], | |
| "level": apps_level, | |
| "propagate": False, | |
| }, | |
| "apps.core.llm": { | |
| "handlers": ["console", "console_error", "file_api", "file_error"], | |
| "level": "INFO", | |
| "propagate": False, | |
| }, | |
| "RapidOCR": { | |
| "handlers": ["console", "console_error", "file_api", "file_error"], | |
| "level": "WARNING", | |
| "propagate": False, | |
| }, | |
| "rapidocr": { | |
| "handlers": ["console", "console_error", "file_api", "file_error"], | |
| "level": "WARNING", | |
| "propagate": False, | |
| }, | |
| "onnxruntime": { | |
| "handlers": ["console", "console_error", "file_api", "file_error"], | |
| "level": "WARNING", | |
| "propagate": False, | |
| }, | |
| # Gunicorn 日志(Docker 生产环境) | |
| "gunicorn.error": { | |
| "handlers": ["console", "console_error"], | |
| "level": "INFO", | |
| "propagate": False, | |
| }, | |
| "gunicorn.access": { | |
| "handlers": ["console"], | |
| "level": "INFO", | |
| "propagate": False, | |
| }, | |
| }, | |
| "root": { | |
| "handlers": ["console", "console_error", "file_error"], | |
| "level": root_level, | |
| }, | |
| } | |
| return config | |
| class JsonFormatter: | |
| """JSON 格式化器,用于结构化日志输出""" | |
| def __init__(self) -> None: | |
| import json | |
| self.json = json | |
| def format(self, record: Any) -> str: | |
| import traceback | |
| from datetime import datetime | |
| log_data = { | |
| "timestamp": datetime.utcnow().isoformat() + "Z", | |
| "level": record.levelname, | |
| "logger": record.name, | |
| "module": record.module, | |
| "function": record.funcName, | |
| "line": record.lineno, | |
| "message": record.getMessage(), | |
| } | |
| # 添加额外字段 | |
| if hasattr(record, "path"): | |
| log_data["path"] = record.path | |
| if hasattr(record, "method"): | |
| log_data["method"] = record.method | |
| if hasattr(record, "user"): | |
| log_data["user"] = str(record.user) | |
| if hasattr(record, "errors"): | |
| log_data["errors"] = record.errors | |
| # 添加异常信息 | |
| if record.exc_info: | |
| log_data["exception"] = "".join(traceback.format_exception(*record.exc_info)) | |
| return self.json.dumps(log_data, ensure_ascii=False) | |
| import logging | |
| import re | |
| from typing import ClassVar | |
| class SensitiveDataFilter(logging.Filter): | |
| """敏感数据过滤器,自动脱敏日志中的敏感信息""" | |
| # 需要完全遮蔽的字段名(不区分大小写) | |
| _SENSITIVE_KEYS = frozenset({"authorization", "token", "password", "secret", "api_key", "apikey"}) | |
| # 消息中需要脱敏的正则模式 | |
| _MSG_PATTERNS: ClassVar[list[tuple[re.Pattern[str], str]]] = [ | |
| (re.compile(r"(Authorization:\s*Bearer\s+)\S+", re.IGNORECASE), r"\1***"), | |
| (re.compile(r"(token\s*=\s*)sk-\S+", re.IGNORECASE), r"\1***"), | |
| (re.compile(r"sk-[A-Za-z0-9]{20,}", re.IGNORECASE), "***"), | |
| ] | |
| def _mask_email(value: str) -> str: | |
| """部分遮蔽邮件地址:保留首2字符和末2字符(含域名末2字符)""" | |
| at_pos = value.find("@") | |
| if at_pos > 0: | |
| full = value | |
| # 保留整体首2字符和末2字符 | |
| if len(full) > 4: | |
| return full[:2] + "***" + full[-2:] | |
| return "***" | |
| # 非邮件字符串,保留首2末2 | |
| if len(value) > 4: | |
| return value[:2] + "***" + value[-2:] | |
| return "***" | |
| def _scrub_value(self, key: str, value: object) -> object: | |
| """根据 key 决定如何脱敏 value""" | |
| if isinstance(value, dict): | |
| return {k: self._scrub_value(k, v) for k, v in value.items()} | |
| if isinstance(value, str): | |
| lower_key = key.lower() | |
| if lower_key in self._SENSITIVE_KEYS: | |
| return "***" | |
| if lower_key in {"account", "email", "username"}: | |
| return self._mask_email(value) | |
| # 检查值本身是否包含敏感 token | |
| if re.search(r"sk-[A-Za-z0-9]{20,}", value, re.IGNORECASE): | |
| return re.sub(r"sk-[A-Za-z0-9]{20,}", "***", value, flags=re.IGNORECASE) | |
| return value | |
| def _scrub_message(self, msg: str) -> str: | |
| for pattern, replacement in self._MSG_PATTERNS: | |
| msg = pattern.sub(replacement, msg) | |
| return msg | |
| def filter(self, record: logging.LogRecord) -> bool: | |
| # 脱敏消息 | |
| record.msg = self._scrub_message(str(record.msg)) | |
| # 脱敏 record 上的额外属性 | |
| for attr in list(vars(record).keys()): | |
| if attr.startswith("_") or attr in { | |
| "name", | |
| "msg", | |
| "args", | |
| "levelname", | |
| "levelno", | |
| "pathname", | |
| "filename", | |
| "module", | |
| "exc_info", | |
| "exc_text", | |
| "stack_info", | |
| "lineno", | |
| "funcName", | |
| "created", | |
| "msecs", | |
| "relativeCreated", | |
| "thread", | |
| "threadName", | |
| "processName", | |
| "process", | |
| "taskName", | |
| }: | |
| continue | |
| val = getattr(record, attr) | |
| setattr(record, attr, self._scrub_value(attr, val)) | |
| return True | |