请求日志、Sentry 与错误监控
MyIP 的后端可观测性由可选的 HTTP 请求日志、限流状态告警和启动前加载的 Sentry SDK 组成。三者分别提供请求级状态、滥用行为记录,以及错误与性能遥测入口,并不共享同一个启用开关。
目的与范围
本页说明后端监控的启动时序、HTTP 日志字段与分级、限流事件的可选磁盘记录,以及 Sentry 初始化和隐私处理挂钩。API 查询业务、数据集更新调度、完整安全策略和部署流程属于相关主题,不在此展开。
**核验边界:**本页直接核对了启动脚本、后端入口前 240 行以及 Sentry 初始化模块。共享 logger 的内部实现、脱敏函数、Sentry tunnel 路由处理器、后端入口末尾的错误中间件、浏览器端初始化及监控测试未在本次有限源码范围内展开。因此,不将注释中的设计描述视为这些组件完整行为的证明,也不承诺所有错误或日志都会成功上报。
概述
理解此子系统时,需要区分四种信号:
| 信号 | 已验证的触发条件 | 主要用途 |
|---|---|---|
| HTTP 请求日志 | LOG_HTTP 严格等于字符串 true,请求经过 /api 中间件 | 观察请求方法、URL、响应状态及日志等级 |
| 限流告警 | 限流器启用,且计数首次达到限额加一 | 记录进入限流状态的客户端,避免每次拒绝都重复告警 |
| 限流本地台账 | 上述告警发生,且配置了台账路径 | 保留 IP、计数及首次记录时间 |
| Sentry 遥测 | 配置后端 DSN,且环境不为 development | 初始化错误、追踪和日志采集能力 |
HTTP 日志默认关闭,目的是减少进程日志体积;限流告警并不依赖 HTTP 日志开关。本地台账也是独立的可选项,不能把“没有台账文件”解释为“没有触发告警”。
依据:backend-server.js、backend-server.js、sentry-instrument.js。
架构与启动顺序
Sources: package.json、sentry-instrument.js、backend-server.js。
为什么使用 node --import
Sentry 初始化文件的注释明确指出:SDK 必须在 Express 等模块加载之前注册 ESM loader hooks,才能实现自动的按路由追踪。项目的 dev、start-backend 和 start 脚本都为后端使用了这一预加载方式。直接运行后端入口不等同于执行这些脚本,不应假设其自动插桩效果相同。
实际启动配置摘录:
"start-backend": "node --import ./sentry-instrument.js backend-server.js",
"start-frontend": "node frontend-server.js",
"start": "concurrently \"node frontend-server.js\" \"node --import ./sentry-instrument.js backend-server.js\"",Source: package.json。
以上为 JSON 属性片段,不是独立配置文件。前端服务启动脚本没有使用此后端预加载文件;不能据此推导浏览器端是否启用了 Sentry。
Sentry 的两层门控
初始化模块先执行 dotenv.config({ quiet: true }),再检查两个条件:
SENTRY_DSN_BACKEND为真值。SENTRY_ENVIRONMENT不严格等于development。
条件不成立时,该文件不会动态导入 @sentry/node,也不会执行 Sentry.init。环境没有配置但 DSN 已配置时,实际上报环境回退到 production。运行 pnpm dev 本身不是这里的禁报条件:脚本仍然预加载该模块,必须结合环境变量判断是否初始化。
依据:sentry-instrument.js、package.json。
请求日志的真实处理链
挂载位置决定覆盖范围
后端先设置 trust proxy 为 1,随后按开关挂载 /api 的 pinoHttp,再挂载可选的限流与延迟中间件。JSON body parser、默认 Cache-Control: no-store 以及全局 referer guard 位于其后。
这一顺序使请求日志中间件先于限流器观察请求,从而在启用时也可以记录 429。它不是全站访问日志:该挂载点只覆盖进入 /api 路径的请求。
依据:backend-server.js、backend-server.js。
等级、消息与字段
| 条件,按判断先后排列 | 日志等级 |
|---|---|
err 为真,或状态码大于等于 500 | error |
| 状态码大于等于 400 | warn |
| 其余响应 | info |
因此,存在错误对象时,等级不只由状态码决定。成功消息包含方法、URL 和状态码;错误消息另外附加 err.message。
真实字段裁剪代码如下:
1 serializers: {
2 req: (req) => ({ method: req.method, url: req.url }),
3 res: (res) => ({ statusCode: res.statusCode }),
4 },Source: backend-server.js。
这里的 req 序列化结果没有请求体或请求头,res 只有状态码。但这不是完整日志对象的字段清单,也不意味着 URL 已脱敏:req.url 被直接保留,并再次用于日志消息。共享 logger 是否进一步过滤字段,需要检查其实现。
请求记录与限流告警并行存在
Source: backend-server.js、backend-server.js。
此图聚焦超限分支;未超限请求继续进入后续中间件与业务路由。关闭 HTTP 日志只移除请求级记录,不会移除限流 handler 中直接调用的 logger.warn。
Sentry 配置、追踪与隐私边界
初始化使用 tracesSampleRate: 1.0,即代码配置为全量追踪采样;enableLogs: true 则开启 Sentry Logs。源码注释说明,共享 logger 的 logMethod hook 会转发 warn/error/fatal,但本页没有核对该 hook 的条件、字段处理、失败兜底或发送可靠性。
隐私处理配置摘录:
1 sendDefaultPii: false,
2 // Upstream query strings carry API keys — redact those params in
3 // breadcrumbs, spans, and request contexts (the rest of the query
4 // stays: it's debugging context).
5 beforeBreadcrumb: scrubBreadcrumb,
6 beforeSendSpan: scrubSpan,
7 beforeSend: scrubEventRequest,
8 beforeSendTransaction: scrubEventRequest,Source: sentry-instrument.js。
这组挂钩分别覆盖 breadcrumb、span、错误事件请求上下文和 transaction 请求上下文。注释给出的设计目标是隐藏上游 URL 中携带 API key 的参数,同时保留其他查询参数用于调试。
需要区分三件事:
sendDefaultPii: false是 SDK 初始化选项,不是整个应用不处理 IP 的保证。- 限流告警显式写入
{ ip },本地台账也保存 IP。 - 本页未读取
scrubBreadcrumb、scrubSpan、scrubEventRequest的实现,因此无法给出具体被删除参数的名单,也不能确认同一规则覆盖本地日志或 Sentry Logs。
监控流量不共用业务限流配额
业务 rateLimiter 对 req.path === '/monitoring' 跳过处理;延迟中间件同样跳过 /monitoring,并额外跳过 /maxmind。在 /api 挂载上下文中,监控路径对应 /api/monitoring。
源码注释解释了这个例外:遥测若与普通 API 共用配额,触发 429 后可能导致浏览器 SDK 一段时间内丢弃事件。注释还说明 tunnel 路由有自己的 limiter,但其实际参数和响应行为不在本页核验范围内,不能将跳过业务限流误读为 tunnel 没有保护。
限流台账:数据、写入与一致性
IP 来源和事件语义
getClientIp(req) 依次选择:cf-connecting-ip、x-forwarded-for 按逗号分割后的首项、cf-connecting-ipv6、req.ip。这一 helper 没有在所读代码中执行格式验证或空白裁剪。由于它优先读取代理头,部署时应确保这些头由可信入口设置,而不是将其自动视为可信身份。
实际告警调用示例:
1 if (req.rateLimit.current === req.rateLimit.limit + 1) {
2 logger.warn({ ip }, 'IP rate-limited');
3 if (blackListIPLogFilePath) {
4 logLimitedIP(ip);
5 }
6 }
7 res.status(429).json({ message: 'Too Many Requests' });Source: backend-server.js。
这里的“首次”由限流计数状态定义,不是永久只记录该 IP 一次。台账计数表示执行这条记录分支的累计次数,不是请求总数,也不是每一次 429 的数量。
数据格式与更新算法
logLimitedIP(ip) 将配置路径与 __dirname 通过 path.join 拼接,然后执行以下步骤:
- 判断父目录是否存在;缺失时同步递归创建,并记录
Created log directory。 - 异步读取整个台账文件。
ENOENT被当作首次创建;其他读取错误记录Error reading the log file并返回。 - 将已有内容按换行分割,再按逗号取出
currentIp、count、timestamp。 - 匹配到 IP 时,执行
parseInt(count, 10) + 1,保留原始时间戳。 - 未匹配到 IP 时,追加计数为 1、时间为当前时间的新记录。
- 使用
fs.writeFile重写整个文件;写入失败记录Failed to write to log file。
每行的三个字段是 IP、计数、首次记录时间。formatDate(timestamp) 使用主机本地时间,显式附带 +0800 形式的 UTC 偏移,避免仅有本地钟面时间带来的歧义。
并发与持久化限制
这一实现不是原子追加,也不是事务性计数器。所读函数内没有锁、排队或原子替换:两个并发调用可能读取相同旧内容,再互相覆盖更新,造成计数或新增条目丢失。因此,台账适合作为辅助记录,不应未经改造就作为严格审计计数来源。
此外,每次更新都读写整个文件并遍历所有行;文件越大,单次更新的 I/O 和内存工作量越大。函数中未见轮转、过期清理或容量上限。它也没有校验历史行的计数字段,损坏的计数可能被解析为 NaN。
这些结论来自该函数的读—改—写顺序,而不是对共享 logger 或其他持久化模块的推断。目录检查和创建为同步调用且未在函数内捕获异常;读写文件的回调错误则只记录日志,没有重试。
配置选项
| 配置项 | 类型与默认值 | 已验证行为 |
|---|---|---|
LOG_HTTP | 环境变量字符串;未设置时关闭 | 仅严格等于 true 时挂载 /api 请求日志;其他大小写或值不会启用 |
SENTRY_DSN_BACKEND | 字符串;无默认 DSN | 非空且环境不是 development 时动态导入并初始化 SDK |
SENTRY_ENVIRONMENT | 字符串;初始化时回退为 production | 严格等于 development 时跳过此模块的 SDK 初始化 |
SECURITY_BLACKLIST_LOG_FILE_PATH | 字符串;空字符串 | 非空时为限流告警额外维护本地台账 |
SECURITY_RATE_LIMIT | 使用 parseInt(..., 10) 解析;默认 0 | 0 时不挂载业务限流器;启用时窗口固定为 20 分钟 |
SECURITY_DELAY_AFTER | 使用 parseInt(..., 10) 解析;默认 0 | 0 时不挂载延迟中间件;监控路径跳过该中间件 |
依据:backend-server.js、backend-server.js、sentry-instrument.js。
以下值在所读实现中是固定代码选项,不是已验证的环境变量接口:
| 代码选项 | 类型 | 设置值 | 含义 |
|---|---|---|---|
tracesSampleRate | number | 1.0 | Sentry 追踪采样设置 |
enableLogs | boolean | true | 启用 Sentry Logs 功能 |
sendDefaultPii | boolean | false | 不启用 SDK 默认 PII 发送 |
trust proxy | number | 1 | Express 代理信任设置,与客户端地址处理有关 |
依据:sentry-instrument.js、backend-server.js。
内部接口参考
这些函数是后端入口中的局部 JavaScript helper,并非对外导出的稳定 API;源码没有 TypeScript 类型声明。
| 实际声明 | 参数与结果 | 错误和边界 |
|---|---|---|
function getClientIp(req) | 读取请求头及 req.ip,返回第一个真值来源 | 不在该 helper 内验证代理头真实性或 IP 格式 |
const formatDate = (timestamp) => { ... } | 将传入值交给 new Date,返回带 UTC 偏移的本地时间字符串 | 未对无效日期输入做显式校验 |
function logLimitedIP(ip) | 接收客户端 IP,调度异步台账读写;没有返回 Promise 或持久化结果 | 非 ENOENT 读取错误和写入错误记录日志;同步目录操作无本地异常捕获 |
customLogLevel(req, res, err)、customSuccessMessage(req, res) 和 customErrorMessage(req, res, err) 是传给 pinoHttp 的回调:分别返回等级字符串、普通消息字符串和错误消息字符串。这里仅记录调用点中可见的接口,不推断依赖包在所有失败场景下的回调契约。
故障排查与运维建议
日志或事件缺失时的检查顺序
- **看不到普通 API 访问日志:**确认
LOG_HTTP是精确的小写字符串true,并确认请求确实经过后端/api。 - **能看到限流告警但没有台账文件:**确认台账路径不为空。该文件是 opt-in,不是默认输出。
- **连续 429 但只有一次
IP rate-limited:**先检查是否符合current === limit + 1的降噪设计,不要直接认定告警丢失。 - **Sentry 没有后端事件:**先检查 DSN 和环境门控,再检查实际启动命令是否保留
--import。SDK 初始化本身不证明网络发送成功。 - **台账创建或更新失败:**检查目录和文件权限,并区分
Error reading the log file与Failed to write to log file。记录函数没有自动重试。 - **浏览器监控请求受阻:**业务限流器和延迟器已显式豁免监控路径;继续检查 tunnel 自身与代理层,而不是仅增加普通 API 配额。
成本与隐私
启用 HTTP 日志会增加每个 API 请求的记录量;进入限流状态的单次告警只能抑制其自身重复,不能抑制已开启的每请求 429 日志。全量追踪采样也应结合真实流量评估遥测开销,不能从 1.0 推导所有 trace 一定到达服务端。
对隐私敏感的部署,应分别审查请求 URL、本地 IP 台账、显式日志属性和 Sentry 各类事件。当前挂钩设计旨在避免 API key 泄露,但未读取脱敏实现前不能承诺完整覆盖。
安全扩展点
- 调整请求字段与分级时,在
pinoHttp的serializers和回调处修改;保留其位于限流器之前的顺序,才能继续观察 429。 - 调整 Sentry 环境策略、采样或事件挂钩时,集中修改预加载模块,并保留早于 Express 加载的启动方式。
- 若台账需要准确并发计数、长期留存或多进程共享,应先替换或串行化现有整文件读—改—写策略;这些能力不是当前函数已有保证。
- 扩展 URL 记录内容或显式错误属性前,应核对脱敏模块与 logger 的实际实现,不能仅依赖
sendDefaultPii: false。
测试与未验证事项
项目的测试脚本使用 Node.js test runner:
"test": "node --import ./tests/setup.js --test tests/*.test.js",
"check": "pnpm run test && pnpm run build",Source: package.json。
本次未读取监控相关测试,也没有执行测试,因此不声明这些行为已有自动化覆盖。建议核验的回归场景包括:日志开关精确匹配、4xx/5xx 分级、首次超限告警、Sentry 开发环境禁报、带密钥 URL 的脱敏,以及并发台账更新。这些是基于实现风险提出的验证建议,不是现有测试清单。
同样,本页不提供未经核验的全局异常处理状态码、Sentry tunnel 限额、SDK 发送重试规则、日志 flush 机制或前端 source map 上传步骤。
相关链接
- 请求日志与中间件挂载顺序:扩展请求级观测的入口。
- 限流告警与本地台账实现:理解告警降噪、持久化及监控路径豁免。
- Sentry 预加载与事件处理挂钩:核对环境门控和初始化顺序。
- 启动、测试与构建脚本:与部署和验证流程衔接。