生产级日志配置:高效排查与安全脱敏(生产级日志formatters配置实践)

生产环境中,日志的详细程度需要在「可排查性」和「性能/存储成本」之间取得平衡。

下面是一套 生产级日志格式(formatters)配置方案,兼顾了:

  • 详细信息(便于问题排查、审计)
  • 结构化输出(支持 ELK/Sentry/日志平台解析)
  • 可读性与压缩性平衡(避免日志爆炸)
  • 安全脱敏(敏感信息不泄露)

🎯 生产环境日志需求清单

要求 说明
1. 包含完整调用链信息 trace_id, span_id, request_id
2. 包含请求上下文 如用户 ID、IP、接口路径
3. 包含异常堆栈信息(错误时) 但不暴露敏感代码
4. 输出为结构化 JSON 便于日志系统(如 ELK、Graylog、Sentry)分析
5. 敏感信息自动脱敏 如密码、token、手机号等
6. 支持日志级别区分(DEBUG 仅开发) 生产只输出 INFO 及以上

✅ 推荐配置:production-formatter.yaml

yaml 复制代码
version: 1

formatters:
  structured:
    format: >
      {
        "timestamp": "%(asctime)s",
        "level": "%(levelname)s",
        "logger": "%(name)s",
        "thread": "%(thread)d",
        "process": "%(process)d",
        "trace_id": "%(trace_id)s",
        "request_id": "%(request_id)s",
        "user_id": "%(user_id)s",
        "client_ip": "%(client_ip)s",
        "endpoint": "%(endpoint)s",
        "function": "%(funcName)s:%(lineno)d",
        "message": "%(message)s",
        "exception": "%(exc_info)s"
      }
    datefmt: "%Y-%m-%d %H:%M:%S"
    class: logging.Formatter

  simple:
    format: "%(name)s | %(levelname)s | %(message)s"
    datefmt: "%Y-%m-%d %H:%M:%S"

🔐 敏感信息脱敏 ------ 用 filter + contextvars 实现

✅ 步骤 1:创建脱敏过滤器(filters/sensitive_filter.py

python 复制代码
import logging
import re
from contextvars import ContextVar

# 全局上下文变量:当前请求上下文
_request_context: ContextVar[dict] = ContextVar("request_context", default={})

# 脱敏规则:匹配敏感字段并替换
SENSITIVE_PATTERNS = {
    "password": re.compile(r'(?i)(password|pwd|pass|secret|token|key|api_key|auth|bearer)\s*[:=]\s*["\']?([a-zA-Z0-9_\-+=%]*)["\']?', re.IGNORECASE),
    "phone": re.compile(r'\b(\+?86)?1[3-9]\d{9}\b'),
    "id_card": re.compile(r'\b(\d{17}[\dXx])\b'),
    "email": re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b'),
}

def mask_sensitive(text: str) -> str:
    for key, pattern in SENSITIVE_PATTERNS.items():
        # 替换为 ***,保留原长度
        text = pattern.sub(lambda m: f'{m.group(1)}: ***', text)
    return text

class SensitiveFilter(logging.Filter):
    def filter(self, record):
        # 从上下文中获取当前请求上下文
        ctx = _request_context.get({})
        user_id = ctx.get("user_id", "N/A")
        client_ip = ctx.get("client_ip", "N/A")
        request_id = ctx.get("request_id", "N/A")
        trace_id = ctx.get("trace_id", "N/A")

        # 写入 record 的额外字段(方便 formatter 使用)
        record.user_id = user_id
        record.client_ip = client_ip
        record.request_id = request_id
        record.trace_id = trace_id
        record.endpoint = ctx.get("endpoint", "unknown")

        # 如果消息是字符串,尝试脱敏
        if hasattr(record, "message") and isinstance(record.message, str):
            record.message = mask_sensitive(record.message)

        # 如果有异常信息,也脱敏
        if hasattr(record, "exc_info") and record.exc_info:
            exc_str = str(record.exc_info[1])
            record.exc_info = (record.exc_info[0], mask_sensitive(exc_str), record.exc_info[2])

        return True

✅ 步骤 2:在 main.py 中注册过滤器

python 复制代码
import logging.config
import yaml
import os
from contextvars import ContextVar
from filters.sensitive_filter import SensitiveFilter, _request_context

# 设置全局上下文变量
_request_context = ContextVar("request_context", default={})

# 启动时加载日志配置
def setup_logging(config_path="config/logging.yaml"):
    with open(config_path, "r", encoding="utf-8") as f:
        config = yaml.safe_load(f)

    # 为每个 handler 添加脱敏过滤器
    for handler_name in config["handlers"]:
        handler = config["handlers"][handler_name]
        if "filters" not in handler:
            handler["filters"] = []
        handler["filters"].append("sensitive_filter")

    # 注册过滤器
    logging.addFilter(SensitiveFilter())

    logging.config.dictConfig(config)
    print("✅ 生产日志系统已配置完成(含脱敏)!")

# 全局上下文注入函数(如中间件中调用)
def set_request_context(**kwargs):
    _request_context.set(kwargs)

✅ 步骤 3:在 Web 框架(如 FastAPI)中注入上下文

python 复制代码
from fastapi import Request
from main import set_request_context

@app.middleware("http")
async def add_request_context(request: Request, call_next):
    # 提取上下文
    ctx = {
        "user_id": request.headers.get("X-User-ID", "anonymous"),
        "client_ip": request.client.host,
        "request_id": request.headers.get("X-Request-ID", "unknown"),
        "trace_id": request.headers.get("X-Trace-ID", "unknown"),
        "endpoint": request.url.path,
    }
    set_request_context(**ctx)

    response = await call_next(request)
    return response

📊 示例输出(JSON 格式)

json 复制代码
{
  "timestamp": "2026-02-06 15:20:10",
  "level": "ERROR",
  "logger": "app.api",
  "thread": 12345,
  "process": 6789,
  "trace_id": "abc123",
  "request_id": "req-456",
  "user_id": "u-789",
  "client_ip": "192.168.1.100",
  "endpoint": "/api/v1/users/123",
  "function": "get_user:15",
  "message": "Failed to fetch user data: invalid token provided",
  "exception": "Traceback (most recent call last):\n  File \"app/api.py\", line 15, in get_user\n    raise ValueError('invalid token')\nValueError: invalid token"
}

✅ 总结:生产日志配置最佳实践

项目 推荐做法
📄 日志格式 JSON 结构化输出
📌 信息维度 trace_id, request_id, user_id, client_ip, endpoint
🛡️ 安全脱敏 使用 logging.Filter 自动脱敏敏感字段
📂 日志级别 生产环境:INFO 及以上,DEBUG 仅开发
📊 日志平台 推荐接入 ELK、Graylog、Sentry、Datadog 等