配置、日志与敏感信息

本节目标

把会随环境变化的值从代码中分离,启动时验证配置,并产生日志证据,使一次 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 给结果
每层重复记录同一堆栈噪声和成本上升在有上下文且负责处理的边界记录

练习

  1. 为 load_settings() 写成功、缺密钥、非法超时、并发越界四组测试。
  2. 设计一个工具日志事件:列出允许字段、禁止字段和保留期。
  3. 给任务队列项目增加 --verbose,但不得改变 JSON 标准输出;说明日志应写 stdout 还是 stderr。
  4. 给嵌套字典写“字段允许列表”转换,证明新增未知字段不会被自动写日志。

自测

  • 能区分业务输入、非敏感配置和密钥。
  • 能解释为什么配置应在启动时集中校验。
  • 能区分结果、日志、指标和 trace。
  • 能设计不会泄漏内容的关联字段。
  • 知道凭据已被 Git 跟踪时不能靠 .gitignore 补救。

相关概念与下一步

参考资料

获取日期:2026-07-14。