在生产环境中,日志的详细程度需要在「可排查性」和「性能/存储成本」之间取得平衡。
下面是一套 生产级日志格式(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 等 |