Vue应用中的可观测性(Observability)集成:实现前后端日志、追踪与指标的统一收集

Vue 应用中的可观测性集成:实现前后端日志、追踪与指标的统一收集

大家好,今天我们来聊聊 Vue 应用中的可观测性集成。在微服务架构和日益复杂的 Web 应用环境中,可观测性变得越来越重要。它不仅仅是监控,而是通过日志、追踪和指标这三大支柱,帮助我们深入了解系统的内部状态,快速定位问题,并优化性能。

本次讲座,我们将重点关注如何在 Vue 应用中实现前后端日志、追踪与指标的统一收集,并提供一些代码示例和最佳实践。

1. 可观测性的三大支柱

在深入 Vue 应用的集成之前,我们先回顾一下可观测性的三大支柱:

  • 日志 (Logs): 离散的事件记录,通常包含时间戳、错误信息、请求详情等。日志是诊断问题的基础,可以帮助我们了解发生了什么。

  • 追踪 (Traces): 记录请求在不同服务或组件之间的调用链,可以帮助我们理解请求的路径和延迟。追踪是理解请求性能瓶颈的关键。

  • 指标 (Metrics): 量化的数据,例如 CPU 使用率、内存占用、请求延迟等。指标可以帮助我们监控系统的健康状况,并及时发现异常。

特性 日志 (Logs) 追踪 (Traces) 指标 (Metrics)
数据类型 文本,结构化文本 Span 组成的树状结构 数值,时序数据
目的 诊断问题,审计,调试 理解请求路径,性能分析,延迟分析 监控系统健康状况,趋势分析,容量规划
示例 错误日志,访问日志,应用日志 用户登录请求的整个调用链,包括前端、后端、数据库等 CPU 使用率,内存占用,请求延迟,错误率
收集方式 文件,标准输出,消息队列 OpenTelemetry,Jaeger,Zipkin 等 Prometheus,Grafana,StatsD 等

2. 前端日志收集

前端日志对于诊断用户遇到的问题至关重要。在 Vue 应用中,我们可以使用多种方式收集日志。

2.1 使用 console 方法

最简单的方式是使用浏览器的 console 对象提供的方法,例如 console.log, console.warn, console.error

// 示例:使用 console.error 记录错误信息
try {
  // 可能会抛出异常的代码
  const data = JSON.parse(invalidJsonString);
} catch (error) {
  console.error('JSON 解析失败:', error);
}

这种方式简单易用,但缺乏集中管理和分析的能力。我们需要更强大的工具来统一收集和分析日志。

2.2 使用 window.onerror

window.onerror 是一个全局事件处理函数,当 JavaScript 运行时发生未捕获的异常时,会被调用。我们可以利用它来捕获全局错误,并将错误信息发送到后端服务。

window.onerror = function(message, source, lineno, colno, error) {
  // 构造错误信息
  const errorInfo = {
    message: message,
    source: source,
    lineno: lineno,
    colno: colno,
    stack: error ? error.stack : null, // 包含调用堆栈
    timestamp: new Date().toISOString()
  };

  // 将错误信息发送到后端
  sendErrorLog(errorInfo);

  // 阻止浏览器默认的错误处理行为 (可选)
  return true;
};

async function sendErrorLog(errorInfo) {
  try {
    const response = await fetch('/api/log', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(errorInfo)
    });

    if (!response.ok) {
      console.error('发送错误日志失败:', response.status, response.statusText);
    }
  } catch (error) {
    console.error('发送错误日志时发生异常:', error);
  }
}

2.3 集成第三方日志库 (Sentry, Bugsnag, LogRocket)

更强大的方案是集成第三方日志库,例如 Sentry, Bugsnag, LogRocket 等。这些库提供了更丰富的功能,例如:

  • 自动捕获未处理的异常和 Promise rejected 错误。
  • 收集用户上下文信息 (例如,浏览器版本,操作系统,用户 ID)。
  • 记录面包屑 (Breadcrumbs),记录用户操作的轨迹。
  • 提供强大的错误分析和报告功能。

以 Sentry 为例,集成步骤如下:

  1. 安装 Sentry SDK:

    npm install @sentry/vue @sentry/tracing
  2. 初始化 Sentry:

    在 Vue 应用的入口文件 (例如 main.js) 中,初始化 Sentry SDK。

    import * as Sentry from "@sentry/vue";
    import { BrowserTracing } from "@sentry/tracing";
    import { createApp } from 'vue'
    import App from './App.vue'
    
    const app = createApp(App)
    
    Sentry.init({
      app,
      dsn: "YOUR_SENTRY_DSN", // 替换为你的 Sentry DSN
      integrations: [
        new BrowserTracing({
          tracePropagationTargets: ['localhost', /^https://yourserver.io/api/], // 追踪的目标 URL,避免追踪所有请求
        }),
      ],
      // 启用性能监控
      tracesSampleRate: 0.1, // 采样率,1.0 表示所有事务都会被追踪
      // 启用重放功能
      replaysSessionSampleRate: 0.1,
      replaysOnErrorSampleRate: 1.0, // 如果发生错误,则始终记录重放
    });
    
    app.mount('#app')

    重要说明: dsn 是 Sentry 项目的唯一标识符,需要替换为你的 Sentry DSN。tracesSampleRatereplaysSessionSampleRate 用于控制性能追踪和重放的采样率。调整这些值以平衡性能开销和数据收集量。tracePropagationTargets指定需要追踪的请求域名。

  3. 手动记录事件:

    可以使用 Sentry.captureException 记录异常,使用 Sentry.captureMessage 记录消息。

    import * as Sentry from "@sentry/vue";
    
    try {
      // 可能会抛出异常的代码
      const data = JSON.parse(invalidJsonString);
    } catch (error) {
      Sentry.captureException(error);
    }
    
    Sentry.captureMessage("用户点击了按钮");
  4. 配置 Sentry 插件 (可选):

    Sentry 提供了 Vue 插件,可以自动捕获 Vue 组件生命周期中的错误。

    // main.js
    app.use(Sentry); // 注册 Sentry Vue 插件

Sentry 会自动收集未处理的异常、Promise rejected 错误、Vue 组件生命周期中的错误,并提供强大的错误分析和报告功能。

3. 后端日志收集

后端日志收集通常使用成熟的日志框架,例如 Node.js 中的 Winston, Morgan, Pino 等。

3.1 使用 Winston

Winston 是一个流行的 Node.js 日志库,支持多种传输方式 (例如,控制台,文件,数据库,远程服务)。

  1. 安装 Winston:

    npm install winston
  2. 配置 Winston:

    const winston = require('winston');
    
    const logger = winston.createLogger({
      level: 'info', // 日志级别
      format: winston.format.combine(
        winston.format.timestamp({
          format: 'YYYY-MM-DD HH:mm:ss'
        }),
        winston.format.errors({ stack: true }),
        winston.format.splat(),
        winston.format.json()
      ),
      defaultMeta: { service: 'your-service' }, // 默认元数据
      transports: [
        new winston.transports.Console(), // 输出到控制台
        new winston.transports.File({ filename: 'error.log', level: 'error' }), // 输出到文件
        new winston.transports.File({ filename: 'combined.log' }), // 输出到文件
      ],
    });
    
    // 如果不是生产环境,则输出更详细的日志到控制台
    if (process.env.NODE_ENV !== 'production') {
      logger.add(new winston.transports.Console({
        format: winston.format.simple(),
      }));
    }
    
    module.exports = logger;
  3. 使用 Winston:

    const logger = require('./logger');
    
    logger.info('服务器启动');
    logger.warn('即将执行高风险操作');
    logger.error('发生错误:', { error: new Error('Something went wrong') });
    
    try {
      // 可能会抛出异常的代码
      throw new Error('Something went wrong');
    } catch (error) {
      logger.error('发生异常:', { error: error });
    }

Winston 提供了灵活的配置选项,可以根据需要配置日志级别、格式和传输方式。

3.2 集成 Morgan

Morgan 是一个 HTTP 请求日志中间件,可以方便地记录 HTTP 请求的详细信息。

  1. 安装 Morgan:

    npm install morgan
  2. 集成 Morgan 到 Express 应用:

    const express = require('express');
    const morgan = require('morgan');
    const logger = require('./logger'); // 引入 Winston logger
    
    const app = express();
    
    // 使用 morgan 记录 HTTP 请求日志
    app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } })); // 将 morgan 的输出重定向到 winston
    
    app.get('/', (req, res) => {
      res.send('Hello World!');
    });
    
    app.listen(3000, () => {
      logger.info('服务器监听 3000 端口');
    });

Morgan 提供了多种预定义的日志格式,也可以自定义日志格式。

4. 前后端追踪

追踪可以帮助我们理解请求在前后端之间的调用链,并快速定位性能瓶颈。OpenTelemetry 是一个流行的开源可观测性框架,提供了统一的 API 和 SDK,可以用于收集和导出追踪数据。

4.1 使用 OpenTelemetry

  1. 安装 OpenTelemetry SDK 和相关依赖:

    npm install @opentelemetry/sdk @opentelemetry/node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-zipkin @opentelemetry/propagator-b3

    对于前端,使用 @opentelemetry/sdk-browser, @opentelemetry/exporter-collector

  2. 配置 OpenTelemetry (后端):

    // tracing.js
    const { NodeTracerProvider } = require('@opentelemetry/sdk-node');
    const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
    const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
    const { registerInstrumentations } = require('@opentelemetry/instrumentation');
    const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
    const { B3Propagator } = require('@opentelemetry/propagator-b3');
    const { Resource } = require('@opentelemetry/resources');
    const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
    
    const serviceName = 'your-node-service'; // 你的服务名称
    const provider = new NodeTracerProvider({
      resource: new Resource({
      }),
    });
    
    // 配置 Zipkin Exporter (或者其他 Exporter, 例如 Jaeger)
    const exporter = new ZipkinExporter({
      serviceName: serviceName,
      url: 'http://localhost:9411/api/v2/spans', // Zipkin 的地址
    });
    
    provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
    
    provider.register({
       propagator: new B3Propagator(), // 使用 B3 传播上下文
    });
    
    // 自动 Instrumentation
    registerInstrumentations({
      instrumentations: [
        getNodeAutoInstrumentations(), // 自动收集 Node.js 相关的信息
      ],
    });
    
    console.log('Tracing initialized');
    
    module.exports = {
      tracer: provider.getTracer(serviceName), // 获取 Tracer
    };
  3. 配置 OpenTelemetry (前端):

    // tracing.js (前端)
    import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
    import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
    import { ZoneContextManager } from '@opentelemetry/context-zone';
    import { CollectorTraceExporter } from '@opentelemetry/exporter-collector';
    import { B3Propagator } from '@opentelemetry/propagator-b3';
    import { registerInstrumentations } from '@opentelemetry/instrumentation';
    import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
    import { Resource } from '@opentelemetry/resources';
    import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
    
    const serviceName = 'your-vue-app'; // 你的服务名称
    
    const provider = new WebTracerProvider({
      resource: new Resource({
      }),
    });
    
    const exporter = new CollectorTraceExporter({
      url: 'http://localhost:55681/v1/traces', // OTLP Collector 的地址
    });
    
    provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
    
    provider.register({
      contextManager: new ZoneContextManager(),
      propagator: new B3Propagator(),
    });
    
    registerInstrumentations({
      instrumentations: [
        getWebAutoInstrumentations({
          '@opentelemetry/instrumentation-fetch': {
            propagate: true, // 传播上下文
          },
        }),
      ],
    });
    
    console.log('Tracing initialized (前端)');
    
    export const tracer = provider.getTracer(serviceName);
  4. 在后端启动追踪:

// index.js (后端)
require('./tracing'); // 引入 tracing.js,启动 OpenTelemetry
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});
  1. 在 Vue 应用中使用 Tracer:

    // App.vue
    import { tracer } from './tracing';
    
    export default {
      mounted() {
        const span = tracer.startSpan('App.mounted');
        setTimeout(() => {
          span.end();
        }, 100);
      },
      methods: {
        async fetchData() {
          const span = tracer.startSpan('fetchData');
          try {
            const response = await fetch('/api/data'); // 确保你的后端也启用了 OpenTelemetry
            const data = await response.json();
            console.log(data);
          } catch (error) {
            console.error(error);
          } finally {
            span.end();
          }
        },
      },
    };
  2. 在后端创建 Span:

    //  (后端)
    const { tracer } = require('./tracing');
    
    app.get('/api/data', (req, res) => {
      const span = tracer.startSpan('get /api/data');
      setTimeout(() => {
        res.json({ message: 'Hello from backend!' });
        span.end();
      }, 50);
    });

    重要说明:

    • @opentelemetry/auto-instrumentations-node@opentelemetry/auto-instrumentations-web 提供了自动 Instrumentation 功能,可以自动收集 HTTP 请求、数据库查询等信息。
    • B3Propagator 用于在前后端之间传播追踪上下文。
    • 需要配置 Exporter 将追踪数据发送到 Zipkin, Jaeger 或其他追踪后端。
    • 确保前后端服务都启用了 OpenTelemetry,并且使用了相同的上下文传播方式。
    • propagate: true 必须在前端的 fetch instrumentation 中启用,才能将上下文传播到后端。

5. 前后端指标

指标可以帮助我们监控系统的健康状况,并及时发现异常。Prometheus 是一个流行的开源指标监控系统,可以用于收集和存储时序数据。

5.1 使用 Prometheus

  1. 安装 Prometheus 客户端库 (后端):

    npm install prom-client
  2. 配置 Prometheus 客户端 (后端):

    // metrics.js
    const client = require('prom-client');
    
    const collectDefaultMetrics = client.collectDefaultMetrics;
    collectDefaultMetrics({ prefix: 'your_service_' }); // 添加前缀
    
    const httpRequestDurationMicroseconds = new client.Histogram({
      name: 'http_request_duration_seconds',
      help: 'Duration of HTTP requests in seconds',
      labelNames: ['method', 'route', 'status_code'],
      buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]  // 定义 buckets
    });
    
    module.exports = {
      register: client.register,
      httpRequestDurationMicroseconds: httpRequestDurationMicroseconds
    };
  3. 集成 Prometheus 到 Express 应用 (后端):

    // server.js
    const express = require('express');
    const app = express();
    const port = 3000;
    const metrics = require('./metrics');
    
    // 记录 HTTP 请求持续时间
    app.use((req, res, next) => {
      const start = Date.now();
      res.on('finish', () => {
        const duration = (Date.now() - start) / 1000;
        metrics.httpRequestDurationMicroseconds
          .labels(req.method, req.path, res.statusCode)
          .observe(duration);
      });
      next();
    });
    
    app.get('/', (req, res) => {
      res.send('Hello World!');
    });
    
    app.get('/metrics', async (req, res) => {
      res.setHeader('Content-Type', metrics.register.contentType);
      res.send(await metrics.register.metrics());
    });
    
    app.listen(port, () => {
      console.log(`Example app listening on port ${port}`);
    });
  4. 配置 Prometheus 客户端 (前端):

    前端通常不直接暴露 Prometheus 指标。可以将一些关键指标 (例如,页面加载时间,API 请求延迟) 上报到后端,由后端统一收集和暴露。

    // App.vue
    methods: {
      async fetchData() {
        const start = Date.now();
        try {
          const response = await fetch('/api/data');
          const data = await response.json();
          console.log(data);
        } catch (error) {
          console.error(error);
        } finally {
          const duration = Date.now() - start;
          // 将延迟上报到后端
          await fetch('/api/report-metric', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              name: 'frontend_api_request_duration_ms',
              value: duration
            })
          });
        }
      },
    },
  5. 后端接收前端上报的指标:

    // server.js
    app.post('/api/report-metric', express.json(), (req, res) => {
      const { name, value } = req.body;
      // 创建或更新 Counter 或 Gauge
      // 这里需要根据指标的类型来选择合适的 metric 类型
      // 这里只是一个示例,实际应用中需要更完善的处理
      console.log(`Received metric: ${name} = ${value}`);
      res.sendStatus(200);
    });
  6. 配置 Prometheus Server:

    配置 Prometheus Server 从 /metrics 端点抓取指标。

    # prometheus.yml
    scrape_configs:
      - job_name: 'your-service'
        scrape_interval: 5s
        static_configs:
          - targets: ['localhost:3000'] # 你的服务地址
  7. 使用 Grafana 可视化指标:

    配置 Grafana 连接到 Prometheus Server,并创建仪表盘可视化指标。

    重要说明:

    • Prometheus 主要用于收集和存储时序数据。
    • 可以使用 prom-client 库创建各种类型的指标,例如 Counter, Gauge, Histogram, Summary。
    • 需要配置 Prometheus Server 从应用程序的 /metrics 端点抓取指标。
    • 可以使用 Grafana 可视化 Prometheus 指标。
    • 前端指标通常需要上报到后端,由后端统一收集和暴露。

6. 统一收集和分析

将前后端日志、追踪和指标统一收集到集中的可观测性平台 (例如,Elasticsearch, Grafana, Loki, Jaeger) 可以帮助我们更全面地了解系统的内部状态,并快速定位问题。

  • 日志: 可以使用 Fluentd, Logstash, Filebeat 等工具将日志收集到 Elasticsearch 或 Loki。
  • 追踪: 可以使用 Jaeger, Zipkin, Tempo 等工具存储和查询追踪数据。
  • 指标: 可以使用 Prometheus 收集和存储指标数据,并使用 Grafana 可视化指标。

7. 注意事项与最佳实践

  • 保护敏感信息: 避免在日志、追踪和指标中记录敏感信息 (例如,密码,信用卡号)。
  • 控制采样率: 在高流量环境下,需要控制追踪和指标的采样率,以避免性能开销。
  • 使用结构化日志: 使用结构化日志 (例如,JSON 格式) 可以方便地进行查询和分析。
  • 添加上下文信息: 在日志、追踪和指标中添加上下文信息 (例如,用户 ID,请求 ID) 可以帮助我们更好地理解数据。
  • 设置合理的告警规则: 设置合理的告警规则可以及时发现异常,并通知相关人员。
  • 持续优化可观测性: 可观测性是一个持续优化的过程,需要根据实际情况不断调整和完善。

总结:构建健全的可观测性体系

我们讨论了如何在 Vue 应用中集成可观测性,涵盖了前后端日志、追踪和指标的收集方法,并介绍了常用的工具和最佳实践。希望本次讲座能帮助大家构建健全的可观测性体系,提升应用的可维护性和性能。

更多IT精英技术系列讲座,到智猿学院

发表回复

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