日志等级

通常,日志等级从低到高可以分为以下几类:

  • DEBUG:详细的开发时信息,用于调试应用。

  • INFO:重要事件的简要信息,如系统启动、配置等。

  • WARN:系统能正常运行,但有潜在错误的情况。

  • ERROR:由于严重的问题,某些功能无法正常运行。

  • FATAL:非常严重的问题,可能导致系统崩溃。

我日常的开发可能就只考虑了自己设计的INFO(相当于分级中的DEBUG和INFO)和系统的ERROR,很多其余的是没有考虑到的,这里的分级更加规范。

日志内容和格式

一个完整的日志消息通常包括:

  • 时间戳:精确到毫秒的事件发生时间。

  • 日志等级:当前日志消息的等级。

  • 消息内容:描述事件的详细信息。

  • 错误堆栈:如果是错误,提供错误堆栈信息。

格式:

# [时间戳] [日志等级] [消息内容] [错误堆栈]
[2024-04-01T12:00:00.000Z] [ERROR] Failed to load user data. {stack}

日志输出

在前端项目中,我们通常使用console对象进行日志输出。不同的日志等级可以使用不同的console方法:

  • console.debug用于DEBUG级别。

  • console.info用于INFO级别。

  • console.warn用于WARN级别。

  • console.error用于ERROR和FATAL级别。

日志收集

生产环境中,我们可能需要将日志发送到后端服务器进行收集和分析(放到前端分析是不现实的)。这可以通过AJAX请求或专门的日志服务来实现。

class Logger {
  // ...其他方法

 // 根据环境变量判断是否发送日志到后端
if (process.env.NODE_ENV === 'production') {
  this.sendLog(formattedMessage);
}

  static sendLog(message) {
    // 假设我们有一个日志收集的API
    const logEndpoint = '/api/logs';
 fetch(logEndpoint, {
  method: 'POST', 
  headers: {
   'Content-Type': 'application/json', 
  }, body: JSON.stringify({ message }), }).catch((error) => {
  console.error('Failed to send log', error); 
 });
}

日志等级控制

在开发环境中,我们可能希望看到尽可能多的日志输出,以便更好地调试应用。但在生产环境中,为了避免性能损耗和过多的日志信息,我们可能只希望输出WARN和以上等级的日志。我们可以在Logger中添加一个等级控制(这也是一个非常好的思路):

class Logger {
  static level = 'DEBUG'; // 默认为DEBUG级别

  static setLevel(newLevel) {
    this.level = newLevel;
  }

  static shouldLog(level) {
    const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];
    return levels.indexOf(level) >= levels.indexOf(this.level);
  }

  static log(level, message, error) {
    if (!this.shouldLog(level)) {
      return;
    }
    // ...日志输出逻辑
  }

  // ...其他方法
}

// 生产环境中设置日志等级
if (process.env.NODE_ENV === 'production') {
  Logger.setLevel('WARN');
}

// 使用示例
Logger.debug('This will not be logged in production');
Logger.warn('This will be logged in production');

日志格式化输出

为了进一步提高日志的可读性,我们可以添加格式化功能,比如为不同等级的日志添加颜色,或者为错误堆栈提供更好的格式化。

class Logger {
  // ...其他方法

  static formatStack(stack) {
    if (!stack) return '';
    // 格式化错误堆栈的逻辑
    return stack.split('\n').map(line => `    at ${line}`).join('\n');
  }

  static log(level, message, error) {
    // ...日志输出逻辑

    // 格式化错误堆栈
    if (error) {
      formattedMessage += `\n${this.formatStack(error.stack)}`;
    }

    // ...输出逻辑
  }

  // ...其他方法
}

这里我之前看过另外一篇文章,也是讲日志输出格式的(利用console.log()):

console.log() 可以接受任何类型的参数,包括字符串、数字、布尔值、对象、数组、函数等。最厉害的是,它支持占位符!常用的占位符:

  • %s - 字符串

  • %d or %i - 整数

  • %f - 浮点数

  • %o - 对象

  • %c - CSS 样式

我觉得可以跟这里混合起来,我整合了一下这俩文章的内容,总体的代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Log Demo</title>
    <style>
        #logOutput {
            border: 1px solid #ccc;
            padding: 10px;
            margin-top: 20px;
            height: 200px;
            overflow-y: scroll;
            background: #f9f9f9;
        }
        button {
            padding: 10px;
            margin: 5px;
            cursor: pointer;
        }
    </style>
</head>
<body>
<button id="infoButton">Info Log</button>
<button id="errorButton">Error Log</button>
<button id="warningButton">Warning Log</button>
<button id="successButton">Success Log</button>
<div id="logOutput"></div>

<script type="module">
    // Set a global variable to determine the environment
    window.ENV = 'development'; // Change this to 'production' as needed

    class Logger {
        static level = 'DEBUG'; // 默认为DEBUG级别

        static setLevel(newLevel) {
            this.level = newLevel;
        }

        static shouldLog(level) {
            const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];
            return levels.indexOf(level) >= levels.indexOf(this.level);
        }

        static formatStack(stack) {
            if (!stack) return '';
            // 格式化错误堆栈的逻辑
            return stack.split('\n').map(line => `    at ${line}`).join('\n');
        }

        static log(level, message, error = null) {
            if (!this.shouldLog(level)) {
                return;
            }

            const timestamp = new Date().toISOString();
            const stack = error ? this.formatStack(error.stack) : '';
            let formattedMessage = `[${timestamp}] [${level}] ${message}`;
            if (stack) {
                formattedMessage += `\n${stack}`;
            }

            // 使用 prettyLog 方法输出日志
            switch (level) {
                case 'DEBUG':
                    Logger.prettyLog.info('DEBUG', formattedMessage);
                    break;
                case 'INFO':
                    Logger.prettyLog.info('INFO', formattedMessage);
                    break;
                case 'WARN':
                    Logger.prettyLog.warning('WARN', formattedMessage);
                    break;
                case 'ERROR':
                case 'FATAL':
                    Logger.prettyLog.error(level, formattedMessage);
                    break;
                default:
                    Logger.prettyLog.info('LOG', formattedMessage);
            }

            // 根据环境变量判断是否发送日志到后端
            if (window.ENV === 'production') {
                this.sendLog(formattedMessage);
            }
        }

        static debug(message) {
            Logger.log('DEBUG', message);
        }

        static info(message) {
            Logger.log('INFO', message);
        }

        static warn(message) {
            Logger.log('WARN', message);
        }

        static error(message, error) {
            Logger.log('ERROR', message, error);
        }

        static fatal(message, error) {
            Logger.log('FATAL', message, error);
        }

        static sendLog(message) {
            // 假设我们有一个日志收集的API
            const logEndpoint = '/api/logs';
            fetch(logEndpoint, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ message }),
            }).catch((error) => {
                console.error('Failed to send log', error);
            });
        }

        static prettyLog = (() => {
            const isProduction = false;  // 假设当前总是开发环境

            const isEmpty = (value) => {
                return value == null || value === '';
            };

            const prettyPrint = (title, text, color) => {
                if (isProduction) return;
                console.log(
                    `%c ${title} %c ${text} %c`,
                    `background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
                    `border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
                    'background:transparent'
                );
            };

            const info = (textOrTitle, content = '') => {
                const title = isEmpty(content) ? 'Info' : textOrTitle;
                const text = isEmpty(content) ? textOrTitle : content;
                prettyPrint(title, text, '#909399');
            };

            const error = (textOrTitle, content = '') => {
                const title = isEmpty(content) ? 'Error' : textOrTitle;
                const text = isEmpty(content) ? textOrTitle : content;
                prettyPrint(title, text, '#F56C6C');
            };

            const warning = (textOrTitle, content = '') => {
                const title = isEmpty(content) ? 'Warning' : textOrTitle;
                const text = isEmpty(content) ? textOrTitle : content;
                prettyPrint(title, text, '#E6A23C');
            };

            const success = (textOrTitle, content = '') => {
                const title = isEmpty(content) ? 'Success' : textOrTitle;
                const text = isEmpty(content) ? textOrTitle : content;
                prettyPrint(title, text, '#67C23A');
            };

            return {
                info,
                error,
                warning,
                success
            };
        })();
    }

    // 生产环境中设置日志等级
    if (window.ENV === 'production') {
        Logger.setLevel('WARN');
    }

    // 使用示例
    Logger.debug('This will not be logged in production');
    Logger.warn('This will be logged in production');
    Logger.error('An error occurred', new Error('Test Error'));
    document.addEventListener("DOMContentLoaded", function() {
        const logOutput = document.getElementById('logOutput');
        const log = Logger;

        document.getElementById('infoButton').addEventListener('click', function() {
            log.prettyLog.info("This is an info message");
            logOutput.innerHTML += '<p style="color: #909399;">Info: This is an info message</p>';
        });

        document.getElementById('errorButton').addEventListener('click', function() {
            log.prettyLog.error("This is an error message");
            logOutput.innerHTML += '<p style="color: #F56C6C;">Error: This is an error message</p>';
        });

        document.getElementById('warningButton').addEventListener('click', function() {
            log.prettyLog.warning("This is a warning message");
            logOutput.innerHTML += '<p style="color: #E6A23C;">Warning: This is a warning message</p>';
        });

        document.getElementById('successButton').addEventListener('click', function() {
            log.prettyLog.success("success","This is a success message");
            logOutput.innerHTML += '<p style="color: #67C23A;">Success: This is a success message</p>';
        });
    });
</script>
</body>
</html>

效果图:

参考资料:
都应该会的前端代码规范 - 日志打印规范 (qq.com)