
一、为什么需要结构化日志?
1.1 传统日志的痛点
大多数 Python 项目的日志长这样:
2026-07-01 10:23:45,123 INFO User logged in: user_id=42
2026-07-01 10:23:46,456 ERROR Failed to process order order_id=9981
这是一段纯文本字符串,看似记录了信息,但在生产环境中面临严峻挑战:
| 问题 | 说明 |
|---|---|
| 难以解析 | 日志被采集到 ELK / Loki 后,需要用 Grok 正则逐字段提取,维护成本高 |
| 缺乏上下文 | 分布式调用链中的 trace_id、request_id 无法自动关联 |
| 格式不一致 | 不同开发者使用不同格式,f-string、% 格式化、logging.info 混用 |
| 敏感信息泄漏 | 密码、Token、身份证号可能被无意中打印 |
1.2 结构化日志的核心理念
结构化日志将每条日志视为一个键值对集合(通常序列化为 JSON),而非自由文本:
{
"timestamp": "2026-07-01T10:23:45.123456+08:00",
"level": "info",
"event": "user_login",
"user_id": 42,
"request_id": "a1b2c3d4",
"service": "order-service",
"module": "auth.views",
"func_name": "login",
"lineno": 57
}
这样做的优势:
- ✅ 机器可读:ELK、Loki、Datadog 可直接索引每个字段,支持 KQL / LogQL 查询
- ✅ 人类可查:开发环境使用彩色 ConsoleRenderer,阅读体验不降级
- ✅ 上下文贯穿:通过
contextvars自动注入trace_id,串联整条调用链 - ✅ 类型安全:字段值为
int、bool、float等原生类型,无需二次转换
二、技术选型:为什么是 structlog?
Python 生态中常见的日志方案对比:
| 方案 | 结构化支持 | 上下文绑定 | Processor 链 | 与标准库兼容 | 生产推荐度 |
|---|---|---|---|---|---|
logging (标准库) | ❌ 需手写 Formatter | ❌ | ❌ | ✅ 原生 | ⭐⭐ |
loguru | ⚠️ serialize=True | ⚠️ 有限 | ❌ | ❌ 独立体系 | ⭐⭐⭐ |
structlog | ✅ JSONRenderer | ✅ bind/contextvars | ✅ 完整 | ✅ 可桥接 | ⭐⭐⭐⭐⭐ |
structlog 的核心优势在于其 processor 链 设计——你可以在日志序列化前,对每条日志做任意变换:注入时间戳、过滤敏感字段、添加调用位置、绑定上下文变量等。这种"管道式"架构,使得日志行为高度可定制且逻辑清晰。
安装
pip install structlog
如需与标准库桥接(推荐),无需额外依赖,structlog 内置了 structlog.stdlib 模块。
三、完整配置方案:开发环境 vs 生产环境
3.1 核心配置代码
以下是一份可直接用于生产的完整配置,通过环境变量自动切换开发 / 生产模式:
# app/logging_config.py
import logging
import os
import sys
import structlog
def setup_logging() -> None:
"""
初始化结构化日志系统。
- 开发环境 (LOG_ENV != "prod"):彩色控制台输出,便于阅读
- 生产环境 (LOG_ENV == "prod"):JSON 行输出,便于 ELK/Loki 采集
"""
is_production = os.getenv("LOG_ENV", "dev").lower() == "prod"
log_level_name = os.getenv("LOG_LEVEL", "INFO").upper()
log_level = getattr(logging, log_level_name, logging.INFO)
# ──────────────────────────────────────────────
# 1. 配置标准库 logging(作为底层输出通道)
# ──────────────────────────────────────────────
logging.basicConfig(
format="%(message)s",
stream=sys.stdout,
level=log_level,
force=True, # 覆盖已有配置,避免重复 Handler
)
# 降低第三方库日志级别,减少噪声
for noisy_logger in ("uvicorn", "uvicorn.access", "sqlalchemy", "httpx", "urllib3"):
logging.getLogger(noisy_logger).setLevel(logging.WARNING)
# ──────────────────────────────────────────────
# 2. 构建 structlog processor 链
# ──────────────────────────────────────────────
# 共享处理器:无论开发还是生产都需要
shared_processors: list = [
# 2.1 将 contextvars 中的绑定变量合并到事件字典
structlog.contextvars.merge_contextvars,
# 2.2 按标准库 Logger 的 level 过滤(确保 LOG_LEVEL 生效)
structlog.stdlib.filter_by_level,
# 2.3 注入标准库 logger 名称(即 __name__)
structlog.stdlib.add_logger_name,
# 2.4 注入日志级别
structlog.stdlib.add_log_level,
# 2.5 注入调用位置(文件名、函数名、行号)
structlog.processors.CallsiteParameterAdder(
[
structlog.processors.CallsiteParameter.FILENAME,
structlog.processors.CallsiteParameter.FUNC_NAME,
structlog.processors.CallsiteParameter.LINENO,
]
),
# 2.6 注入 ISO 8601 时间戳(带时区)
structlog.processors.TimeStamper(fmt="iso"),
# 2.7 格式化异常堆栈(exc_info=True 时自动提取 traceback)
structlog.processors.format_exc_info,
# 2.8 将事件字典中的列表/字典等序列化为 Unicode 字符串
structlog.processors.UnicodeDecoder(),
# 2.9 将事件字典排序,保证输出字段顺序一致(便于 diff)
structlog.processors.SortedDictSorter(),
]
# ──────────────────────────────────────────────
# 3. 根据环境选择渲染器
# ──────────────────────────────────────────────
if is_production:
# 生产环境:JSON 行格式,一行一条日志
renderer: structlog.types.Processor = structlog.processors.JSONRenderer(
ensure_ascii=False # 保留中文字符,不转义为 \uXXXX
)
else:
# 开发环境:彩色控制台,人类友好
renderer = structlog.dev.ConsoleRenderer(
colors=True,
pad_event=35, # event 字段对齐宽度
)
# ──────────────────────────────────────────────
# 4. 全局配置 structlog
# ──────────────────────────────────────────────
structlog.configure(
processors=[
*shared_processors,
# 将 structlog 事件字典转换为标准库 LogRecord
structlog.stdlib.ProcessorFormatter.wrap_events_in_event_key,
# 桥接标准库:通过标准库 Handler 输出
structlog.stdlib.ProcessorFormatter(
processor=renderer,
foreign_pre_chain=shared_processors,
),
],
# 使用标准库 LoggerFactory,使得 structlog 复用 logging 的 Handler
logger_factory=structlog.stdlib.LoggerFactory(),
# 缓存 logger 实例,避免每次调用都创建
cache_logger_on_first_use=True,
)
3.2 使用 ProcessorFormatter 的简化方案
上面用桥接方式较复杂,如果项目完全由你控制,更推荐直接使用 ProcessorFormatter 配合标准库:
# app/logging_config_simple.py(推荐方案)
import logging
import os
import sys
import structlog
def setup_logging() -> None:
is_production = os.getenv("LOG_ENV", "dev").lower() == "prod"
log_level_name = os.getenv("LOG_LEVEL", "INFO").upper()
log_level = getattr(logging, log_level_name, logging.INFO)
# 共享 processor
shared_processors: list = [
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.CallsiteParameterAdder(
[
structlog.processors.CallsiteParameter.FILENAME,
structlog.processors.CallsiteParameter.FUNC_NAME,
structlog.processors.CallsiteParameter.LINENO,
]
),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.SortedDictSorter(),
]
# 渲染器
if is_production:
renderer = structlog.processors.JSONRenderer(ensure_ascii=False)
else:
renderer = structlog.dev.ConsoleRenderer(colors=True)
# 构建 Formatter
formatter = structlog.stdlib.ProcessorFormatter(
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
renderer,
],
foreign_pre_chain=shared_processors,
)
# 配置 Handler
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
handler.setLevel(log_level)
# 配置 Root Logger
root_logger = logging.getLogger()
root_logger.handlers.clear()
root_logger.addHandler(handler)
root_logger.setLevel(log_level)
# 降噪
for name in ("uvicorn", "uvicorn.access", "sqlalchemy", "httpx", "urllib3"):
logging.getLogger(name).setLevel(logging.WARNING)
# 配置 structlog
structlog.configure(
processors=[
*shared_processors,
structlog.stdlib.ProcessorFormatter.wrap_events_in_event_key,
],
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
3.3 在应用入口调用
# main.py
from app.logging_config import setup_logging
# 必须在其他模块导入 logger 之前调用
setup_logging()
import structlog
logger = structlog.get_logger(__name__)
logger.info("application_started", version="2.3.1", env="production")
四、日志分级策略
4.1 五级日志的使用规范
| 级别 | 用途 | 生产是否默认开启 | 示例 |
|---|---|---|---|
| DEBUG | 开发调试细节,函数入参、SQL 语句、中间状态 | ❌ | logger.debug("sql_query", query="SELECT ...", duration_ms=12) |
| INFO | 业务关键节点,正常流程里程碑 | ✅ | logger.info("order_created", order_id="O-9981", amount=299.0) |
| WARNING | 可恢复的异常,降级、重试、配置缺失 | ✅ | logger.warning("cache_miss_fallback_to_db", key="user:42") |
| ERROR | 不可恢复的错误,需要人工介入 | ✅ | logger.error("payment_failed", order_id="O-9981", exc_info=True) |
| CRITICAL | 服务即将崩溃,如数据库连接池耗尽 | ✅ | logger.critical("db_pool_exhausted", active_conns=100) |
4.2 分级策略的黄金法则
# ✅ 正确:用 exc_info=True 自动捕获异常堆栈
try:
result = payment_gateway.charge(order_id, amount)
except PaymentError:
logger.error(
"payment_gateway_error",
order_id=order_id,
amount=amount,
exc_info=True, # 自动记录完整 traceback
)
# ❌ 错误:手动拼字符串,丢失结构化信息
except PaymentError as e:
logger.error(f"Payment failed for order {order_id}: {e}")
# ✅ 正确:WARNING 用于可自动恢复的情况
try:
cached = redis_client.get(cache_key)
except redis.ConnectionError:
logger.warning("redis_unavailable_fallback_to_db", cache_key=cache_key)
cached = db.query(cache_key) # 降级到数据库
# ❌ 错误:将降级也记为 ERROR,导致告警疲劳
except redis.ConnectionError:
logger.error("redis_down", cache_key=cache_key) # 半夜触发 on-call 告警
4.3 通过环境变量动态控制
# 生产环境默认 INFO
export LOG_ENV=prod
export LOG_LEVEL=INFO
# 排查线上问题时,临时调为 DEBUG(无需重启代码)
export LOG_LEVEL=DEBUG
提示:如果需要运行时动态调整日志级别(不重启进程),可以通过暴露一个 HTTP 端点或监听信号(如
SIGUSR1)来修改logging.getLogger().setLevel()。
五、请求上下文追踪:trace_id 贯穿全链路
5.1 利用 contextvars 自动注入
在 Web 中间件中为每个请求生成唯一 request_id 和 trace_id,绑定到 contextvars,后续所有日志自动携带:
# app/middleware.py (以 FastAPI 为例)
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
import structlog
class LoggingContextMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 从请求头获取上游 trace_id,或自行生成
trace_id = request.headers.get("X-Trace-Id", str(uuid.uuid4()))
request_id = str(uuid.uuid4())
# 绑定到 contextvars(协程安全)
structlog.contextvars.bind_contextvars(
trace_id=trace_id,
request_id=request_id,
method=request.method,
path=request.url.path,
)
logger = structlog.get_logger("access")
logger.info("request_started", client_ip=request.client.host)
try:
response = await call_next(request)
logger.info(
"request_completed",
status_code=response.status_code,
)
# 将 trace_id 回传给客户端
response.headers["X-Trace-Id"] = trace_id
return response
except Exception:
logger.error("request_failed", exc_info=True)
raise
finally:
# 请求结束,清理上下文
structlog.contextvars.clear_contextvars()
5.2 日志输出效果
生产环境 (JSON):
{
"timestamp": "2026-07-01T10:23:45.123456+08:00",
"level": "info",
"event": "request_started",
"logger": "access",
"filename": "middleware.py",
"func_name": "dispatch",
"lineno": 28,
"trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"request_id": "f7e8d9c0-b1a2-3456-7890-abcdef123456",
"method": "POST",
"path": "/api/v1/orders",
"client_ip": "10.0.1.23"
}
开发环境 (Console):
2026-07-01 10:23:45 [info ] request_started client_ip=10.0.1.23 filename=middleware.py func_name=dispatch lineno=28 method=POST path=/api/v1/orders request_id=f7e8d9c0-... trace_id=a1b2c3d4-...
六、敏感字段脱敏
在生产日志中,绝不能出现密码、Token、身份证号等敏感信息。通过自定义 processor 在序列化前拦截:
# app/log_processors.py
import structlog
# 需要脱敏的字段名集合
SENSITIVE_FIELDS = frozenset({
"password", "token", "secret", "api_key", "authorization",
"credit_card", "id_number", "ssn", "phone",
})
def redact_sensitive_fields(
logger: structlog.types.WrappedLogger,
method: str,
event_dict: dict,
) -> dict:
"""
将敏感字段值替换为 '***REDACTED***'。
递归检查嵌套字典(深度限制为 3 层)。
"""
def _redact(d: dict, depth: int = 0) -> dict:
if depth > 3:
return d
for key in list(d.keys()):
if key.lower() in SENSITIVE_FIELDS:
d[key] = "***REDACTED***"
elif isinstance(d[key], dict):
d[key] = _redact(d[key], depth + 1)
return d
return _redact(event_dict)
将脱敏 processor 插入到 processor 链中(放在渲染器之前):
shared_processors = [
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.format_exc_info,
# 👇 脱敏处理器放在序列化之前
redact_sensitive_fields,
structlog.processors.UnicodeDecoder(),
structlog.processors.SortedDictSorter(),
]
七、注入全局静态字段
每个服务实例的日志都应包含服务标识,便于在 ELK/Loki 中按服务过滤:
import structlog
import socket
def add_service_context(
logger: structlog.types.WrappedLogger,
method: str,
event_dict: dict,
) -> dict:
"""注入服务级静态上下文字段。"""
event_dict.setdefault("service", "order-service")
event_dict.setdefault("environment", os.getenv("APP_ENV", "dev"))
event_dict.setdefault("hostname", socket.gethostname())
event_dict.setdefault("pid", os.getpid())
return event_dict
将其加入 processor 链的最前面:
shared_processors = [
add_service_context, # 👈 最先执行
structlog.contextvars.merge_contextvars,
# ... 其余 processor
]
输出效果:
{
"timestamp": "2026-07-01T10:23:45.123456+08:00",
"level": "info",
"event": "order_created",
"service": "order-service",
"environment": "production",
"hostname": "order-pod-7b9f4-xk2mz",
"pid": 1,
"order_id": "O-9981",
"amount": 299.0,
"trace_id": "a1b2c3d4-..."
}
八、接入 ELK(Elasticsearch + Logstash + Kibana)
8.1 架构总览
Python 服务 ──(JSON stdout)──> Filebeat ──> Logstash ──> Elasticsearch
│
Kibana
8.2 Filebeat 配置
Filebeat 负责从容器/宿主机的标准输出中采集 JSON 行日志:
# filebeat.yml
filebeat.inputs:
- type: container
paths:
- "/var/log/containers/order-service-*.log"
# 将每行当作 JSON 解析
json.keys_under_root: true
json.add_error_key: true
json.message_key: event
output.logstash:
hosts: ["logstash:5044"]
# 添加元数据标签,便于 Kibana 过滤
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
- add_fields:
target: ""
fields:
log_source: python-structlog
8.3 Logstash Pipeline 配置
# logstash/conf.d/python-app.conf
input {
beats {
port => 5044
}
}
filter {
# 确保时间戳被正确解析
if [timestamp] {
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
}
}
# 将 level 字段映射为 Elasticsearch 的 log.level
mutate {
rename => { "level" => "log.level" }
}
# 根据日志级别打标签
if [log][level] == "error" or [log][level] == "critical" {
mutate { add_tag => ["alert_candidate"] }
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "python-app-%{+YYYY.MM.dd}"
}
}
8.4 Kibana 查询示例
接入后,你可以在 Kibana 中使用 KQL 进行精准查询:
# 查找某次请求的完整链路
trace_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# 查找最近 1 小时的所有 ERROR
log.level: "error" AND @timestamp > now-1h
# 按服务 + 级别聚合
service: "order-service" AND log.level: "warning"
九、接入 Grafana Loki
9.1 架构总览
Python 服务 ──(JSON stdout)──> Promtail ──> Loki ──> Grafana
Loki 与 ELK 的核心区别:Loki 不索引日志内容,只索引标签(labels),因此成本更低、部署更轻量。
9.2 Promtail 配置
# promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: python-app
static_configs:
- targets: [localhost]
labels:
job: order-service
__path__: /var/log/app/*.json.log
pipeline_stages:
# 解析 JSON 日志
- json:
expressions:
level: level
service: service
trace_id: trace_id
event: event
# 将解析出的字段提升为 Loki 标签
- labels:
level:
service:
# 设置时间戳
- timestamp:
source: timestamp
format: RFC3339Nano
# 保留完整 JSON 作为日志行
- output:
source: event
9.3 Grafana LogQL 查询
# 查找指定 trace_id 的所有日志
{service="order-service"} |= `"trace_id": "a1b2c3d4"`
# 按级别过滤
{service="order-service", level="error"}
# 统计最近 5 分钟各级别日志量
count_over_time({service="order-service"} |= `` [5m]) by (level)
# 查找包含异常堆栈的日志
{service="order-service"} | json | level="error" | line_format "{{.event}} {{.exception}}"
十、实战:FastAPI 完整集成示例
# app/main.py
import os
import uuid
import logging
import sys
import structlog
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
# ═══════════════════════════════════════
# 第一步:初始化日志(必须在最前面)
# ═══════════════════════════════════════
SENSITIVE_FIELDS = frozenset({
"password", "token", "secret", "api_key", "authorization",
})
def redact_sensitive_fields(logger, method, event_dict):
for key in list(event_dict.keys()):
if key.lower() in SENSITIVE_FIELDS:
event_dict[key] = "***REDACTED***"
return event_dict
def add_service_context(logger, method, event_dict):
event_dict.setdefault("service", "order-service")
event_dict.setdefault("environment", os.getenv("APP_ENV", "dev"))
return event_dict
def setup_logging():
is_prod = os.getenv("LOG_ENV", "dev").lower() == "prod"
log_level = getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO)
shared_processors = [
add_service_context,
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.CallsiteParameterAdder([
structlog.processors.CallsiteParameter.FILENAME,
structlog.processors.CallsiteParameter.FUNC_NAME,
structlog.processors.CallsiteParameter.LINENO,
]),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.format_exc_info,
redact_sensitive_fields,
structlog.processors.UnicodeDecoder(),
structlog.processors.SortedDictSorter(),
]
renderer = (
structlog.processors.JSONRenderer(ensure_ascii=False)
if is_prod
else structlog.dev.ConsoleRenderer(colors=True)
)
formatter = structlog.stdlib.ProcessorFormatter(
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
renderer,
],
foreign_pre_chain=shared_processors,
)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
handler.setLevel(log_level)
root = logging.getLogger()
root.handlers.clear()
root.addHandler(handler)
root.setLevel(log_level)
structlog.configure(
processors=[
*shared_processors,
structlog.stdlib.ProcessorFormatter.wrap_events_in_event_key,
],
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
setup_logging()
# ═══════════════════════════════════════
# 第二步:创建 FastAPI 应用
# ═══════════════════════════════════════
app = FastAPI(title="Order Service")
logger = structlog.get_logger("order_service")
class AccessLogMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
trace_id = request.headers.get("X-Trace-Id", str(uuid.uuid4()))
request_id = str(uuid.uuid4())
structlog.contextvars.bind_contextvars(
trace_id=trace_id,
request_id=request_id,
method=request.method,
path=request.url.path,
)
log = structlog.get_logger("access")
log.info("request_started", client_ip=request.client.host)
try:
response = await call_next(request)
log.info("request_completed", status_code=response.status_code)
response.headers["X-Trace-Id"] = trace_id
return response
except Exception:
log.error("request_failed", exc_info=True)
raise
finally:
structlog.contextvars.clear_contextvars()
app.add_middleware(AccessLogMiddleware)
# ═══════════════════════════════════════
# 第三步:业务代码
# ═══════════════════════════════════════
@app.post("/api/v1/orders")
async def create_order(request: Request):
body = await request.json()
log = structlog.get_logger("orders")
order_id = f"O-{uuid.uuid4().hex[:8]}"
log.info(
"order_created",
order_id=order_id,
user_id=body.get("user_id"),
amount=body.get("amount"),
# 注意:即使不小心传了敏感字段,也会被脱敏
token=body.get("token"),
)
# 模拟业务处理
if body.get("amount", 0) > 10000:
log.warning(
"high_value_order",
order_id=order_id,
amount=body["amount"],
)
return {"order_id": order_id, "status": "created"}
@app.get("/health")
async def health():
return {"status": "healthy"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
运行效果
开发环境 (LOG_ENV=dev):
2026-07-01 18:30:00 [info ] request_started client_ip=127.0.0.1 environment=dev filename=main.py func_name=dispatch lineno=84 method=POST path=/api/v1/orders request_id=f7e8d9c0-... service=order-service trace_id=a1b2c3d4-...
2026-07-01 18:30:00 [info ] order_created amount=299.0 environment=dev filename=main.py func_name=create_order lineno=109 order_id=O-a1b2c3d4 service=order-service token=***REDACTED*** trace_id=a1b2c3d4-... user_id=42
2026-07-01 18:30:00 [info ] request_completed status_code=200 ...
生产环境 (LOG_ENV=prod):
{
"amount": 299.0,
"environment": "production",
"event": "order_created",
"filename": "main.py",
"func_name": "create_order",
"level": "info",
"lineno": 109,
"logger": "orders",
"order_id": "O-a1b2c3d4",
"service": "order-service",
"timestamp": "2026-07-01T18:30:00.123456+08:00",
"token": "***REDACTED***",
"trace_id": "a1b2c3d4-e5f6-7890",
"user_id": 42
}
十一、字段命名规范
为确保跨服务一致性以及 ELK/Loki 的查询效率,制定统一的字段命名规范:
| 规则 | 说明 | 示例 |
|---|---|---|
| 命名风格 | snake_case,全小写 + 下划线 | user_id、order_id ✅ |
| 禁用风格 | PascalCase、camelCase、带空格 | UserId、orderId ❌ |
| 事件名称 | 动词_名词,snake_case | order_created、payment_failed |
| 时间字段 | 使用 timestamp,ISO 8601 带时区 | 2026-07-01T10:23:45.123+08:00 |
| ID 字段 | 统一为字符串类型 | "user_id": "u-42" |
| 布尔字段 | 使用 is_ 前缀 | is_retry、is_internal |
| 数值字段 | 使用带单位的后缀 | duration_ms、size_bytes |
十二、生产环境注意事项与踩坑指南
12.1 避免日志性能瓶颈
# ❌ 错误:每次日志调用都做昂贵的字符串运算
logger.debug(f"Full response body: {json.dumps(response_body, indent=2)}")
# ✅ 正确:structlog 的 lazy 机制——如果 DEBUG 未启用,processor 链不会执行
logger.debug("response_body", body=response_body)
12.2 多进程场景
在 Gunicorn / uWSGI 多 worker 模式下,每个 worker 是独立进程,各自调用 setup_logging() 即可。JSON 行格式天然支持多进程写入(每行是原子操作),但需注意:
- 使用
StreamHandler(sys.stdout)而非文件写入 - 由 Filebeat/Promtail 在进程外统一采集
- 避免使用
RotatingFileHandler(多进程下文件锁冲突)
12.3 异常日志的完整捕获
# ✅ 使用 exc_info=True 而非手动 traceback.format_exc()
try:
risky_operation()
except Exception:
logger.error(
"operation_failed",
operation="risky_operation",
exc_info=True, # structlog 会自动格式化完整堆栈
)
12.4 避免 Handler 重复添加
# 在 setup_logging() 中务必清理已有 Handler
root = logging.getLogger()
root.handlers.clear() # 👈 防止重复添加
root.addHandler(handler)
十三、总结
| 维度 | 方案 |
|---|---|
| 日志库 | structlog + 标准库 logging 桥接 |
| 输出格式 | 开发:ConsoleRenderer(彩色);生产:JSONRenderer(JSON 行) |
| 分级策略 | DEBUG/INFO/WARNING/ERROR/CRITICAL,通过 LOG_LEVEL 环境变量控制 |
| 上下文追踪 | contextvars + 中间件自动注入 trace_id、request_id |
| 敏感脱敏 | 自定义 redact_sensitive_fields processor |
| 全局字段 | 自定义 add_service_context processor 注入 service、hostname |
| ELK 接入 | Filebeat 采集 JSON → Logstash 标准化 → Elasticsearch 索引 |
| Loki 接入 | Promtail 解析 JSON 标签 → Loki 存储 → Grafana LogQL 查询 |
| 字段规范 | snake_case、事件名 verb_noun、时间 ISO 8601、数值带单位 |
结构化日志不是锦上添花,而是生产系统的生命线。 当你凌晨三点被 on-call 叫醒,打开 Kibana 用 trace_id 一搜就能看到完整调用链时,你会感谢今天做的每一个设计决策。
自我审查清单
| 检查项 | 状态 | 说明 |
|---|---|---|
| structlog API 调用是否正确 | ✅ | configure、get_logger、bind_contextvars 等均为当前稳定版 API |
| Processor 链顺序是否合理 | ✅ | merge_contextvars → add_log_level → TimeStamper → format_exc_info → 脱敏 → Renderer |
| JSONRenderer 参数 | ✅ | ensure_ascii=False 正确处理中文 |
| ProcessorFormatter 用法 | ✅ | foreign_pre_chain 确保标准库日志也被结构化 |
| contextvars 线程/协程安全 | ✅ | bind_contextvars 基于 contextvars.ContextVar,协程安全 |
| 分级策略是否符合最佳实践 | ✅ | WARNING 用于可恢复、ERROR 用于不可恢复 |
| 敏感字段脱敏实现 | ✅ | 递归字典扫描,深度限制防止死循环 |
| Filebeat JSON 解析配置 | ✅ | json.keys_under_root: true 将字段提升到根级别 |
| Promtail pipeline_stages | ✅ | json 解析 → labels 提升 → timestamp 设置 |
| 代码可直接运行 | ✅ | FastAPI 示例完整,包含 middleware、业务代码、健康检查 |
| 字段命名一致性 | ✅ | 全文使用 snake_case,事件名使用 verb_noun |
| 无过时/废弃 API | ✅ | 未使用 structlog.get_logger() 的已弃用签名 |
365

被折叠的 条评论
为什么被折叠?



