删了一个没用的 console.log,生产环境直接崩了

事故发生时,先暂停继续发布,保存浏览器控制台、Node.js stderr、监控截图、当前容器状态和已经生成的静态产物,再重新部署上一份已知可用的镜像 digest 或静态文件。不要把“补回一行 console.log”直接当成根因修复,旧产物应连同当时的配置和环境变量一起恢复,避免重新构建出另一个版本。

删除日志与线上崩溃同时发生,只能先建立时间相关性,不能直接证明因果关系。常见情况是同一次发布还改动了 Babel、Terser、依赖锁文件、环境变量或部署流程;另一种情况是原日志语句并非真正独立的调试语句,删除后改变了语法、控制流或异步执行顺序。排查时应先分清构建失败、浏览器运行异常、静态资源错配、Node.js 进程退出和内存不足,再沿着对应路径定位。掘金热榜里的同名文章可以作为问题入口,但不能从标题反推具体根因。

先分清是构建失败、浏览器报错还是 Node 进程退出

先补齐发布时间线:最后一个正常版本、第一个异常版本、实际部署的 commit、镜像 digest、静态产物哈希,以及故障是否只影响部分页面、接口或用户。Git 历史通常能先缩小范围:

git log --oneline --decorate -n 20
git show --stat --oneline HEAD
git diff HEAD^ HEAD -- .

如果异常提交不是 HEAD,就把上述命令中的 HEAD 替换为实际提交号。只有故障能够稳定复现时,才适合用 git bisect 配合生产构建和关键路径测试;线上偶发问题不要为了二分而反复替换生产版本。

构建阶段失败和浏览器运行时异常是两回事。资料中的 Vue CLI 3.x 历史案例显示,配置为删除 console 后出现的是下面这种构建错误:

ERROR Failed to compile with 5 errors 11:19:57 AM
static/js/app.2cd76486.js from UglifyJs
Unexpected token: punc «(» [static/js/app.2cd76486.js:1,23125]
static/js/chunk-66db1624.14c7d3b2.js from UglifyJs
Unexpected token: punc «(» [static/js/chunk-66db1624.14c7d3b2.js:1,733956]
static/js/exception_403.5d780122.js from UglifyJs
Unexpected token: punc «(» [static/js/exception_403.5d780122.js:1,281]
static/js/exception_404.3457fc52.js from UglifyJs
Unexpected token: punc «(» [static/js/exception_404.3457fc52.js:1,281]
static/js/exception_500.94c7c527.js from UglifyJs
Unexpected token: punc «(» [static/js/exception_500.94c7c527.js:1,283]
ERROR Build failed with errors.
error Command failed with exit code 1.

这类输出首先说明新产物没有通过构建,应回到 Node.js 版本、锁文件、压缩器和解析器配置上排查。除非发布流程绕过了构建成功检查,否则没有必要先到线上寻找浏览器异常。Unexpected token 指向的是解析失败的位置,不等于已经找到了删除 console 的具体语句;应使用与 CI 相同的工具链复现,再逐项还原配置。

如果构建成功且页面已经发布,则要分开看四类信号:未捕获的 JavaScript 异常、接口 5xx、动态 chunk 404,以及 HTML 与静态资源版本不一致。浏览器里应先找第一条未捕获异常及其调用栈,后面的白屏、组件不渲染或二次异常可能只是连锁结果。若首条错误是资源加载失败,尤其要检查失败 URL,而不是继续在业务代码里找空指针。

Node.js 服务则要检查退出原因、stderr、容器重启记录和内存曲线。PM2 管理的服务可以先看状态、日志、监控和进程详情:

pm2 list
pm2 logs my-app
pm2 monit
pm2 show my-app

PM2 会自动重启意外退出的进程,并记录标准输出与错误日志,因此“进程显示 online”并不等于服务没有异常重启,还要结合日志和重启次数判断。Docker 环境则保留容器状态与近期日志:

docker inspect my-container
docker logs --since 30m my-container

重点查看 State、ExitCode、OOMKilled、RestartCount 等字段。OOM 结论尤其需要证据闭环:高频循环或事件监听中的大量日志可能增加 CPU、内存与 GC 压力,也有资料指出频繁日志可能触发 GC 甚至 OOM[1];但删除日志后发生崩溃,仍需用崩溃前的内存曲线、容器终止原因和实际日志量验证,不能只凭提交时间判断。反过来,如果浏览器标签页内存上涨,也不能据此认定服务器进程 OOM。

对比坏版本与上一份可用产物,找出被改坏的规则

排查时不能只看 Git diff。源码、构建配置和最终产物需要三方对照:源码确认改了什么,配置确认哪些规则会被执行,产物确认规则最终作用到了哪些文件。依赖锁文件、构建环境变量、压缩选项、Dockerfile、静态资源清单和部署配置都应纳入比较,因为重新打包本身就可能改变 chunk 文件名和内容哈希。

先找出删除的日志是否真的是一条独立语句。下面这种没有花括号的分支,删掉 console.log 后可能直接留下无法解析的 if/else;即使删除工具保留了空分支,业务路径也可能改变:

if (condition)
  console.log(value)
else
  doSomething()

下面这种写法则要留意分号和自动分号插入。日志行连同末尾分号一起被删除后,getValue() 与下一行以 [ 开头的表达式可能被解析为同一条连续表达式:

const value = getValue()
console.log(value);
[1, 2].forEach(run)

如果日志调用本身带有 await,删除它还会少掉一个异步等待点,可能改变 Promise、事件回调或异常处理顺序。遇到这些代码时,不能因为“它只是日志”就认定删除没有副作用。

接下来搜索所有删除规则和日志入口,别只盯着 console.log:

rg -n --glob '!node_modules/**' --glob '!dist/**' 'transform-remove-console|drop_console|drop_debugger|pure_funcs' .
rg -n --glob '!node_modules/**' --glob '!dist/**' --glob '!coverage/**' 'console\.(log|info|warn|error)|console\[' .

Babel 场景要确认 transform-remove-console 是否启用,以及 exclude 是否保留了生产环境真正需要的错误日志。资料中的 Vue/Babel 示例会在生产构建中保留 info、error 和 warn。下面只是应合并进现有 Babel 配置的片段,不能覆盖项目原有 presets:

...process.env.NODE_ENV === 'production'
  ? [['transform-remove-console', { exclude: ['info', 'error', 'warn'] }]]
  : []

Terser 场景要区分 drop_console、drop_debugger 和 pure_funcs。drop_console: true 的删除范围是整组 console 方法,并不只针对 console.log;如果目标仅仅是普通调试日志,可以考虑更窄的规则,下面是语义示意,具体层级仍需按当前 Webpack 或 Vue CLI 适配器确认:

{
  compress: {
    pure_funcs: ['console.log']
  }
}

Vue CLI 3.x 的历史案例最终是通过 optimization.minimizer 配置 Terser,因此排查时还要确认项目当前真正使用的压缩器,而不是照着旧版配置猜。

Vite、Vue 3 和 Nuxt 3 走的是另一条配置路径。Nuxt 3 的资料给出了通过 Vite/esbuild 和环境变量控制删除日志的做法,并强调要在本地检查构建产物。需要注意的是,esbuild 的 drop: ['console'] 会移除整类 console 调用;若只想处理 console.log,应确认当前 Vite、Nuxt 版本支持的方式,使用精确的 pure 标记或专用插件,并在产物中验证结果。不要假设 Babel 插件会自动作用于 Vite 项目,也不要用开发服务器的 NODE_ENV 推断生产构建环境。

静态规则还可能漏掉别名、包装函数和动态属性访问,例如:

const output = console.log
output(value)

或者:

console[method](value)

因此源码搜索只能用来发现线索,最终产物才是准绳。前端项目可以这样检查实际输出目录:

rg -l --glob '!**/*.map' 'console\.log' dist
rg -l --glob '!**/*.map' 'console\.(warn|error)' dist
rg -l --glob '!**/*.map' 'debugger' dist

搜索结果为空只说明没有匹配到这种静态文本,不能证明动态调用不存在;source map 也可能保留原始源码,需要与实际运行代码分开看。反过来,构建产物里仍然存在 console.log,则说明删除规则没有生效,但它本身未必就是崩溃原因。

静态资源错配要单独核对。删除日志会改变输入代码,重新打包后 chunk 哈希也可能变化。如果缓存中的旧 HTML 仍引用旧 chunk,而发布过程已经清理了旧文件,浏览器就会拿到 404。比较本地构建和线上实际返回的 HTML:

rg -n '<script|<link' dist/index.html
curl -fsS "$BASE_URL/" -o /tmp/prod-index.html
rg -n '<script|<link' /tmp/prod-index.html

执行下面两条命令前,应先把 BASE_URL 设置为生产站点根地址。再从 HTML 中取出入口脚本和动态 chunk 的实际 URL,与本次部署目录、上一份可用产物目录以及服务器缓存逐项核对。若页面引用的是旧文件而服务器只保留新文件,问题就不在业务异常,而在发布与缓存的原子性。

删除日志有性能和安全上的合理动机,但不能采用“生产环境禁止所有 console”的粗暴策略。过多日志会占用资源、淹没真正的错误信息,敏感参数、Token 和用户数据也可能因此进入生产日志。更合适的方向是删除无业务价值的普通调试日志,保留 console.error 和 console.warn,并通过项目专用日志器记录必要事件,在日志入口增加级别、脱敏和采样控制。

先回滚止血,再修正规则并复验

回滚负责恢复服务,修正后的构建负责证明问题确实被解决。止血时优先重新部署上一份已知可用的镜像 digest 或静态产物,并配套恢复当时的配置,不要临时用新依赖重新构建旧源码。必须从源码回退时,在共享分支上用新提交撤销变更,不直接执行 reset --hard:

git status --short
git revert HEAD

这只适用于工作区干净、问题提交确实是当前 HEAD 的情况。多个提交共同引入问题时,应按发布范围逐个撤销,或者直接恢复旧产物。回退源码后还要核对锁文件、构建环境变量、资源清单和部署配置,否则仍可能构建出与旧版本不同的代码。

修正删除策略时,范围应收敛到无业务价值的 console.log,明确保留错误和警告方法;Babel 项目使用 exclude,Terser 项目避免直接用整类删除冒充精准删除。源码中存在别名或自定义日志封装时,最好让调用点可识别,而不是依赖一条覆盖所有 console.* 的规则。Node.js 服务可以接入项目已有的日志库。log4js-node v3 的资料提到,新版改进了异步写入和异常捕获,可降低日志写入异常直接拖垮进程的风险,但升级时仍要按项目锁文件和对应版本文档验证。

随后用项目原有的生产构建命令、依赖锁文件和构建参数完成一次干净构建。不要为了排障临时更换包管理器、更新依赖或套用另一套环境变量,那会让变量继续增加。构建完成后先检查产物中的 console、debugger、入口脚本和动态 chunk,再把产物部署到灰度实例。

复验至少覆盖这些路径:

  • 被删除日志所在的 if/else、异常分支和 await 流程;
  • 登录、核心页面、关键接口等实际业务路径;
  • 动态 chunk 是否全部加载成功,HTML 与资源版本是否一致;
  • 高频调用路径的 CPU、内存和 GC 情况;
  • Node.js 服务的 stderr、退出次数与重启次数;
  • 故障发生时的相同输入,包括请求参数、功能开关和必要上下文。

验证应针对生产构建产物进行,不能只在 dev 模式下点一遍页面。修正版本先灰度发布,观察首条未捕获异常、错误率、5xx、资源 404、CPU、内存、GC 和进程重启数;上一份可用产物继续保留到观察窗口结束。验收标准也不应只有“页面能打开”,而应包括关键路径通过、没有新增首条未捕获异常、静态资源无错配、资源曲线稳定,以及 Node.js 进程不再异常重启。

如果回滚后服务恢复,只能先确认“本次发布与故障高度相关”。只有当修正版本在相同输入下再次复现,并且排查能缩小到某条删除语句或某个构建规则时,才有更强的证据建立因果关系。对于无法复现的偶发问题,应保留坏版本和好版本的产物、source map、锁文件、构建参数与环境变量清单、部署 ID、镜像 digest、浏览器控制台、Node.js stderr、容器状态以及监控数据,并继续寻找首条异常或资源失败记录;在证据闭环前,不应把结论写成“删掉 console.log 就是根因”。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容