在一个 Vue 项目中,如何实现一个通用的日志管理系统,支持日志级别、上报和可视化?

各位观众,老司机又来开车了!今天咱们聊点儿实际的,如何在 Vue 项目里打造一个靠谱的日志管理系统。这玩意儿就像汽车的黑匣子,平时默默无闻,关键时刻能帮你找到问题的根源,避免背锅。

第一章:需求分析与架构设计

先别急着撸代码,咱们得搞清楚目标:

  1. 日志级别: 必须要有不同的日志级别,比如 DEBUGINFOWARNERROR,方便我们筛选重要信息。
  2. 本地存储: 日志要先存在本地,万一网络不好,数据也不会丢。
  3. 自动上报: 当日志达到一定数量或者满足特定条件时,自动上报到服务器。
  4. 可视化展示: 在服务器端,能清晰地看到各种日志信息,最好还能按级别、时间等进行过滤。

基于以上需求,我们可以设计一个简单的架构:

graph LR
    A[Vue 前端] --> B(日志管理模块);
    B --> C{LocalStorage};
    B --> D[日志上报模块];
    D --> E(服务器端);
    E --> F{数据库};
    E --> G[可视化展示];

第二章:前端日志管理模块

咱们先从前端开始,核心是创建一个日志管理模块,负责记录、存储和上报日志。

2.1 日志级别定义

先定义一个枚举类型的日志级别,方便管理:

// src/utils/logger.js
const LogLevel = {
  DEBUG: 'debug',
  INFO: 'info',
  WARN: 'warn',
  ERROR: 'error'
};

Object.freeze(LogLevel); // 防止被修改

export default LogLevel;

2.2 日志记录函数

接下来,创建一个 Logger 类,提供记录不同级别日志的方法:

// src/utils/logger.js
import LogLevel from './log-level';

class Logger {
  constructor(options = {}) {
    this.level = options.level || LogLevel.DEBUG; // 默认 DEBUG 级别
    this.appName = options.appName || 'default-app'; // 应用名称
    this.storageKey = `log-${this.appName}`; // localStorage key
    this.logs = this.loadLogs(); // 从 localStorage 加载日志
    this.maxLogs = options.maxLogs || 1000; // 最大日志数量
    this.uploadThreshold = options.uploadThreshold || 50; // 上报阈值
    this.uploadUrl = options.uploadUrl || '/api/logs'; // 上报接口
    this.uploadInterval = options.uploadInterval || 60000; // 上报间隔
    this.uploadTimer = null; // 上报定时器

    this.startAutoUpload();
  }

  // 判断是否允许记录该级别的日志
  isLevelEnabled(level) {
    const levelMap = {
      [LogLevel.DEBUG]: 1,
      [LogLevel.INFO]: 2,
      [LogLevel.WARN]: 3,
      [LogLevel.ERROR]: 4
    };

    return levelMap[level] >= levelMap[this.level];
  }

  // 记录日志
  log(level, message, ...args) {
    if (!this.isLevelEnabled(level)) {
      return;
    }

    const logEntry = {
      level,
      message,
      timestamp: new Date().toISOString(),
      args
    };

    this.logs.push(logEntry);

    // 限制日志数量
    if (this.logs.length > this.maxLogs) {
      this.logs.shift(); // 移除最旧的日志
    }

    this.saveLogs(); // 保存到 localStorage
    console.log(`[${level.toUpperCase()}] ${message}`, ...args); // 同时输出到控制台

    if (this.logs.length >= this.uploadThreshold) {
      this.uploadLogs();
    }
  }

  debug(message, ...args) {
    this.log(LogLevel.DEBUG, message, ...args);
  }

  info(message, ...args) {
    this.log(LogLevel.INFO, message, ...args);
  }

  warn(message, ...args) {
    this.log(LogLevel.WARN, message, ...args);
  }

  error(message, ...args) {
    this.log(LogLevel.ERROR, message, ...args);
  }

  // 从 localStorage 加载日志
  loadLogs() {
    try {
      const logs = localStorage.getItem(this.storageKey);
      return logs ? JSON.parse(logs) : [];
    } catch (error) {
      console.error('Failed to load logs from localStorage:', error);
      return [];
    }
  }

  // 保存日志到 localStorage
  saveLogs() {
    try {
      localStorage.setItem(this.storageKey, JSON.stringify(this.logs));
    } catch (error) {
      console.error('Failed to save logs to localStorage:', error);
    }
  }

  // 上报日志
  async uploadLogs() {
    if (this.isUploading) {
      return; // 避免重复上报
    }

    if (this.logs.length === 0) {
      return; // 没有日志
    }

    this.isUploading = true;

    try {
      const logsToSend = [...this.logs]; // 复制一份,避免上报过程中日志被修改
      this.logs = []; // 清空本地日志

      const response = await fetch(this.uploadUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          appName: this.appName,
          logs: logsToSend
        })
      });

      if (!response.ok) {
        // 上报失败,重新保存到 localStorage
        this.logs = logsToSend.concat(this.loadLogs());
        this.saveLogs();
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      this.saveLogs(); // 保存清空后的日志
      console.log('Logs uploaded successfully!');

    } catch (error) {
      console.error('Failed to upload logs:', error);
      // 上报失败,重新保存到 localStorage
      this.logs = logsToSend.concat(this.loadLogs());
      this.saveLogs();
    } finally {
      this.isUploading = false;
    }
  }

  // 自动上报
  startAutoUpload() {
    if (this.uploadTimer) {
      clearInterval(this.uploadTimer);
    }

    this.uploadTimer = setInterval(() => {
      if (this.logs.length > 0) {
        this.uploadLogs();
      }
    }, this.uploadInterval);
  }

  // 停止自动上报
  stopAutoUpload() {
    if (this.uploadTimer) {
      clearInterval(this.uploadTimer);
      this.uploadTimer = null;
    }
  }
}

export { Logger };

2.3 Vue 项目中使用

main.js 中初始化 Logger 实例,并挂载到 Vue 原型上:

// src/main.js
import Vue from 'vue';
import App from './App.vue';
import { Logger } from './utils/logger';
import LogLevel from './utils/log-level';

Vue.config.productionTip = false;

const logger = new Logger({
  level: LogLevel.DEBUG, // 设置日志级别
  appName: 'my-vue-app', // 应用名称
  uploadUrl: '/api/logs', // 上报接口地址
  uploadThreshold: 10,     // 达到10条日志就上报
  uploadInterval: 30000    // 每隔30秒上报一次
});

Vue.prototype.$logger = logger;

new Vue({
  render: h => h(App),
}).$mount('#app');

这样,在任何 Vue 组件中,都可以通过 this.$logger.debug('message')this.$logger.info('message') 等方法记录日志。

第三章:服务器端日志处理

接下来,我们需要在服务器端接收并存储前端上报的日志。这里以 Node.js + Express + MongoDB 为例:

3.1 安装依赖

npm install express body-parser mongoose --save

3.2 创建 Express 应用

// server.js
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

const app = express();
const port = 3000;

// 连接 MongoDB
mongoose.connect('mongodb://localhost:27017/vue-logs', { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connected to MongoDB'))
  .catch(err => console.error('MongoDB connection error:', err));

// 定义日志 Schema
const LogSchema = new mongoose.Schema({
  appName: String,
  level: String,
  message: String,
  timestamp: String,
  args: [mongoose.Schema.Types.Mixed]
});

const Log = mongoose.model('Log', LogSchema);

// 配置 body-parser
app.use(bodyParser.json());

// 定义接收日志的接口
app.post('/api/logs', async (req, res) => {
  try {
    const { appName, logs } = req.body;

    if (!Array.isArray(logs)) {
      return res.status(400).send('Invalid logs format.');
    }

    const logDocuments = logs.map(log => ({
      ...log,
      appName: appName
    }));

    await Log.insertMany(logDocuments);
    res.status(200).send('Logs received successfully.');
  } catch (error) {
    console.error('Failed to save logs:', error);
    res.status(500).send('Failed to save logs.');
  }
});

// 启动服务器
app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

3.3 创建前端页面(可选)

如果想做个简单的可视化展示,可以写一个前端页面,从 MongoDB 读取日志并显示出来。这个就看个人需求了,可以用 Vue,React,或者原生JS来实现。

一个简单的Vue页面:

<template>
  <div>
    <h1>Logs</h1>
    <div v-for="log in logs" :key="log._id">
      <p><strong>{{ log.timestamp }}</strong> - {{ log.appName }} - {{ log.level }}: {{ log.message }}</p>
      <p v-if="log.args && log.args.length">Arguments: {{ log.args }}</p>
      <hr>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      logs: []
    };
  },
  mounted() {
    this.fetchLogs();
  },
  methods: {
    async fetchLogs() {
      try {
        const response = await axios.get('/api/logs');
        this.logs = response.data;
      } catch (error) {
        console.error('Failed to fetch logs:', error);
      }
    }
  }
};
</script>

对应的后端接口:

app.get('/api/logs', async (req, res) => {
  try {
    const logs = await Log.find().sort({ timestamp: -1 }).limit(100); // 获取最新的100条日志
    res.status(200).json(logs);
  } catch (error) {
    console.error('Failed to fetch logs:', error);
    res.status(500).send('Failed to fetch logs.');
  }
});

第四章:进阶功能与优化

  • 日志采样: 如果日志量太大,可以考虑只采样一部分日志进行上报,比如只上报 WARNERROR 级别的日志。
  • 错误堆栈信息: 对于 ERROR 级别的日志,可以捕获错误堆栈信息,方便定位问题。
  • 日志搜索与过滤: 在可视化展示界面,提供按时间、级别、关键字等条件搜索和过滤日志的功能。
  • 告警机制: 当出现特定类型的错误日志时,自动发送告警邮件或短信。
  • 性能优化: 批量上报日志,减少网络请求次数。使用更高效的存储方式,比如压缩日志文件。

总结

一个完善的日志管理系统,就像给你的 Vue 项目装了一个千里眼,能让你及时发现并解决问题。当然,这只是一个简单的示例,实际项目中还需要根据具体需求进行调整和优化。希望今天的分享对大家有所帮助!记住,好的日志系统能让你少加班,多摸鱼!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注