接入 EveryInfra 的返工,通常不是业务逻辑写错,而是三件事在动手前没定:密钥放哪、调用地址怎么按环境切、错误码在哪一层收口。这三件事留到写业务代码时再想,结果往往是每个调用点各写一套,换一次密钥或切一次环境就要改很多文件,出错后也不知道该动哪一层。比较省事的做法是先分别定成:一个只读配置的密钥入口、一份按环境分组的地址配置、一层统一的错误包装,最后用一次最小调用把这条链路验证通,再开始写业务。
先把密钥、调用地址、错误码拆成三层来定:密钥只从环境变量或本地配置文件读取,换密钥不改代码、不发版;调用地址按环境注入,并在启动日志里能直接看到当前指向;错误码在客户端包装层统一转成自定义异常,业务代码只处理成功分支。适用范围是需要多环境运行的接入。边界是:具体字段名、错误码含义和接口路径要以实际返回和接口说明为准,下面是可替换的骨架,不是接口定义。
确定密钥的存放位置与替换方式
判断标准很简单:换一次密钥需要改几个文件、要不要重新发版。如果答案是“要改代码再发一次”,就说明密钥位置选错了。建议密钥不进 Git、不进镜像、不写死在代码里,通常从环境变量读取,由容器编排或 CI 在部署时注入;本地开发可以用一个 .env 文件,但这个文件要进 .gitignore。
import os
def load_infra_config():
key = os.environ.get("EVERYINFRA_API_KEY", "")
base = os.environ.get("EVERYINFRA_BASE_URL", "").rstrip("/")
missing = [n for n, v in (("EVERYINFRA_API_KEY", key),
("EVERYINFRA_BASE_URL", base)) if not v]
if missing:
# 只打印变量名,不打印值
raise RuntimeError("缺少必需配置: " + ", ".join(missing))
return {"api_key": key, "base_url": base}
密钥缺失时的启动行为建议是 fail fast:进程启动阶段就调用一次 load_infra_config(),缺变量直接报错退出,并打印缺的是哪个变量名。不要给空字符串默认值,也不要等到第一次业务请求才失败——那样错误会出现在离配置很远的地方,排查耗时。日志里只出现变量名和掩码后的前缀(例如前 4 位),完整密钥不进日志、不进错误堆栈。
确定调用地址按环境切换的方式
测试环境的请求打到线上配置,多数是因为地址写死在代码或默认值里,而不是环境切换本身复杂。建议地址只以配置形式出现,按环境分组,运行时由当前环境名选中一组。下面是一个可替换的配置结构示例,实际字段名按项目习惯调整。
# everyinfra.yaml
default:
timeout_ms: 5000
verify_path: /v1/ping # 按实际接口替换
environments:
dev:
base_url: https://infra-dev.example.invalid
staging:
base_url: https://infra-staging.example.invalid
prod:
base_url: https://infra.example.invalid
切换后要能一眼确认当前指向。常用做法有两个:一是启动日志固定打印一行 env=<环境名> host=<域名>,只打域名,不打完整路径和查询串;二是提供一个只读诊断入口或一条命令(例如 print-config)输出当前生效的配置。验证方式就是在目标环境重启一次,看这行日志是否与预期环境一致,而不是凭记忆判断。生产地址建议只出现在生产配置里,其他环境的配置文件里根本不写,这样误配也没法选中它。
确定错误码在哪一层统一处理
判断标准可以落在一个动作上:同一个错误码在业务代码里被 if 判断超过一次,就该往上层收;被判断三次以上,几乎一定该收。建议把统一处理放在 HTTP 客户端或 SDK 的包装层,业务层只 catch 一种自定义异常。包装函数接收的字段通常包括:HTTP 状态码、响应体里的业务 code、message、request_id(或 trace_id)、原始响应体、以及根据状态码和 code 判断出的 retryable 布尔值。
class InfraError(Exception):
def __init__(self, code, message, http_status, request_id, retryable, raw):
super().__init__(message)
self.code = code # 统一后的错误码,未知一律归为 UNKNOWN
self.message = message
self.http_status = http_status
self.request_id = request_id # 排障时最有用,务必透出
self.retryable = retryable # 是否值得重试,由包装层判断
self.raw = raw # 原始响应,保留但不默认全量打日志
def wrap_response(resp):
if 200 <= resp.status_code < 300:
return resp.json()
body = resp.text
code = (resp.json().get("code") if resp.headers.get("content-type", "").startswith("application/json") else None) or "HTTP_%d" % resp.status_code
raise InfraError(code=code, message=body[:200], http_status=resp.status_code,
request_id=resp.headers.get("x-request-id", ""),
retryable=resp.status_code in (429, 502, 503, 504), raw=body)
向外抛出的结构固定为 InfraError,业务代码就只需要知道:retryable 为真时按自己的退避策略重试,为假时按 code 决定提示或降级,不需要再理解 HTTP 细节。边界要写清楚:包装层不要吞掉未知错误,未知一律转成 code=UNKNOWN 并保留 raw,否则问题会被藏起来;请求超时和连接失败这类传输层异常也走同一个异常类型,只是 http_status 为空。
写一个只调一次的最小验证脚本
在写业务逻辑前,用这个脚本确认配置、网络和认证这三段是通的。它只发一次请求、不做重试、不做并发,避免把重试和网络问题混在一起看。
# verify_everyinfra.py
import os, sys, time, urllib.request, urllib.error
def main():
base = os.environ.get("EVERYINFRA_BASE_URL", "").rstrip("/")
key = os.environ.get("EVERYINFRA_API_KEY", "")
path = os.environ.get("EVERYINFRA_VERIFY_PATH", "/v1/ping") # 按实际接口替换
env = os.environ.get("APP_ENV", "unknown")
if not base or not key:
print("FAIL stage=config env=%s missing=EVERYINFRA_BASE_URL/EVERYINFRA_API_KEY" % env)
sys.exit(2)
req = urllib.request.Request(base + path, headers={"Authorization": "Bearer " + key})
start = time.time()
try:
with urllib.request.urlopen(req, timeout=10) as resp:
body = resp.read().decode("utf-8", "replace")
print("OK env=%s host=%s status=%s elapsed_ms=%d" % (env, base, resp.status, (time.time() - start) * 1000))
print("request_id=%s" % resp.headers.get("x-request-id", "-"))
print("body_head=%s" % body[:200])
except urllib.error.HTTPError as e:
print("FAIL stage=http env=%s status=%s request_id=%s body_head=%s" % (env, e.code, e.headers.get("x-request-id", "-"), e.read(200)))
sys.exit(1)
except Exception as e:
print("FAIL stage=network env=%s type=%s msg=%s" % (env, type(e).__name__, e))
sys.exit(1)
if __name__ == "__main__":
main()
输入参数是环境变量注入的 base_url、api_key、目标路径,以及用于标识的 APP_ENV,超时先固定 10 秒即可。成功时的输出包含:当前环境名、解析出的 host、HTTP 状态码、耗时、request_id、响应体前 200 字符——这些足够判断自己连的是哪套环境。失败时的输出包含:失败阶段(config / network / http)、环境名、状态码或异常类型、request_id、响应体摘要,据此能直接区分是变量没注入、域名不通,还是鉴权或参数被拒。运行方式是先导出变量,再执行 python verify_everyinfra.py,退出码非 0 即视为未通过,不要急着往下写业务。
把三项决定写进项目配置说明文件
这三项决定如果只存在于某次对话里,下一个人接手时会重新问一遍。建议在仓库里留一份配置说明文件(例如 docs/everyinfra.md),列出下面的字段,每个字段都要标明取值来源,而不是只写名字。
- 变量名与用途:例如 EVERYINFRA_API_KEY(鉴权)、EVERYINFRA_BASE_URL(调用地址)、EVERYINFRA_VERIFY_PATH(验证路径)、APP_ENV(当前环境标识)。
- 是否必填与默认值:标明缺失时的行为是启动失败还是走默认,默认值不能是生产地址。
- 取值来源:写清由谁提供、在哪里生成、在哪里注入。密钥来源写“由密钥管理侧下发并在部署时注入环境变量”,地址来源写“环境配置文件 environments.
.base_url”。 - 适用环境:哪些值只在 dev、staging、prod 生效,生产地址是否只出现在生产配置中。
- 变更与轮换方式:换密钥走哪个流程、是否需要重启、是否要同步更新 CI 变量。
- 错误码约定:列出已确认的错误码及其含义、哪些标记为可重试,并注明未收录的一律按 UNKNOWN 处理。
- 验证方式与责任人:验证脚本的路径和运行命令,以及配置变更时找谁确认。
字段的取值来源应当可以被核对:变量名能在部署配置里搜到,地址能在环境配置文件里找到,错误码能在接口返回中看到。写不进取值来源的项,通常说明这个决定还没真正定下来,建议先补上再继续写业务代码。