第12章:中间件、CORS、日志与可观测性
约 4 分钟 · 更新于 2026-09-02
横切关注点章:请求前后的通用逻辑(middleware)、浏览器跨域(CORS)、结构化日志(log)与开箱即用的分布式追踪。它们共同的特点是——在传统栈里各需要一个库 + 一堆配置,在 Encore 里是框架的一部分。
一、中间件 (Middleware)
定义与挂载
中间件挂在服务声明上,按数组顺序执行:
typescript
import { Service } from "encore.dev/service";
import { middleware } from "encore.dev/api";
const timing = middleware({ target: { all: true } }, async (req, next) => {
const start = Date.now();
const resp = await next(req); // 调用后续链路
resp.header.set("X-Response-Time", `${Date.now() - start}ms`);
return resp;
});
export default new Service("order", {
middlewares: [timing], // 多个时按定义顺序执行
});
洋葱模型:next(req) 之前是请求前逻辑,之后是响应后逻辑;不调 next 即短路(直接抛 APIError 拦截请求)。
定向 (target):编译期过滤
typescript
middleware({ target: { all: true } }, handler); // 全部端点
middleware({ target: { auth: true } }, handler); // 仅需鉴权的端点
middleware({ target: { expose: true } }, handler); // 仅公开端点
middleware({ target: { isRaw: true } }, handler); // 仅 raw 端点
middleware({ target: { isStream: true } }, handler); // 仅流式端点
middleware({ target: { tags: ["admin"] } }, handler); // 按端点 tags 匹配
用 target,别在运行时 if
target 在编译期由静态分析落到具体端点上,不匹配的端点完全不经过这个中间件;handler 里自己 if (req.requestMeta.path.startsWith(...)) 则每个请求都要跑判断。tags 与 target 配合是"管理端点统一加审计"这类需求的标准解法。
请求对象与自定义数据
typescript
const audit = middleware({ target: { tags: ["admin"] } }, async (req, next) => {
// 类型化/流式端点:req.requestMeta —— { method, path, pathParams, headers }
// raw 端点:req.rawRequest / req.rawResponse
// 流式端点:req.stream
req.data = { startTime: Date.now() }; // 传递给后续中间件/处理链的自定义数据
const resp = await next(req);
await writeAuditLog(req.requestMeta?.path, Date.now() - req.data.startTime);
return resp;
});
中间件的适用清单:审计日志、响应头注入、限流、权限组校验(403 裁决,接第 10 章)、请求级缓存。不适用:请求体校验(框架已做)、鉴权(authHandler 已做)——别用中间件重造框架已有的轮子。
二、CORS:浏览器跨域
配置写在 encore.app 的 global_cors 键:
json
{
"id": "my-app",
"global_cors": {
"debug": false,
"allow_origins_without_credentials": ["*"],
"allow_origins_with_credentials": [
"http://localhost:5173",
"https://booking.example.com",
"https://*.example.com"
],
"allow_headers": ["X-Client-Version"],
"expose_headers": ["X-Request-Id"]
}
}
| 键 | 含义 |
|---|
| allow_origins_without_credentials | 非凭据请求允许的来源,默认 ["*"] |
| allow_origins_with_credentials | 带凭据(Cookie / Authorization 头)请求允许的来源,支持通配 https://*.example.com |
| allow_headers / expose_headers | 额外允许的请求头 / 额外暴露的响应头("*" 全开) |
| debug | 开 CORS 调试日志 |
默认行为三句话:本地开发全放行;线上非凭据请求全来源可用;凭据请求默认全拒——前端登录态调不通接口,第一个查的就是 allow_origins_with_credentials。
普通端点用到的头由静态分析自动加入 CORS 允许集;只有 raw 端点里手写的自定义头才需要 allow_headers / expose_headers 补充。
三、结构化日志
typescript
import log from "encore.dev/log";
log.info("order created", { orderNo: "ORD123", amountCents: 158800 });
log.error(err, "payment gateway call failed");
// 五个级别
log.trace(...); log.debug(...); log.info(...); log.warn(...); log.error(...);
// with:给一组日志绑定公共字段
const logger = log.with({ orderNo: "ORD123" });
logger.info("start refund"); // 自动带 orderNo
logger.warn("partial refund only"); // 同上
两条纪律:
- 用 log 不用 console.log——结构化字段可检索,且自动关联当前 trace(在仪表盘里从日志一键跳到那次请求的完整调用链);
- 字段进第二个参数,不要拼进消息字符串——log.info("order created", {orderNo}) 可按 orderNo 过滤,log.info(\order ${orderNo} created`)` 不可。
四、分布式追踪:零埋点的由来
Encore 的追踪不需要接入任何 SDK,因为所有 I/O 都从框架原语走:
| 自动成为 span 的操作 | 来源 |
|---|
| API 请求/响应(含载荷) | api() |
| 服务间调用 | ~encore/clients |
| SQL 查询(含语句与参数) | db.query/exec |
| Pub/Sub 发布与消费 | Topic/Subscription |
| 缓存操作 | keyspace 方法 |
| 对象存储操作 | Bucket 方法 |
| 出站 HTTP | 内置 fetch 插桩 |
这就是第 1 章"绕过原语的代价"的具象化:pg.connect 直连的查询不在这张表里,trace 上就是一段空白。
载荷默认记录,两个控制开关:端点 sensitive: true 整体脱敏(第 3 章);生产环境的采样与保留策略在 Encore Cloud / 自托管配置侧控制。
本地看 trace:仪表盘 Tracing 面板。云上(Encore Cloud):Cloud Dashboard 同款界面。自托管:日志与 trace 可导出对接自有观测栈(OpenTelemetry 兼容导出)。
五、日志 CLI
bash
encore logs --env=prod # 流式拉取线上日志
encore logs --env=prod --json # JSON 格式(喂给 jq / 采集器)
六、常见误区
- 中间件里重造校验/鉴权——框架已有,写了也是重复执行。
- 不用 target 全靠运行时 if——所有请求白付判断成本。
- 生产联调时 allow_origins_with_credentials 忘配——症状是"Postman 通、浏览器不通"。
- console.log 打关键业务日志——丢结构化、丢 trace 关联。
- 把变量拼进日志消息字符串——不可检索;字段一律走第二参数。
- 敏感接口没开 sensitive、又在日志里打全量请求体——两处都要管。
- 以为要自己接 OpenTelemetry 才有 trace——Encore 的 trace 是原语自动生成的,先用起来再考虑导出。
七、实践练习
- 写一个计时中间件(响应头 X-Response-Time)挂到订单服务,用 curl -i 验证。
- 给退款端点打 tags: ["admin"],写按 tag 定向的审计中间件,把操作人(getAuthData())与路径写进日志。
- 配置 CORS:允许 http://localhost:5173 凭据请求,用浏览器 fetch(credentials: "include")验证;再删掉配置看报错,记住这个报错长相。
- 把某服务里所有 console.log 改成 log.info + 结构化字段,在仪表盘用字段过滤日志并跳转到关联 trace。
- 制造一次慢请求(SQL 里 pg_sleep(2)),在 Tracing 面板定位耗时 span,截图留档——这是第 20 章 AI 工作流"用 trace 验证"的手动版。
八、总结
- 中间件挂服务声明,洋葱模型,target 编译期定向优于运行时判断;tags 支撑按业务组定向。
- CORS 在 encore.app 的 global_cors 配置;凭据请求默认拒绝所有跨域来源,是联调 401/CORS 报错的第一嫌疑。
- 日志用 encore.dev/log:五级别、结构化字段、with 绑定公共字段、自动关联 trace。
- 追踪零埋点的前提是 I/O 全走原语;绕过原语 = 观测盲区。
- encore logs --env 拉线上日志;trace 本地云上同款界面。
请继续阅读:第13章:实战一 URL 短链服务。
原始资料引用