Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/Encore/第12章

第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.appglobal_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");     // 同上

两条纪律:

  1. log 不用 console.log——结构化字段可检索,且自动关联当前 trace(在仪表盘里从日志一键跳到那次请求的完整调用链);
  2. 字段进第二个参数,不要拼进消息字符串——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 / 采集器)

六、常见误区

  1. 中间件里重造校验/鉴权——框架已有,写了也是重复执行。
  2. 不用 target 全靠运行时 if——所有请求白付判断成本。
  3. 生产联调时 allow_origins_with_credentials 忘配——症状是"Postman 通、浏览器不通"。
  4. console.log 打关键业务日志——丢结构化、丢 trace 关联。
  5. 把变量拼进日志消息字符串——不可检索;字段一律走第二参数。
  6. 敏感接口没开 sensitive、又在日志里打全量请求体——两处都要管。
  7. 以为要自己接 OpenTelemetry 才有 trace——Encore 的 trace 是原语自动生成的,先用起来再考虑导出。

七、实践练习

  1. 写一个计时中间件(响应头 X-Response-Time)挂到订单服务,用 curl -i 验证。
  2. 给退款端点打 tags: ["admin"],写按 tag 定向的审计中间件,把操作人(getAuthData())与路径写进日志。
  3. 配置 CORS:允许 http://localhost:5173 凭据请求,用浏览器 fetch(credentials: "include")验证;再删掉配置看报错,记住这个报错长相。
  4. 把某服务里所有 console.log 改成 log.info + 结构化字段,在仪表盘用字段过滤日志并跳转到关联 trace。
  5. 制造一次慢请求(SQL 里 pg_sleep(2)),在 Tracing 面板定位耗时 span,截图留档——这是第 20 章 AI 工作流"用 trace 验证"的手动版。

八、总结

  1. 中间件挂服务声明,洋葱模型,target 编译期定向优于运行时判断;tags 支撑按业务组定向。
  2. CORS 在 encore.appglobal_cors 配置;凭据请求默认拒绝所有跨域来源,是联调 401/CORS 报错的第一嫌疑。
  3. 日志用 encore.dev/log:五级别、结构化字段、with 绑定公共字段、自动关联 trace。
  4. 追踪零埋点的前提是 I/O 全走原语;绕过原语 = 观测盲区。
  5. encore logs --env 拉线上日志;trace 本地云上同款界面。

请继续阅读:第13章:实战一 URL 短链服务


原始资料引用



本章目录
一、中间件 (Middleware)二、CORS:浏览器跨域三、结构化日志四、分布式追踪:零埋点的由来五、日志 CLI六、常见误区七、实践练习八、总结原始资料引用Related Documents
苏ICP备2025204887号-2