日志记录在前端项目中的实战应用与性能优化策略

Code°义霞 安全 阅读 795
赞 22 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

日志记录这事,我干了快八年,前三年是 console.log(1)、console.log(2)、console.log(‘这里好像卡了’),后五年开始被运维拉着骂:你打的日志根本没法查!线上报错找不到上下文,用户反馈“点不动”,我们翻了二十分钟日志才定位到是某个 Promise 被 silent reject 了,但 log 里连个 traceId 都没有。

日志记录在前端项目中的实战应用与性能优化策略

现在我团队里所有前端项目(React/Vue/纯 JS)都用这套轻量级日志方案,不依赖任何第三方 SDK(比如 Sentry 的 logger 模块太重,且默认行为容易误打敏感字段),核心就一个 Logger 类,50 行搞定,稳定跑了两年多,没出过漏打、乱序、污染 console 的问题。

先上核心代码:

class Logger {
  constructor(prefix = '', options = {}) {
    this.prefix = prefix;
    this.level = options.level || 'info'; // 'debug' | 'info' | 'warn' | 'error'
    this.enabled = options.enabled !== false;
    this.silentInProd = options.silentInProd !== false;
  }

  _shouldLog(level) {
    const levels = ['debug', 'info', 'warn', 'error'];
    const currentLevelIndex = levels.indexOf(this.level);
    const levelIndex = levels.indexOf(level);
    return this.enabled && 
           levelIndex >= currentLevelIndex &&
           !(this.silentInProd && process.env.NODE_ENV === 'production' && level === 'debug');
  }

  debug(...args) {
    if (this._shouldLog('debug')) {
      console.debug([${this.prefix}], ...args);
    }
  }

  info(...args) {
    if (this._shouldLog('info')) {
      console.info([${this.prefix}], ...args);
    }
  }

  warn(...args) {
    if (this._shouldLog('warn')) {
      console.warn([${this.prefix}], ...args);
    }
  }

  error(...args) {
    if (this._shouldLog('error')) {
      const err = args.find(arg => arg instanceof Error);
      if (err && err.stack) {
        // ✅ 关键:保留原始 stack,避免被 console.error 吞掉
        console.error([${this.prefix}], ...args.filter(a => a !== err), err);
      } else {
        console.error([${this.prefix}], ...args);
      }
    }
  }
}

// 全局实例(按模块创建)
const apiLogger = new Logger('API', { level: 'info' });
const authLogger = new Logger('AUTH', { level: 'debug' });

// 使用示例
apiLogger.info('fetch start', { url: '/user/profile' });
apiLogger.error('fetch failed', { url: '/user/profile', status: 503, traceId: 'tr-abc123' });

为什么这么写?三点最实在的体会:

  • prefix 是命门:不加前缀的 log 就是垃圾。上线后 grep 日志全靠它,grep "API" app.loggrep "AUTH" app.log 直接分层,比看时间戳靠谱十倍;
  • error 方法必须透传 Error 实例:我踩过三次坑——第一次直接 console.error(msg, err.toString()),stack 全丢了;第二次 console.error(msg, { ...err }),非 enumerable 属性(比如 stack、lineNumber)全没了;最后这个写法,让浏览器 DevTools 点开就能跳转源码,救命;
  • production 下自动降级 debug 日志:不是为了性能(console.debug 几乎没开销),而是防 QA 或用户手贱打开控制台,看到一堆 debug 日志以为系统崩了——真有用户截图发微博说“你们网站在疯狂报错”,结果全是 debug("user token refreshed")……

这几种错误写法,别再踩坑了

下面这些,都是我亲手写过、被拉去喝茶、改了三版才收敛的反面案例:

❌ 错误 1:把敏感字段直接塞进 log

比如登录成功后:

console.log('login success', { token: res.data.token, user: res.data.user });

token 是 JWT?base64 编码后就是明文,日志一落盘,全公司都能 grep 出来。更糟的是,有些日志系统会自动上报到中心平台(比如 ELK),等于把密钥贴在公告栏上。

✅ 我的处理方式:所有可能含敏感信息的对象,统一走脱敏函数:

function sanitize(obj) {
  if (!obj || typeof obj !== 'object') return obj;
  const result = { ...obj };
  if (result.token) result.token = '[REDACTED]';
  if (result.password) result.password = '[REDACTED]';
  if (result.idCard) result.idCard = result.idCard.replace(/(d{4})d{10}(d{4})/, '$1******$2');
  return result;
}

apiLogger.info('login success', sanitize({ token: res.data.token, user: res.data.user }));

❌ 错误 2:log 里拼字符串,而不是传参

比如:

console.log('[API] fetch /user/' + userId + ' failed: ' + err.message);

问题有二:一是字符串拼接丢失类型信息(userId 是 number 还是 string?err.message 为空时会不会报错?),二是无法被结构化日志系统(如 Datadog)提取字段。DevOps 后期想按 userId 统计失败率,发现全是字符串,正则都写吐了。

✅ 我现在只传参数,不拼串console.info('[API]', { url: /user/${userId}, error: err.message }),结构清晰,字段可索引。

❌ 错误 3:在 try/catch 外层统一兜底 log

写个全局 error handler,然后所有错误都打一条 catch all error,看似省事,实际等于没 log。因为丢失了调用栈、丢失了业务上下文、丢失了那个关键的 traceId

✅ 我的做法是:每个异步链路自己负责自己的 error log,比如:

async function loadUserProfile(id) {
  const traceId = generateTraceId();
  apiLogger.info('loadUserProfile start', { id, traceId });
  try {
    const res = await fetch(/api/user/${id});
    if (!res.ok) throw new Error(HTTP ${res.status});
    const data = await res.json();
    apiLogger.info('loadUserProfile success', { id, traceId, data });
    return data;
  } catch (err) {
    apiLogger.error('loadUserProfile failed', { id, traceId, error: err });
    throw err; // 不吞异常
  }
}

实际项目中的坑

我们有个后台管理系统,用了微前端架构(qiankun),主应用和子应用各自打日志。一开始子应用也 new 一个 Logger,prefix 设为 ‘SUB-AUTH’,结果上线后发现:子应用的 error 日志在主应用控制台里不显示——因为 qiankun 默认隔离了 console API(沙箱机制)。折腾半天,最后方案是:子应用通过 props 把 logger 实例传进来,或者直接 postMessage 到主应用,由主应用统一打。

还有个坑:Safari 对 console.group 的支持不稳定,尤其配合 source map 时,折叠组经常展开不了。所以现在我一律不用 group,改用 prefix + emoji 区分层级:

apiLogger.info('🔑 auth init');
apiLogger.info('📦 fetch config');
apiLogger.info('⚡️ send login req');

最后提一句:日志不是越多越好。我们曾经在表单提交前加了一行 console.log('form submit triggered'),结果用户狂点提交按钮,日志刷屏,反而掩盖了真正的报错。现在规则是:只在**分支判断点、异步入口/出口、异常捕获点**打 log,其他地方免谈。

结尾

以上是我这些年在日志记录上踩出来的最佳实践。没搞什么花哨的采样、分级上报、日志聚合——那些是后端或 SRE 的活。前端要做的,就三件事:打得准(位置对)、打得清(结构明)、打得安全(不泄密)。

这套方案不是最优解,但它简单、可控、排查快。我们最近也在试水把部分关键 error 自动上报到 https://jztheme.com/api/log(仅作技术演示用),但还没全量,因为得先跑通 GDPR 合规流程……这事下次再聊。

如果你有更好的日志组织方式,比如怎么优雅地做前端日志采样、怎么对接 OpenTelemetry、或者怎么让 QA 一键复制当前页面所有日志——欢迎评论区砸过来,我真会看,也会试。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论