Back

JavaScript 错误日志记录最佳实践

JavaScript 错误日志记录最佳实践

生产环境中的 JavaScript 应用程序每天都在悄无声息地失败。用户遇到的错误开发者永远看不到,导致糟糕的用户体验和收入损失。能够捕获这些问题的应用程序与无法捕获的应用程序之间的区别是什么?答案是适当的错误日志记录。

本文涵盖了在前端和后端环境中实现健壮的 JavaScript 错误日志记录的基本实践。您将学习如何超越 console.log,使用经过验证的框架实现结构化日志记录,并构建一个在用户报告之前就能捕获关键错误的系统。

核心要点

  • 控制台日志记录缺乏生产环境所需的持久性、集中化和结构化
  • 使用 Winston 或 Pino 等框架进行结构化日志记录可提供机器可解析的数据用于分析
  • 前端错误处理需要全局处理器和框架特定的解决方案,如 React Error Boundaries
  • 保护敏感数据和包含上下文信息对于有效的日志记录至关重要

为什么控制台日志记录在生产环境中不够用

大多数开发者从 console.log() 开始进行调试。虽然在开发期间足够使用,但这种方法在生产环境中会失效:

// This error disappears into the user's browser
try {
  processPayment(order);
} catch (error) {
  console.error(error); // Lost forever in production
}

控制台方法缺少:

  • 超出当前会话的持久性
  • 跨用户的集中收集
  • 用于分析的结构化数据
  • 用于优先级排序的严重性级别
  • 敏感数据保护

生产应用程序需要能够捕获、结构化并将错误传输到中心位置进行分析的日志记录。

使用经过验证的框架实现结构化日志记录

选择合适的框架

对于 Node.js 日志记录,两个框架主导着生态系统:

Winston 提供灵活性和广泛的传输选项:

const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log', level: 'error' })
  ]
});

Pino 优先考虑性能,开销最小:

const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  timestamp: pino.stdTimeFunctions.isoTime,
  formatters: {
    level: (label) => ({ level: label })
  }
});

结构化您的日志

用机器可以解析和分析的 JSON 对象替换非结构化字符串:

// Bad: Unstructured string
logger.info(`User ${userId} failed login attempt`);

// Good: Structured JSON
logger.info({
  event: 'login_failed',
  userId: userId,
  ip: request.ip,
  timestamp: new Date().toISOString(),
  userAgent: request.headers['user-agent']
});

有效的 JavaScript 错误日志记录的基本组成部分

1. 使用适当的日志级别

在整个应用程序中实现一致的严重性级别:

logger.debug('Detailed debugging information');
logger.info('Normal application flow');
logger.warn('Warning: degraded performance detected');
logger.error('Error occurred but application continues');
logger.fatal('Critical failure, application shutting down');

2. 始终包含堆栈跟踪

捕获完整的错误上下文以便调试:

process.on('uncaughtException', (error) => {
  logger.fatal({
    message: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString()
  });
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error({
    message: 'Unhandled Promise Rejection',
    reason: reason,
    promise: promise
  });
});

3. 添加上下文信息

包含请求 ID、用户 ID 和会话数据以追踪问题:

const requestLogger = logger.child({
  requestId: generateRequestId(),
  sessionId: request.session.id
});

requestLogger.info('Processing payment request');

4. 保护敏感数据

永远不要记录密码、令牌或个人信息:

const logger = pino({
  redact: ['password', 'creditCard', 'ssn', 'authorization']
});

// These fields will be automatically redacted
logger.info({
  user: email,
  password: 'secret123', // Will show as [REDACTED]
  action: 'login_attempt'
});

前端错误处理策略

全局错误处理器

在浏览器环境中捕获所有未处理的错误:

window.addEventListener('error', (event) => {
  logToServer({
    message: event.message,
    source: event.filename,
    line: event.lineno,
    column: event.colno,
    stack: event.error?.stack
  });
});

window.addEventListener('unhandledrejection', (event) => {
  logToServer({
    type: 'unhandledRejection',
    reason: event.reason,
    promise: event.promise
  });
});

React 错误边界

对于 React 应用程序,实现错误边界以捕获组件错误:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    logger.error({
      message: error.toString(),
      componentStack: errorInfo.componentStack,
      timestamp: new Date().toISOString()
    });
  }

  render() {
    if (this.state.hasError) {
      return <h2>Something went wrong. Please refresh the page.</h2>;
    }
    return this.props.children;
  }
}

集中日志以便分析

将所有日志定向到 stdout,让您的基础设施处理路由:

// Configure logger to output to stdout only
const logger = pino({
  transport: {
    target: 'pino-pretty',
    options: {
      destination: 1 // stdout
    }
  }
});

这种方法允许 Docker、Kubernetes 或日志传输工具(如 Fluentd)收集日志并将其路由到集中式系统进行分析。对于客户端应用程序,实现一个简单的端点来接收并转发从浏览器到集中式日志基础设施的日志。

结论

有效的 JavaScript 错误日志记录不仅仅是用框架替换 console.log。它需要结构化数据、适当的严重性级别、全面的错误上下文和集中收集。通过使用 Winston 或 Pino 等框架实现这些实践,保护敏感数据,并在前端代码中建立适当的错误边界,您可以创建一个在问题影响用户之前就能捕获它们的系统。从这些基础开始,然后根据应用程序的特定监控需求进行扩展。

常见问题

Pino 的开销最小,在大多数情况下只增加 2-3% 的延迟。Winston 稍重一些,但对于大多数应用程序来说仍然可以忽略不计。两者都已准备好用于生产环境,并被全球高流量应用程序使用。

使用日志框架中的内置脱敏功能来自动屏蔽敏感字段。定义要脱敏的字段名称列表,如密码、令牌和信用卡号。始终定期审核您的日志以防止意外的数据泄露。

在两端都记录错误。客户端日志记录捕获浏览器特定的问题和永远不会到达服务器的 JavaScript 错误。服务器端日志记录处理 API 错误和后端故障。使用集中式系统来聚合这两个来源。

立即为 error 和 fatal 级别配置警报。warning 级别可以触发每日摘要。info 和 debug 级别应该是可搜索的,但不应触发警报,除非您正在调查特定问题或监控关键业务事件。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay