配置、日志与敏感信息
本节目标
把会随环境变化的值从代码中分离,启动时验证配置,并产生日志证据,使一次 Agent 工具调用可以定位、关联和复盘,同时不记录密钥、完整提示词或不必要的个人数据。
配置不是散落的全局变量
配置回答“同一份代码在这个环境如何运行”,例如超时、并发上限、服务地址和功能开关。业务输入回答“这一次要处理什么”。密钥属于受保护配置,但不应进入仓库。
优先级必须明确,例如:
代码中的安全默认值 < 配置文件 < 环境变量 < 明确命令行参数并非所有项目都需要四层。关键是只有一个集中解析入口,启动时一次性得到已校验对象:
import os
from dataclasses import dataclass, field
class ConfigError(ValueError):
pass
@dataclass(frozen=True, slots=True)
class Settings:
request_timeout_seconds: float
max_concurrency: int
api_key: str = field(repr=False)
def load_settings(environ: dict[str, str] | None = None) -> Settings:
source = os.environ if environ is None else environ
api_key = source.get("EXAMPLE_API_KEY", "").strip()
if not api_key:
raise ConfigError("缺少 EXAMPLE_API_KEY")
try:
timeout = float(source.get("REQUEST_TIMEOUT_SECONDS", "20"))
concurrency = int(source.get("MAX_CONCURRENCY", "4"))
except ValueError as exc:
raise ConfigError("超时和并发量必须是数字") from exc
if not 0 < timeout <= 120:
raise ConfigError("REQUEST_TIMEOUT_SECONDS 必须在 (0, 120] 内")
if not 1 <= concurrency <= 32:
raise ConfigError("MAX_CONCURRENCY 必须在 [1, 32] 内")
return Settings(timeout, concurrency, api_key)把 environ 作为参数可让测试传入假字典,而不污染真实进程环境。
repr=False 可减少调试输出意外显示密钥,但不是安全边界:属性仍可被读取、序列化或由其他日志记录。更高风险系统应使用专门的秘密提供者,并避免让秘密对象进入通用配置打印、异常和遥测流程。
密钥只提供占位说明
仓库可包含 .env.example,但只能放变量名和无效占位符:
EXAMPLE_API_KEY=replace-with-your-own-key
REQUEST_TIMEOUT_SECONDS=20
MAX_CONCURRENCY=4真实 .env、令牌、cookies 和服务账号文件不得进入 vault 或 Git。生产环境优先使用平台密钥管理服务,并限制读取身份、用途和生命周期。密钥一旦进入 Git 历史,仅新增 .gitignore 不能消除泄漏;应停止提交、撤销/轮换凭据,并按授权处理历史。
日志与用户输出是两条通道
- 用户输出:任务结果或可操作的失败说明;
- 诊断日志:时间、级别、事件、关联标识、耗时和稳定错误类别;
- 指标:可聚合的计数与分布;
- trace:一次请求跨步骤或服务的因果路径。
库模块只获取 logger 并产生日志,由应用入口配置 handler 和级别:
import logging
logger = logging.getLogger(__name__)
def run_task(task_id: str) -> None:
# process 与 KnownDependencyError 由应用的执行适配器定义。
logger.info("task_started task_id=%s", task_id)
try:
process(task_id)
except KnownDependencyError:
logger.exception("task_failed task_id=%s category=dependency", task_id)
raise
else:
logger.info("task_completed task_id=%s", task_id)logger.info("count=%d", count) 使用延迟格式化;不要为了日志先构造昂贵字符串。logger.exception() 应在异常上下文中使用,自动附带堆栈。
入口配置示例:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)basicConfig() 在 root logger 已有 handler 时可能不生效。应用应统一配置一次;可复用库不要擅自调用它。生产环境若使用 JSON 日志,应由已审核的日志适配器统一编码,不能手拼看似 JSON 的字符串。
记录什么,避免记录什么
推荐字段:
trace_id/operation_id:串联同一次操作;- 稳定的事件名与错误类别;
- 阶段、尝试次数、耗时、输入/输出大小;
- 模型、工具或配置的非敏感版本标识;
- 是否命中缓存、降级或人工审批。
默认不记录:
- API key、Authorization header、cookie、连接字符串;
- 完整提示词、模型回复、上传文档或用户隐私数据;
- 本机绝对路径、原始数据库记录和完整外部错误正文;
- 任何“以后可能有用”但没有诊断目的的数据。
若业务确需内容级审计,应先定义目的、最小字段、脱敏、访问控制、保留期限和删除流程。字符串替换式“脱敏”无法保证覆盖嵌套对象、编码值或新字段,最好在结构化数据层采用允许列表。
关联标识与幂等键不同
trace_id 用来观察一次执行;幂等键用来让服务识别重复业务请求。二者可能同时出现,但不能互换。关联标识也不应直接采用邮箱、身份证号等个人信息。
常见错误
| 错误 | 后果 | 改进 |
|---|---|---|
| 模块导入时读取所有环境变量 | 测试和复用困难 | 在应用启动边界显式加载 |
| 缺配置时静默使用危险默认值 | 行为不可预测 | 启动即失败并指出变量名 |
| 把完整请求对象写日志 | 泄漏凭据和个人数据 | 字段允许列表与最小化 |
用 print 代替诊断日志 | 无级别、时间和关联上下文 | 日志给诊断,stdout 给结果 |
| 每层重复记录同一堆栈 | 噪声和成本上升 | 在有上下文且负责处理的边界记录 |
练习
- 为
load_settings()写成功、缺密钥、非法超时、并发越界四组测试。 - 设计一个工具日志事件:列出允许字段、禁止字段和保留期。
- 给任务队列项目增加
--verbose,但不得改变 JSON 标准输出;说明日志应写 stdout 还是 stderr。 - 给嵌套字典写“字段允许列表”转换,证明新增未知字段不会被自动写日志。
自测
- 能区分业务输入、非敏感配置和密钥。
- 能解释为什么配置应在启动时集中校验。
- 能区分结果、日志、指标和 trace。
- 能设计不会泄漏内容的关联字段。
- 知道凭据已被 Git 跟踪时不能靠
.gitignore补救。
相关概念与下一步
- 前置:异常、超时、重试与资源管理。
- 下一节:单元测试、Mock 与回归。
- 生产日志与告警进入 运行监控;隐私边界进入 AI安全 与 隐私计算。
参考资料
获取日期:2026-07-14。