|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
在JavaScript开发过程中,日志输出是一项不可或缺的技术。无论是简单的调试、复杂的错误追踪,还是生产环境中的问题排查,日志都扮演着至关重要的角色。本文将全面介绍JavaScript日志输出的各个方面,从基础的console.log方法到高级的日志管理技术,帮助开发者掌握调试技巧、错误追踪、性能优化和生产环境应用,最终提升开发效率和代码质量。
基础日志输出:console.log及其相关方法
console.log基础
console.log()是JavaScript中最基础也是最常用的日志输出方法。它可以将信息输出到浏览器的控制台或Node.js的终端中。
- console.log("Hello, World!"); // 输出字符串
- console.log(42); // 输出数字
- console.log({name: "John", age: 30}); // 输出对象
- console.log([1, 2, 3]); // 输出数组
复制代码
console.log()可以接受多个参数,它们会以空格分隔的形式输出:
- const name = "Alice";
- const age = 25;
- console.log("Name:", name, "Age:", age); // 输出: Name: Alice Age: 25
复制代码
其他console方法
除了console.log(),Console API还提供了许多其他有用的方法:
1. console.info():用于输出信息性消息,在大多数浏览器中与console.log()行为相同。
- console.info("This is an informational message");
复制代码
1. console.warn():用于输出警告信息,通常会有黄色背景或警告图标。
- console.warn("This is a warning message");
复制代码
1. console.error():用于输出错误信息,通常会有红色背景或错误图标。
- console.error("This is an error message");
复制代码
1. console.debug():用于输出调试信息,在默认情况下可能不可见,需要在控制台设置中启用。
- console.debug("This is a debug message");
复制代码
1. console.table():以表格形式显示数据,特别适合显示数组或对象数组。
- const users = [
- {name: "John", age: 30, city: "New York"},
- {name: "Alice", age: 25, city: "Los Angeles"},
- {name: "Bob", age: 35, city: "Chicago"}
- ];
- console.table(users);
复制代码
1. console.time()和console.timeEnd():用于测量代码执行时间。
- console.time("Operation");
- // 执行一些操作
- for (let i = 0; i < 1000000; i++) {
- Math.sqrt(i);
- }
- console.timeEnd("Operation"); // 输出: Operation: 12.345ms
复制代码
1. console.count():用于计数,可以标记某个代码段被执行的次数。
- for (let i = 0; i < 5; i++) {
- console.count("Loop executed");
- }
- // 输出:
- // Loop executed: 1
- // Loop executed: 2
- // Loop executed: 3
- // Loop executed: 4
- // Loop executed: 5
复制代码
1. console.group()和console.groupEnd():用于将相关日志分组显示,使输出更有层次。
- console.group("User Details");
- console.log("Name: John");
- console.log("Age: 30");
- console.group("Address");
- console.log("Street: 123 Main St");
- console.log("City: New York");
- console.groupEnd();
- console.groupEnd();
复制代码
1. console.trace():输出当前调用堆栈的跟踪信息。
- function functionA() {
- functionB();
- }
- function functionB() {
- functionC();
- }
- function functionC() {
- console.trace();
- }
- functionA();
复制代码
1. console.assert():当断言为false时输出错误信息。
- const value = 10;
- console.assert(value > 20, "Value is not greater than 20"); // 输出: Assertion failed: Value is not greater than 20
复制代码
格式化输出
console.log()支持格式化字符串,使用占位符来插入变量值:
- const name = "Alice";
- const age = 25;
- console.log("Name: %s, Age: %d", name, age); // 输出: Name: Alice, Age: 25
复制代码
支持的格式占位符包括:
• %s:字符串
• %d或%i:整数
• %f:浮点数
• %o:对象(可展开的DOM元素)
• %O:对象(可展开的JavaScript对象)
• %c:CSS样式
使用CSS样式:
- console.log("%cThis is a styled message", "color: blue; font-size: 20px; font-weight: bold;");
复制代码
高级日志管理技术
自定义日志系统
为了更好地控制日志输出,我们可以创建一个自定义的日志系统。这样的系统可以提供日志级别、格式化、过滤等功能。
- class Logger {
- constructor(level = 'info') {
- this.levels = {
- error: 0,
- warn: 1,
- info: 2,
- debug: 3
- };
- this.level = level;
- }
- setLevel(level) {
- if (this.levels.hasOwnProperty(level)) {
- this.level = level;
- } else {
- console.error(`Invalid log level: ${level}`);
- }
- }
- _log(level, ...args) {
- if (this.levels[level] <= this.levels[this.level]) {
- const timestamp = new Date().toISOString();
- console[level](`[${timestamp}] [${level.toUpperCase()}]`, ...args);
- }
- }
- error(...args) {
- this._log('error', ...args);
- }
- warn(...args) {
- this._log('warn', ...args);
- }
- info(...args) {
- this._log('info', ...args);
- }
- debug(...args) {
- this._log('debug', ...args);
- }
- }
- // 使用示例
- const logger = new Logger('debug');
- logger.error('This is an error message');
- logger.warn('This is a warning message');
- logger.info('This is an info message');
- logger.debug('This is a debug message');
- // 更改日志级别
- logger.setLevel('warn');
- logger.debug('This debug message will not be displayed');
- logger.warn('This warning message will still be displayed');
复制代码
日志级别管理
日志级别允许我们根据重要性过滤日志消息。常见的日志级别包括:
1. ERROR:严重错误,导致应用程序无法正常运行
2. WARN:潜在问题,不会阻止应用程序运行但需要注意
3. INFO:一般信息,表示应用程序正常运行
4. DEBUG:调试信息,通常仅在开发环境中使用
在生产环境中,我们通常将日志级别设置为WARN或ERROR,以减少日志量并提高性能。
日志格式化
统一的日志格式可以使日志更易于阅读和分析。我们可以扩展自定义日志系统以支持格式化:
- class FormattedLogger extends Logger {
- constructor(level = 'info', options = {}) {
- super(level);
- this.options = {
- showTimestamp: options.showTimestamp !== false,
- showLevel: options.showLevel !== false,
- template: options.template || '[{timestamp}] [{level}] {message}'
- };
- }
- _formatMessage(level, message) {
- let formatted = this.options.template;
-
- if (this.options.showTimestamp) {
- formatted = formatted.replace('{timestamp}', new Date().toISOString());
- }
-
- if (this.options.showLevel) {
- formatted = formatted.replace('{level}', level.toUpperCase());
- }
-
- formatted = formatted.replace('{message}', message);
-
- return formatted;
- }
- _log(level, ...args) {
- if (this.levels[level] <= this.levels[this.level]) {
- const formattedMessage = this._formatMessage(level, args.join(' '));
- console[level](formattedMessage);
- }
- }
- }
- // 使用示例
- const formattedLogger = new FormattedLogger('debug', {
- template: '[{timestamp}] [{level}] => {message}'
- });
- formattedLogger.info('Application started');
- formattedLogger.warn('Low disk space');
- formattedLogger.error('Database connection failed');
复制代码
日志持久化
在浏览器环境中,我们可以使用localStorage或IndexedDB来持久化日志:
- class PersistentLogger extends FormattedLogger {
- constructor(level = 'info', options = {}) {
- super(level, options);
- this.maxLogs = options.maxLogs || 1000;
- this.storageKey = options.storageKey || 'app_logs';
- this._loadLogs();
- }
- _loadLogs() {
- try {
- const storedLogs = localStorage.getItem(this.storageKey);
- this.logs = storedLogs ? JSON.parse(storedLogs) : [];
- } catch (e) {
- console.error('Failed to load logs from storage:', e);
- this.logs = [];
- }
- }
- _saveLogs() {
- try {
- localStorage.setItem(this.storageKey, JSON.stringify(this.logs));
- } catch (e) {
- console.error('Failed to save logs to storage:', e);
- }
- }
- _log(level, ...args) {
- super._log(level, ...args);
-
- const logEntry = {
- timestamp: new Date().toISOString(),
- level: level,
- message: args.join(' ')
- };
-
- this.logs.push(logEntry);
-
- // 保持日志数量不超过最大值
- if (this.logs.length > this.maxLogs) {
- this.logs = this.logs.slice(-this.maxLogs);
- }
-
- this._saveLogs();
- }
- getLogs(level) {
- if (level) {
- return this.logs.filter(log => log.level === level);
- }
- return this.logs;
- }
- clearLogs() {
- this.logs = [];
- this._saveLogs();
- }
- }
- // 使用示例
- const persistentLogger = new PersistentLogger('debug');
- persistentLogger.info('Application started');
- persistentLogger.warn('Low disk space');
- persistentLogger.error('Database connection failed');
- // 获取所有日志
- console.log(persistentLogger.getLogs());
- // 仅获取错误日志
- console.log(persistentLogger.getLogs('error'));
复制代码
远程日志记录
在生产环境中,将日志发送到远程服务器进行集中管理和分析是非常有用的:
- class RemoteLogger extends PersistentLogger {
- constructor(level = 'info', options = {}) {
- super(level, options);
- this.remoteUrl = options.remoteUrl;
- this.batchSize = options.batchSize || 10;
- this.flushInterval = options.flushInterval || 10000; // 10秒
- this.pendingLogs = [];
-
- if (this.remoteUrl) {
- setInterval(() => this.flush(), this.flushInterval);
- }
- }
- _log(level, ...args) {
- super._log(level, ...args);
-
- if (this.remoteUrl) {
- const logEntry = {
- timestamp: new Date().toISOString(),
- level: level,
- message: args.join(' '),
- userAgent: navigator.userAgent,
- url: window.location.href
- };
-
- this.pendingLogs.push(logEntry);
-
- if (this.pendingLogs.length >= this.batchSize) {
- this.flush();
- }
- }
- }
- async flush() {
- if (this.pendingLogs.length === 0) return;
-
- const logsToSend = [...this.pendingLogs];
- this.pendingLogs = [];
-
- try {
- const response = await fetch(this.remoteUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ logs: logsToSend })
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- } catch (e) {
- console.error('Failed to send logs to remote server:', e);
- // 如果发送失败,将日志重新放回待发送队列
- this.pendingLogs = [...logsToSend, ...this.pendingLogs];
- }
- }
- }
- // 使用示例
- const remoteLogger = new RemoteLogger('debug', {
- remoteUrl: 'https://api.example.com/logs',
- batchSize: 5,
- flushInterval: 5000
- });
- remoteLogger.info('Application started');
- remoteLogger.warn('Low disk space');
- remoteLogger.error('Database connection failed');
复制代码
调试技巧
条件日志记录
条件日志记录允许我们只在特定条件下输出日志,这在调试复杂问题时非常有用:
- function debugLog(condition, ...args) {
- if (condition) {
- console.log(...args);
- }
- }
- // 使用示例
- const debugMode = true;
- const user = { name: 'Alice', role: 'admin' };
- debugLog(debugMode, 'Current user:', user);
- debugLog(user.role === 'admin', 'Admin action performed');
复制代码
命名空间日志
使用命名空间可以帮助我们更好地组织和过滤日志:
- const createNamespaceLogger = (namespace) => {
- return {
- log: (...args) => console.log(`[${namespace}]`, ...args),
- info: (...args) => console.info(`[${namespace}]`, ...args),
- warn: (...args) => console.warn(`[${namespace}]`, ...args),
- error: (...args) => console.error(`[${namespace}]`, ...args),
- debug: (...args) => console.debug(`[${namespace}]`, ...args)
- };
- };
- // 使用示例
- const authLogger = createNamespaceLogger('AUTH');
- const dbLogger = createNamespaceLogger('DATABASE');
- const uiLogger = createNamespaceLogger('UI');
- authLogger.info('User logged in');
- dbLogger.warn('Slow query detected');
- uiLogger.error('Failed to render component');
复制代码
性能分析日志
使用日志来分析代码性能:
- function analyzePerformance(fn, ...args) {
- console.time(`Performance analysis: ${fn.name}`);
- const result = fn(...args);
- console.timeEnd(`Performance analysis: ${fn.name}`);
- return result;
- }
- // 使用示例
- function fibonacci(n) {
- if (n <= 1) return n;
- return fibonacci(n - 1) + fibonacci(n - 2);
- }
- const result = analyzePerformance(fibonacci, 30);
- console.log('Result:', result);
复制代码
状态变化日志
记录应用程序状态的变化,有助于理解数据流:
- class StateLogger {
- constructor(initialState) {
- this.state = initialState;
- this.logState('Initial state');
- }
- logState(action) {
- console.log(`[${action}] State:`, JSON.parse(JSON.stringify(this.state)));
- }
- updateState(updates, action = 'State update') {
- this.state = { ...this.state, ...updates };
- this.logState(action);
- return this.state;
- }
- }
- // 使用示例
- const appState = new StateLogger({ user: null, isLoading: false });
- appState.updateState({ isLoading: true }, 'Fetching user data');
- appState.updateState({ user: { name: 'Alice' }, isLoading: false }, 'User data fetched');
复制代码
事件流日志
记录事件流可以帮助我们理解应用程序的交互流程:
- function createEventLogger() {
- const events = [];
-
- return {
- log: (event, data) => {
- const timestamp = new Date().toISOString();
- events.push({ event, data, timestamp });
- console.log(`[EVENT] ${event}:`, data);
- },
- getEvents: () => events,
- clear: () => { events.length = 0; }
- };
- }
- // 使用示例
- const eventLogger = createEventLogger();
- // 模拟用户操作
- eventLogger.log('page_load', { page: 'home' });
- eventLogger.log('button_click', { button: 'login' });
- eventLogger.log('form_submit', { form: 'login', data: { username: 'alice' } });
- eventLogger.log('api_request', { endpoint: '/login', method: 'POST' });
- eventLogger.log('api_response', { status: 200, data: { token: 'abc123' } });
- eventLogger.log('navigation', { from: 'login', to: 'dashboard' });
- // 获取所有事件
- console.log(eventLogger.getEvents());
复制代码
错误追踪
错误日志记录
正确记录错误信息对于调试和修复问题至关重要:
- function logError(error, context = {}) {
- console.error('Error occurred:', {
- message: error.message,
- stack: error.stack,
- context: context,
- timestamp: new Date().toISOString(),
- userAgent: navigator.userAgent,
- url: window.location.href
- });
- }
- // 使用示例
- try {
- // 可能出错的代码
- JSON.parse('{invalid json}');
- } catch (error) {
- logError(error, {
- action: 'parseJSON',
- input: '{invalid json}'
- });
- }
复制代码
全局错误处理
设置全局错误处理器来捕获未处理的错误:
- // 全局JavaScript错误处理
- window.addEventListener('error', (event) => {
- console.error('Global error caught:', {
- message: event.message,
- filename: event.filename,
- lineno: event.lineno,
- colno: event.colno,
- error: event.error,
- timestamp: new Date().toISOString()
- });
-
- // 可以在这里添加将错误发送到服务器的代码
- });
- // 未处理的Promise拒绝处理
- window.addEventListener('unhandledrejection', (event) => {
- console.error('Unhandled promise rejection:', {
- reason: event.reason,
- timestamp: new Date().toISOString()
- });
-
- // 可以在这里添加将错误发送到服务器的代码
- });
- // 使用示例
- // 触发全局错误处理
- setTimeout(() => {
- undefinedFunction(); // 未定义的函数
- }, 1000);
- // 触发未处理的Promise拒绝
- setTimeout(() => {
- Promise.reject(new Error('Intentional rejection'));
- }, 2000);
复制代码
错误边界(React)
在React应用中,使用错误边界来捕获组件树中的JavaScript错误:
- import React from 'react';
- class ErrorBoundary extends React.Component {
- constructor(props) {
- super(props);
- this.state = { hasError: false, error: null, errorInfo: null };
- }
- static getDerivedStateFromError(error) {
- // 更新state以触发下一次渲染显示降级UI
- return { hasError: true };
- }
- componentDidCatch(error, errorInfo) {
- // 记录错误信息
- this.setState({
- error: error,
- errorInfo: errorInfo
- });
-
- // 可以在这里将错误日志发送到服务器
- console.error('Error caught by boundary:', error, errorInfo);
- }
- render() {
- if (this.state.hasError) {
- // 自定义降级UI
- return (
- <div>
- <h2>Something went wrong.</h2>
- <details style={{ whiteSpace: 'pre-wrap' }}>
- {this.state.error && this.state.error.toString()}
- <br />
- {this.state.errorInfo.componentStack}
- </details>
- </div>
- );
- }
- return this.props.children;
- }
- }
- // 使用示例
- function BuggyComponent() {
- // 这将触发错误
- return <>{undefinedVariable}</>;
- }
- function App() {
- return (
- <ErrorBoundary>
- <h1>My App</h1>
- <BuggyComponent />
- </ErrorBoundary>
- );
- }
复制代码
错误报告系统
创建一个错误报告系统,收集和发送错误信息:
- class ErrorReporter {
- constructor(options = {}) {
- this.url = options.url;
- this.appVersion = options.appVersion || 'unknown';
- this.environment = options.environment || 'development';
- this.setupGlobalHandlers();
- }
- setupGlobalHandlers() {
- window.addEventListener('error', (event) => {
- this.reportError(event.error, {
- type: 'global_error',
- filename: event.filename,
- lineno: event.lineno,
- colno: event.colno
- });
- });
- window.addEventListener('unhandledrejection', (event) => {
- this.reportError(event.reason, {
- type: 'unhandled_promise_rejection'
- });
- });
- }
- reportError(error, context = {}) {
- const errorReport = {
- message: error.message || String(error),
- stack: error.stack,
- context: {
- ...context,
- appVersion: this.appVersion,
- environment: this.environment,
- userAgent: navigator.userAgent,
- url: window.location.href,
- timestamp: new Date().toISOString()
- }
- };
- console.error('Error reported:', errorReport);
- if (this.url) {
- this.sendToServer(errorReport);
- }
- }
- async sendToServer(errorReport) {
- try {
- const response = await fetch(this.url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(errorReport)
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- } catch (e) {
- console.error('Failed to send error report:', e);
- }
- }
- }
- // 使用示例
- const errorReporter = new ErrorReporter({
- url: 'https://api.example.com/errors',
- appVersion: '1.0.0',
- environment: 'production'
- });
- // 手动报告错误
- try {
- // 可能出错的代码
- JSON.parse('{invalid json}');
- } catch (error) {
- errorReporter.reportError(error, {
- action: 'parseJSON',
- input: '{invalid json}'
- });
- }
复制代码
性能优化
性能监控日志
使用日志来监控应用程序的性能:
- class PerformanceMonitor {
- constructor() {
- this.metrics = {};
- }
- startTimer(name) {
- this.metrics[name] = {
- startTime: performance.now(),
- endTime: null,
- duration: null
- };
- }
- endTimer(name) {
- if (this.metrics[name] && !this.metrics[name].endTime) {
- this.metrics[name].endTime = performance.now();
- this.metrics[name].duration =
- this.metrics[name].endTime - this.metrics[name].startTime;
-
- console.log(`[PERFORMANCE] ${name}: ${this.metrics[name].duration.toFixed(2)}ms`);
-
- return this.metrics[name].duration;
- }
- return null;
- }
- getMetric(name) {
- return this.metrics[name];
- }
- getAllMetrics() {
- return this.metrics;
- }
- }
- // 使用示例
- const perfMonitor = new PerformanceMonitor();
- // 监控函数执行时间
- function fetchData() {
- perfMonitor.startTimer('fetchData');
-
- // 模拟API调用
- return new Promise(resolve => {
- setTimeout(() => {
- perfMonitor.endTimer('fetchData');
- resolve({ data: 'example data' });
- }, 500);
- });
- }
- fetchData().then(data => {
- console.log('Data received:', data);
- console.log('All metrics:', perfMonitor.getAllMetrics());
- });
复制代码
资源加载监控
监控资源加载时间:
- function monitorResourceLoading() {
- // 使用Performance API获取资源加载时间
- window.addEventListener('load', () => {
- const resources = performance.getEntriesByType('resource');
-
- console.group('Resource Loading Performance');
- resources.forEach(resource => {
- const duration = resource.duration.toFixed(2);
- const size = (resource.transferSize / 1024).toFixed(2);
-
- if (duration > 100 || size > 100) { // 只显示加载时间超过100ms或大小超过100KB的资源
- console.warn(`Slow or large resource: ${resource.name}`, {
- type: resource.initiatorType,
- duration: `${duration}ms`,
- size: `${size}KB`
- });
- }
- });
- console.groupEnd();
- });
- }
- // 使用示例
- monitorResourceLoading();
复制代码
用户交互性能
监控用户交互的性能:
- function measureInteractionPerformance() {
- // 使用Event Timing API
- const observer = new PerformanceObserver((list) => {
- for (const entry of list.getEntries()) {
- const delay = entry.processingStart - entry.startTime;
- const duration = entry.duration;
-
- console.log(`[INTERACTION] ${entry.name}:`, {
- delay: `${delay.toFixed(2)}ms`,
- duration: `${duration.toFixed(2)}ms`
- });
-
- // 如果交互延迟超过100ms,记录警告
- if (delay > 100) {
- console.warn(`Slow interaction detected: ${entry.name}`, {
- delay: `${delay.toFixed(2)}ms`,
- target: entry.target
- });
- }
- }
- });
-
- observer.observe({ entryTypes: ['event'] });
- }
- // 使用示例
- measureInteractionPerformance();
- // 添加一些交互元素来测试
- document.addEventListener('DOMContentLoaded', () => {
- const button = document.createElement('button');
- button.textContent = 'Click me';
- button.addEventListener('click', () => {
- // 模拟一些处理
- const start = performance.now();
- while (performance.now() - start < 50) {
- // 空循环,模拟处理时间
- }
- console.log('Button clicked');
- });
- document.body.appendChild(button);
- });
复制代码
内存使用监控
监控内存使用情况:
- function monitorMemoryUsage() {
- if (performance.memory) {
- const memoryData = {
- used: `${(performance.memory.usedJSHeapSize / 1048576).toFixed(2)} MB`,
- total: `${(performance.memory.totalJSHeapSize / 1048576).toFixed(2)} MB`,
- limit: `${(performance.memory.jsHeapSizeLimit / 1048576).toFixed(2)} MB`,
- usage: `${((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100).toFixed(2)}%`
- };
-
- console.log('[MEMORY USAGE]', memoryData);
-
- // 如果内存使用超过80%,发出警告
- if (parseFloat(memoryData.usage) > 80) {
- console.warn('High memory usage detected:', memoryData);
- }
-
- return memoryData;
- } else {
- console.log('Memory monitoring not supported in this browser');
- return null;
- }
- }
- // 使用示例
- // 每隔5秒监控一次内存使用
- setInterval(monitorMemoryUsage, 5000);
复制代码
生产环境应用
生产环境日志最佳实践
在生产环境中,日志记录需要遵循一些最佳实践:
1. 适当的日志级别:在生产环境中,通常只记录WARN和ERROR级别的日志,以减少性能影响。
2. 避免敏感信息:确保日志中不包含敏感信息,如密码、令牌、个人身份信息等。
3. 日志轮转:实现日志轮转机制,防止日志文件过大。
4. 结构化日志:使用结构化格式(如JSON)记录日志,便于后续分析。
5. 异步日志:使用异步方式记录日志,减少对主线程性能的影响。
适当的日志级别:在生产环境中,通常只记录WARN和ERROR级别的日志,以减少性能影响。
避免敏感信息:确保日志中不包含敏感信息,如密码、令牌、个人身份信息等。
日志轮转:实现日志轮转机制,防止日志文件过大。
结构化日志:使用结构化格式(如JSON)记录日志,便于后续分析。
异步日志:使用异步方式记录日志,减少对主线程性能的影响。
- class ProductionLogger {
- constructor(options = {}) {
- this.level = options.level || 'warn';
- this.levels = { error: 0, warn: 1, info: 2, debug: 3 };
- this.remoteUrl = options.remoteUrl;
- this.sanitizeFields = options.sanitizeFields || ['password', 'token', 'creditCard'];
- this.batchSize = options.batchSize || 10;
- this.pendingLogs = [];
- this.flushInterval = options.flushInterval || 10000;
-
- if (this.remoteUrl) {
- setInterval(() => this.flush(), this.flushInterval);
- }
- }
- sanitize(data) {
- if (!data || typeof data !== 'object') return data;
-
- const sanitized = { ...data };
-
- for (const field in sanitized) {
- if (this.sanitizeFields.includes(field)) {
- sanitized[field] = '[REDACTED]';
- } else if (typeof sanitized[field] === 'object' && sanitized[field] !== null) {
- sanitized[field] = this.sanitize(sanitized[field]);
- }
- }
-
- return sanitized;
- }
- _log(level, message, data = {}) {
- if (this.levels[level] > this.levels[this.level]) return;
-
- const logEntry = {
- timestamp: new Date().toISOString(),
- level: level,
- message: message,
- data: this.sanitize(data),
- userAgent: navigator.userAgent,
- url: window.location.href
- };
-
- // 在开发环境中,也输出到控制台
- if (process.env.NODE_ENV !== 'production') {
- console[level](`[${level.toUpperCase()}] ${message}`, logEntry.data);
- }
-
- // 添加到待发送队列
- this.pendingLogs.push(logEntry);
-
- if (this.pendingLogs.length >= this.batchSize) {
- this.flush();
- }
- }
- async flush() {
- if (this.pendingLogs.length === 0 || !this.remoteUrl) return;
-
- const logsToSend = [...this.pendingLogs];
- this.pendingLogs = [];
-
- try {
- // 使用navigator.sendBeacon进行非阻塞请求
- const data = JSON.stringify({ logs: logsToSend });
- const blob = new Blob([data], { type: 'application/json' });
-
- if (navigator.sendBeacon(this.remoteUrl, blob)) {
- console.log(`${logsToSend.length} logs sent successfully`);
- } else {
- // 如果sendBeacon不可用,回退到fetch
- const response = await fetch(this.remoteUrl, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: data,
- keepalive: true
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- }
- } catch (e) {
- console.error('Failed to send logs to remote server:', e);
- // 如果发送失败,将日志重新放回待发送队列
- this.pendingLogs = [...logsToSend, ...this.pendingLogs];
- }
- }
- error(message, data) {
- this._log('error', message, data);
- }
- warn(message, data) {
- this._log('warn', message, data);
- }
- info(message, data) {
- this._log('info', message, data);
- }
- debug(message, data) {
- this._log('debug', message, data);
- }
- }
- // 使用示例
- const prodLogger = new ProductionLogger({
- level: 'warn',
- remoteUrl: 'https://api.example.com/logs',
- sanitizeFields: ['password', 'token', 'creditCard', 'ssn'],
- batchSize: 5,
- flushInterval: 5000
- });
- prodLogger.info('Application started', { version: '1.0.0' });
- prodLogger.warn('Feature flag disabled', { flag: 'newFeature' });
- prodLogger.error('API request failed', {
- endpoint: '/users',
- status: 500,
- error: 'Internal server error',
- user: { id: 123, name: 'Alice', password: 'secret123' } // 密码将被自动过滤
- });
复制代码
客户端错误监控
在生产环境中,监控客户端错误非常重要:
- class ClientErrorMonitor {
- constructor(options = {}) {
- this.url = options.url;
- this.appVersion = options.appVersion || 'unknown';
- this.environment = options.environment || 'production';
- this.sampleRate = options.sampleRate || 1.0; // 错误采样率
- this.setupGlobalHandlers();
- }
- setupGlobalHandlers() {
- // 全局JavaScript错误处理
- window.addEventListener('error', (event) => {
- if (Math.random() < this.sampleRate) {
- this.reportError({
- type: 'javascript_error',
- message: event.message,
- filename: event.filename,
- lineno: event.lineno,
- colno: event.colno,
- stack: event.error?.stack
- });
- }
- });
- // 未处理的Promise拒绝处理
- window.addEventListener('unhandledrejection', (event) => {
- if (Math.random() < this.sampleRate) {
- this.reportError({
- type: 'unhandled_promise_rejection',
- message: event.reason?.message || String(event.reason),
- stack: event.reason?.stack
- });
- }
- });
- // 控制台错误拦截
- const originalError = console.error;
- console.error = (...args) => {
- originalError.apply(console, args);
-
- if (Math.random() < this.sampleRate) {
- this.reportError({
- type: 'console_error',
- message: args.map(arg =>
- typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
- ).join(' ')
- });
- }
- };
- }
- reportError(errorData) {
- const errorReport = {
- ...errorData,
- appVersion: this.appVersion,
- environment: this.environment,
- userAgent: navigator.userAgent,
- url: window.location.href,
- timestamp: new Date().toISOString(),
- screenSize: `${window.screen.width}x${window.screen.height}`,
- viewportSize: `${window.innerWidth}x${window.innerHeight}`
- };
- if (this.url) {
- this.sendToServer(errorReport);
- }
- }
- sendToServer(errorReport) {
- // 使用navigator.sendBeacon进行非阻塞请求
- const data = JSON.stringify(errorReport);
- const blob = new Blob([data], { type: 'application/json' });
-
- if (navigator.sendBeacon) {
- navigator.sendBeacon(this.url, blob);
- } else {
- // 回退方案
- fetch(this.url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: data,
- keepalive: true
- }).catch(e => console.error('Failed to send error report:', e));
- }
- }
- // 手动报告错误
- captureException(error, context = {}) {
- this.reportError({
- type: 'manual_report',
- message: error.message,
- stack: error.stack,
- context: context
- });
- }
- // 手动报告消息
- captureMessage(message, level = 'info') {
- this.reportError({
- type: 'manual_message',
- message: message,
- level: level
- });
- }
- }
- // 使用示例
- const errorMonitor = new ClientErrorMonitor({
- url: 'https://api.example.com/errors',
- appVersion: '1.0.0',
- environment: 'production',
- sampleRate: 0.5 // 只采样50%的错误
- });
- // 手动报告错误
- try {
- // 可能出错的代码
- JSON.parse('{invalid json}');
- } catch (error) {
- errorMonitor.captureException(error, {
- action: 'parseJSON',
- input: '{invalid json}'
- });
- }
- // 手动报告消息
- errorMonitor.captureMessage('User completed onboarding', 'info');
复制代码
性能监控
在生产环境中监控应用性能:
- class PerformanceMonitor {
- constructor(options = {}) {
- this.url = options.url;
- this.appVersion = options.appVersion || 'unknown';
- this.environment = options.environment || 'production';
- this.sampleRate = options.sampleRate || 0.1; // 性能数据采样率
- this.metrics = {};
- this.init();
- }
- init() {
- // 监控页面加载性能
- window.addEventListener('load', () => {
- if (Math.random() < this.sampleRate) {
- this.collectPageLoadMetrics();
- }
- });
- // 监控资源加载性能
- if ('PerformanceObserver' in window) {
- const resourceObserver = new PerformanceObserver((list) => {
- if (Math.random() < this.sampleRate) {
- this.collectResourceMetrics(list.getEntries());
- }
- });
- resourceObserver.observe({ entryTypes: ['resource'] });
- }
- // 监控用户交互性能
- if ('PerformanceObserver' in window) {
- const interactionObserver = new PerformanceObserver((list) => {
- if (Math.random() < this.sampleRate) {
- this.collectInteractionMetrics(list.getEntries());
- }
- });
- interactionObserver.observe({ entryTypes: ['event'] });
- }
- // 监控内存使用情况
- if (performance.memory) {
- setInterval(() => {
- if (Math.random() < this.sampleRate) {
- this.collectMemoryMetrics();
- }
- }, 60000); // 每分钟收集一次
- }
- }
- collectPageLoadMetrics() {
- const navigation = performance.getEntriesByType('navigation')[0];
-
- if (navigation) {
- const metrics = {
- type: 'page_load',
- domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
- loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
- firstPaint: this.getFirstPaint(),
- firstContentfulPaint: this.getFirstContentfulPaint(),
- timeToInteractive: this.getTimeToInteractive(),
- timestamp: new Date().toISOString()
- };
-
- this.sendMetrics(metrics);
- }
- }
- collectResourceMetrics(resources) {
- const slowResources = resources.filter(resource => resource.duration > 100);
-
- if (slowResources.length > 0) {
- const metrics = {
- type: 'slow_resources',
- resources: slowResources.map(resource => ({
- name: resource.name,
- type: resource.initiatorType,
- duration: resource.duration,
- size: resource.transferSize
- })),
- timestamp: new Date().toISOString()
- };
-
- this.sendMetrics(metrics);
- }
- }
- collectInteractionMetrics(interactions) {
- const slowInteractions = interactions.filter(interaction => {
- const delay = interaction.processingStart - interaction.startTime;
- return delay > 100; // 延迟超过100ms的交互
- });
-
- if (slowInteractions.length > 0) {
- const metrics = {
- type: 'slow_interactions',
- interactions: slowInteractions.map(interaction => ({
- name: interaction.name,
- delay: interaction.processingStart - interaction.startTime,
- duration: interaction.duration
- })),
- timestamp: new Date().toISOString()
- };
-
- this.sendMetrics(metrics);
- }
- }
- collectMemoryMetrics() {
- const metrics = {
- type: 'memory_usage',
- used: performance.memory.usedJSHeapSize,
- total: performance.memory.totalJSHeapSize,
- limit: performance.memory.jsHeapSizeLimit,
- timestamp: new Date().toISOString()
- };
-
- this.sendMetrics(metrics);
- }
- getFirstPaint() {
- const paintEntries = performance.getEntriesByType('paint');
- const fpEntry = paintEntries.find(entry => entry.name === 'first-paint');
- return fpEntry ? fpEntry.startTime : null;
- }
- getFirstContentfulPaint() {
- const paintEntries = performance.getEntriesByType('paint');
- const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
- return fcpEntry ? fcpEntry.startTime : null;
- }
- getTimeToInteractive() {
- // 这是一个简化的TTI计算,实际应用中可能需要更复杂的算法
- const navigation = performance.getEntriesByType('navigation')[0];
- if (!navigation) return null;
-
- const fcp = this.getFirstContentfulPaint();
- if (!fcp) return null;
-
- // 查找FCP之后5秒内没有长任务的窗口
- const longTasks = performance.getEntriesByType('longtask');
- const lastLongTask = longTasks
- .filter(task => task.startTime > fcp)
- .sort((a, b) => b.startTime - a.startTime)[0];
-
- if (!lastLongTask) return fcp + 5000; // 如果没有长任务,假设TTI是FCP后5秒
-
- return Math.max(lastLongTask.startTime + lastLongTask.duration, fcp + 5000);
- }
- sendMetrics(metrics) {
- const fullMetrics = {
- ...metrics,
- appVersion: this.appVersion,
- environment: this.environment,
- userAgent: navigator.userAgent,
- url: window.location.href
- };
-
- if (this.url) {
- // 使用navigator.sendBeacon进行非阻塞请求
- const data = JSON.stringify(fullMetrics);
- const blob = new Blob([data], { type: 'application/json' });
-
- if (navigator.sendBeacon) {
- navigator.sendBeacon(this.url, blob);
- } else {
- // 回退方案
- fetch(this.url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: data,
- keepalive: true
- }).catch(e => console.error('Failed to send metrics:', e));
- }
- }
- }
- // 手动记录自定义指标
- measureCustomMetric(name, value, tags = {}) {
- const metrics = {
- type: 'custom_metric',
- name: name,
- value: value,
- tags: tags,
- timestamp: new Date().toISOString()
- };
-
- this.sendMetrics(metrics);
- }
- }
- // 使用示例
- const perfMonitor = new PerformanceMonitor({
- url: 'https://api.example.com/metrics',
- appVersion: '1.0.0',
- environment: 'production',
- sampleRate: 0.1
- });
- // 手动记录自定义指标
- function checkout() {
- const startTime = performance.now();
-
- // 执行结账流程
- // ...
-
- const duration = performance.now() - startTime;
- perfMonitor.measureCustomMetric('checkout_duration', duration, {
- payment_method: 'credit_card'
- });
- }
复制代码
日志分析工具
浏览器开发者工具
现代浏览器提供了强大的开发者工具,用于分析日志和调试应用程序:
1. Console面板:查看和过滤日志消息使用console对象的方法进行日志记录执行JavaScript代码
2. 查看和过滤日志消息
3. 使用console对象的方法进行日志记录
4. 执行JavaScript代码
5. Network面板:监控网络请求查看请求和响应详情分析加载性能
6. 监控网络请求
7. 查看请求和响应详情
8. 分析加载性能
9. Performance面板:记录和分析运行时性能识别性能瓶颈分析CPU使用情况
10. 记录和分析运行时性能
11. 识别性能瓶颈
12. 分析CPU使用情况
13. Memory面板:分析内存使用情况识别内存泄漏比较内存快照
14. 分析内存使用情况
15. 识别内存泄漏
16. 比较内存快照
Console面板:
• 查看和过滤日志消息
• 使用console对象的方法进行日志记录
• 执行JavaScript代码
Network面板:
• 监控网络请求
• 查看请求和响应详情
• 分析加载性能
Performance面板:
• 记录和分析运行时性能
• 识别性能瓶颈
• 分析CPU使用情况
Memory面板:
• 分析内存使用情况
• 识别内存泄漏
• 比较内存快照
ELK Stack (Elasticsearch, Logstash, Kibana)
ELK Stack是一个强大的日志分析平台:
1. Elasticsearch:用于存储和索引日志数据
2. Logstash:用于收集、处理和转发日志数据
3. Kibana:用于可视化和分析日志数据
以下是如何将JavaScript应用与ELK Stack集成的示例:
- class ELKLogger {
- constructor(options = {}) {
- this.elasticsearchUrl = options.elasticsearchUrl || 'http://localhost:9200';
- this.indexName = options.indexName || 'js-logs';
- this.appName = options.appName || 'javascript-app';
- this.environment = options.environment || 'development';
- this.level = options.level || 'info';
- this.levels = { error: 0, warn: 1, info: 2, debug: 3 };
- this.batchSize = options.batchSize || 10;
- this.flushInterval = options.flushInterval || 10000;
- this.pendingLogs = [];
-
- if (this.elasticsearchUrl) {
- setInterval(() => this.flush(), this.flushInterval);
- }
- }
- _log(level, message, data = {}) {
- if (this.levels[level] > this.levels[this.level]) return;
-
- const logEntry = {
- '@timestamp': new Date().toISOString(),
- level: level,
- message: message,
- application: this.appName,
- environment: this.environment,
- data: data,
- userAgent: navigator.userAgent,
- url: window.location.href,
- screenSize: `${window.screen.width}x${window.screen.height}`,
- viewportSize: `${window.innerWidth}x${window.innerHeight}`
- };
-
- // 在开发环境中,也输出到控制台
- if (this.environment === 'development') {
- console[level](`[${level.toUpperCase()}] ${message}`, data);
- }
-
- // 添加到待发送队列
- this.pendingLogs.push(logEntry);
-
- if (this.pendingLogs.length >= this.batchSize) {
- this.flush();
- }
- }
- async flush() {
- if (this.pendingLogs.length === 0 || !this.elasticsearchUrl) return;
-
- const logsToSend = [...this.pendingLogs];
- this.pendingLogs = [];
-
- try {
- // 批量索引格式
- const bulkBody = logsToSend.flatMap(log => [
- { index: { _index: this.indexName } },
- log
- ]);
-
- const response = await fetch(`${this.elasticsearchUrl}/_bulk`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-ndjson'
- },
- body: bulkBody.map(JSON.stringify).join('\n') + '\n'
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const result = await response.json();
-
- if (result.errors) {
- console.error('Some logs failed to index:', result);
- }
- } catch (e) {
- console.error('Failed to send logs to Elasticsearch:', e);
- // 如果发送失败,将日志重新放回待发送队列
- this.pendingLogs = [...logsToSend, ...this.pendingLogs];
- }
- }
- error(message, data) {
- this._log('error', message, data);
- }
- warn(message, data) {
- this._log('warn', message, data);
- }
- info(message, data) {
- this._log('info', message, data);
- }
- debug(message, data) {
- this._log('debug', message, data);
- }
- }
- // 使用示例
- const elkLogger = new ELKLogger({
- elasticsearchUrl: 'http://localhost:9200',
- indexName: 'my-app-logs',
- appName: 'My JavaScript App',
- environment: 'production',
- level: 'info',
- batchSize: 5,
- flushInterval: 5000
- });
- elkLogger.info('Application started', { version: '1.0.0' });
- elkLogger.warn('Feature flag disabled', { flag: 'newFeature' });
- elkLogger.error('API request failed', {
- endpoint: '/users',
- status: 500,
- error: 'Internal server error'
- });
复制代码
Sentry
Sentry是一个流行的错误监控平台,特别适合前端应用:
- // 集成Sentry的示例
- import * as Sentry from "@sentry/browser";
- import { BrowserTracing } from "@sentry/tracing";
- // 初始化Sentry
- Sentry.init({
- dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
- integrations: [new BrowserTracing()],
-
- // 设置采样率,在生产环境中建议设置为0.2
- tracesSampleRate: 1.0,
-
- // 设置环境
- environment: process.env.NODE_ENV || 'development',
-
- // 设置发布版本
- release: "my-app@1.0.0",
-
- // 在发送前可以修改或丢弃事件
- beforeSend(event) {
- if (event.request) {
- event.request.headers = {
- ...event.request.headers,
- 'Authorization': '[REDACTED]'
- };
- }
- return event;
- }
- });
- // 手动捕获异常
- try {
- // 可能出错的代码
- myUndefinedFunction();
- } catch (error) {
- Sentry.captureException(error);
- }
- // 手动捕获消息
- Sentry.captureMessage("Something went wrong");
- // 添加用户上下文
- Sentry.withScope((scope) => {
- scope.setUser({ id: "123", email: "user@example.com" });
- scope.setTag("page", "checkout");
- scope.setExtra("data", {
- value1: "some data",
- value2: "another data"
- });
- Sentry.captureException(new Error("Custom error with context"));
- });
- // 事务跟踪
- const transaction = Sentry.startTransaction({
- name: "checkout-process",
- op: "transaction"
- });
- const span = transaction.startChild({ op: "task", description: "processing payment" });
- // 执行一些操作
- processPayment();
- span.finish();
- transaction.finish();
复制代码
LogRocket
LogRocket是一个前端监控和调试工具,可以记录用户会话、控制台日志、网络请求等:
- // 集成LogRocket的示例
- import LogRocket from 'logrocket';
- import setupLogRocketReact from 'logrocket-react';
- // 初始化LogRocket
- LogRocket.init('your-app-id');
- // 对于React应用,设置LogRocket React插件
- setupLogRocketReact(LogRocket);
- // 添加用户信息
- LogRocket.identify('123', {
- name: 'John Doe',
- email: 'john@example.com',
-
- // 添加自定义属性
- subscriptionType: 'pro',
- favoriteColor: 'blue'
- });
- // 记录自定义事件
- LogRocket.track('checkout_started', {
- items: 3,
- total: 99.99,
- currency: 'USD'
- });
- // 记录会话URL以便于查找
- const sessionURL = LogRocket.sessionURL;
- console.log('LogRocket session URL:', sessionURL);
- // 将LogRocket会话URL与其他错误报告系统集成
- Sentry.configureScope((scope) => {
- scope.setExtra("sessionURL", LogRocket.sessionURL);
- });
复制代码
Grafana Loki
Grafana Loki是一个日志聚合系统,与Grafana集成用于可视化:
- class LokiLogger {
- constructor(options = {}) {
- this.lokiUrl = options.lokiUrl || 'http://localhost:3100/loki/api/v1/push';
- this.username = options.username;
- this.password = options.password;
- this.labels = {
- ...options.labels,
- app: options.appName || 'javascript-app',
- env: options.environment || 'development'
- };
- this.level = options.level || 'info';
- this.levels = { error: 0, warn: 1, info: 2, debug: 3 };
- this.batchSize = options.batchSize || 10;
- this.flushInterval = options.flushInterval || 10000;
- this.pendingLogs = [];
-
- if (this.lokiUrl) {
- setInterval(() => this.flush(), this.flushInterval);
- }
- }
- _log(level, message, data = {}) {
- if (this.levels[level] > this.levels[this.level]) return;
-
- const logEntry = {
- timestamp: new Date().toISOString(),
- level: level,
- message: message,
- data: data,
- userAgent: navigator.userAgent,
- url: window.location.href
- };
-
- // 在开发环境中,也输出到控制台
- if (this.labels.env === 'development') {
- console[level](`[${level.toUpperCase()}] ${message}`, data);
- }
-
- // 添加到待发送队列
- this.pendingLogs.push(logEntry);
-
- if (this.pendingLogs.length >= this.batchSize) {
- this.flush();
- }
- }
- async flush() {
- if (this.pendingLogs.length === 0 || !this.lokiUrl) return;
-
- const logsToSend = [...this.pendingLogs];
- this.pendingLogs = [];
-
- try {
- // 准备Loki格式的日志
- const streams = [{
- stream: {
- ...this.labels,
- level: logsToSend[0].level
- },
- values: logsToSend.map(log => [
- `${Date.now()}000000`, // 纳秒时间戳
- JSON.stringify({
- message: log.message,
- data: log.data,
- userAgent: log.userAgent,
- url: log.url
- })
- ])
- }];
-
- const headers = {
- 'Content-Type': 'application/json'
- };
-
- // 添加基本认证(如果提供了用户名和密码)
- if (this.username && this.password) {
- headers['Authorization'] = `Basic ${btoa(`${this.username}:${this.password}`)}`;
- }
-
- const response = await fetch(this.lokiUrl, {
- method: 'POST',
- headers: headers,
- body: JSON.stringify({ streams })
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- } catch (e) {
- console.error('Failed to send logs to Loki:', e);
- // 如果发送失败,将日志重新放回待发送队列
- this.pendingLogs = [...logsToSend, ...this.pendingLogs];
- }
- }
- error(message, data) {
- this._log('error', message, data);
- }
- warn(message, data) {
- this._log('warn', message, data);
- }
- info(message, data) {
- this._log('info', message, data);
- }
- debug(message, data) {
- this._log('debug', message, data);
- }
- }
- // 使用示例
- const lokiLogger = new LokiLogger({
- lokiUrl: 'http://localhost:3100/loki/api/v1/push',
- username: 'loki',
- password: 'password',
- labels: {
- app: 'my-app',
- env: 'production',
- version: '1.0.0'
- },
- level: 'info',
- batchSize: 5,
- flushInterval: 5000
- });
- lokiLogger.info('Application started');
- lokiLogger.warn('Feature flag disabled', { flag: 'newFeature' });
- lokiLogger.error('API request failed', {
- endpoint: '/users',
- status: 500,
- error: 'Internal server error'
- });
复制代码
总结
有效的日志记录是JavaScript开发中不可或缺的一部分。从基础的console.log到高级的日志管理技术,良好的日志实践可以帮助我们:
1. 提高调试效率:通过详细的日志信息,快速定位和解决问题。
2. 增强错误追踪:捕获和记录错误,包括堆栈跟踪和上下文信息,以便于分析。
3. 优化性能:通过性能监控日志,识别和解决性能瓶颈。
4. 改善生产环境稳定性:在生产环境中实施适当的日志策略,及时发现和解决问题。
5. 提升用户体验:通过分析用户交互日志,了解用户行为并改进应用。
通过本文介绍的各种技术和工具,开发者可以构建一个全面的日志系统,从开发到生产环境,有效提升开发效率和代码质量。记住,好的日志不仅仅是记录信息,更是提供洞察力和解决问题的能力。
在实际应用中,根据项目需求和团队情况选择合适的日志策略和工具,并随着项目的发展不断调整和优化日志系统,使其成为开发流程中的有力助手。 |
|