🚀前端监控全链路解析+全栈实现🚀

前言

想一想,现在老板让你做一个性能优化,你怎么知道需要优化哪些地方、哪些指标,以及做完优化之后怎么判断是否有效呢?这个时候你就需要一个性能监控。

再想想,生产有某个地方用户反馈点击没反应,这个时候应该如何排查呢?成熟的错误日志平台会给你帮助。

假设说你做的是一个电商平台,怎么更直观的看到今天有多少用户点击了下单按钮跳到购物车了呢?也许有一个自定义埋点监控会更好。

所以本文会围绕着 3 个与前端息息相关的方向展开,即:

  • 性能监控
  • 自定义埋点监控
  • 错误监控(下篇会详细讲述)

包含了监控 SDK 设计、数据上报、数据存储、数据可视化分析,是一次全栈实战的体验。

本文比较长,建议点赞收藏🐶。希望耐心看完之后对你有帮助,如果我有哪里说得不对的,也恳请各位大佬批评指教。

PS:我记得有一次面试被面试官问过如何搭建一个监控平台,希望看完本文之后你再遇到类似的面试题目可以游刃有余的回答。

性能监控

对于性能监控,我们把它分为两类,一类是一些首屏加载指标,比如加载时间、 FCP 等;另一类是运行时的指标,比如 FPS 、内存占用、网络指标等。

首屏指标浅析

Navigation Timing API 是一个由 W3C 标准化的 Web API ,提供了关于页面加载和导航过程中各个时间点的详细性能数据。这些数据可以帮助我们分析和优化页面加载性能。以下是 Navigation Timing API 中一些主要的属性和方法:

主要的时间戳属性

  • navigationStart:浏览器准备加载新页面的起始时间。
  • unloadEventStart / unloadEventEnd:前一个页面的 unload 事件开始和结束的时间。
  • redirectStart / redirectEnd:重定向开始和结束的时间。
  • fetchStart:浏览器准备从服务器获取第一个字节的时间。
  • domainLookupStart / domainLookupEnd:DNS 查询开始和结束的时间。
  • connectStart / connectEnd:浏览器与服务器建立连接的开始和结束的时间。
  • secureConnectionStart:安全连接开始的时间。
  • requestStart / responseStart / responseEnd:请求开始、收到响应的第一个字节、收到响应的最后一个字节的时间。
  • domLoading / domInteractive / domContentLoadedEventStart / domContentLoadedEventEnd:DOM 加载开始、可以交互的时间、DOMContentLoaded 事件开始和结束的时间。
  • domComplete:DOM 加载完成的时间。
  • loadEventStart / loadEventEndload 事件开始和结束的时间。

通过这些属性,我们可以来计算一些我们比较关心的指标信息:

网络请求指标

  1. 请求开始时间:浏览器准备加载新页面到开始请求资源的时间。
  2. 首字节时间(Time to First Byte, TTFB):从发出请求到收到响应的第一个字节的时间。
  3. 内容下载时间:开始接收响应数据到接收完所有响应数据的时间。
  4. 请求完成时间:从开始请求资源到完成接收所有响应数据的时间。
js 复制代码
window.addEventListener('load', function() {
    const performanceTiming = window.performance.timing;
    // 请求开始时间
    const requestStartTime = performanceTiming.requestStart - performanceTiming.navigationStart;
    console.log('Request Start Time: ' + requestStartTime + ' ms');

    // 首字节时间(TTFB)
    const ttfb = performanceTiming.responseStart - performanceTiming.requestStart;
    console.log('Time to First Byte (TTFB): ' + ttfb + ' ms');

    // 内容下载时间
    const contentDownloadTime = performanceTiming.responseEnd - performanceTiming.responseStart;
    console.log('Content Download Time: ' + contentDownloadTime + ' ms');

    // 请求完成时间
    const requestCompletionTime = performanceTiming.responseEnd - performanceTiming.requestStart;
    console.log('Request Completion Time: ' + requestCompletionTime + ' ms');
});

页面加载时间(Page Load Time):从浏览器加载新页面开始,到页面所有资源,包括图像、脚本等完全加载。

js 复制代码
window.addEventListener('load', function() {
    const performanceTiming = window.performance.timing;
    const pageLoadTime = performanceTiming.loadEventEnd - performanceTiming.navigationStart;
    console.log('Page Load Time: ' + pageLoadTime + ' ms');
});

文档加载完成时间(DOMContentLoaded) :从浏览器加载新页面开始,到 DOM 内容完全加载并解析完成。

js 复制代码
document.addEventListener('DOMContentLoaded', function() {
    const performanceTiming = window.performance.timing;
    const domContentLoadedTime = performanceTiming.domContentLoadedEventEnd - performanceTiming.navigationStart;
    console.log('DOMContentLoaded Time: ' + domContentLoadedTime + ' ms');
});

首次内容绘制时间(First Contentful Paint, FCP) :指浏览器渲染出第一个文本、图片、非空白 canvasSVG 的时间。

js 复制代码
  if (
    PerformanceObserver &&
    PerformanceObserver.supportedEntryTypes &&
    PerformanceObserver.supportedEntryTypes.includes("paint")
  ) {
    const observer = new PerformanceObserver((list) => {
      list.getEntriesByName("first-contentful-paint").forEach((entry) => {
        console.log("First Contentful Paint: " + entry.startTime + " ms");
      });
    });
    observer.observe({ type: "paint", buffered: true });
  }

首次绘制时间(First Paint, FP) :计算浏览器开始渲染任何内容的时间。

js 复制代码
  if (
    PerformanceObserver &&
    PerformanceObserver.supportedEntryTypes &&
    PerformanceObserver.supportedEntryTypes.includes("paint")
  ) {
    const observer = new PerformanceObserver((list) => {
      list.getEntriesByName("first-paint").forEach((entry) => {
        console.log("First Paint: " + entry.startTime + " ms");
      });
    });
    observer.observe({ type: "paint", buffered: true });
  }

最大内容绘制时间(Largest Contentful Paint, LCP) :计算视口内最大内容元素完成渲染的时间。

js 复制代码
  if (
    PerformanceObserver &&
    PerformanceObserver.supportedEntryTypes &&
    PerformanceObserver.supportedEntryTypes.includes(
      "largest-contentful-paint"
    )
  ) {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        console.log("Largest Contentful Paint: " + entry.startTime + " ms");
      });
    });
    observer.observe({ type: "largest-contentful-paint", buffered: true });
  }

首次输入延迟(First Input Delay, FID) :计算用户首次与页面交互到浏览器响应的时间。

js 复制代码
  if (
    PerformanceObserver &&
    PerformanceObserver.supportedEntryTypes &&
    PerformanceObserver.supportedEntryTypes.includes("first-input")
  ) {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        console.log(
          "First Input Delay: " +
            (entry.processingStart - entry.startTime) +
            " ms"
        );
      });
    });
    observer.observe({ type: "first-input", buffered: true });
  }

可互动时间(Time to Interactive, TTI) :指页面完全加载,并能够快速响应用户输入的时间。通常情况下,TTI 可以定义为主线程空闲时间达到一定阈值(例如 5 秒)并且页面已经完成渲染并可以响应用户交互的时间点。

这里手动计算比较麻烦,所以使用了Google官方的计算TTI的库 ------ tti-polyfill

js 复制代码
// 伪代码
  ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
    longtaskObserver.disconnect();
  });

累积布局偏移(Cumulative Layout Shift, CLS) :主要用于衡量页面在加载过程中的视觉稳定性。它反映了页面内容在加载过程中是否会发生不期而至的布局变化,这些变化可能会导致用户误操作或者视觉上的不适。

减少布局偏移可以提升页面的视觉稳定性和用户满意度,从而降低用户流失率,提高页面的转化率和留存率。

js 复制代码
  let totalCls = 0;
  const clsObserver = new PerformanceObserver((list) => {
    const entries = list.getEntries();

    // 遍历所有 layout-shift 类型的 PerformanceEntry
    for (const entry of entries) {
      if (entry.entryType === "layout-shift") {
        // 计算每个 layout-shift 事件的 CLS 分数
        const clsScore = entry.value; // CLS 分数
        console.log("Layout Shift Score:", clsScore);
        totalCls += clsScore;
        // 在这里进行累加,计算累计布局偏移 CLS
        // 累计布局偏移 CLS = 累计 layout-shift 事件的 CLS 分数之和
      }
    }
  });

  // 开始监听 layout-shift 类型的 PerformanceEntry
  clsObserver.observe({ type: "layout-shift", buffered: true });
  window.addEventListener("load", function () {
    // 整个页面(包括资源)已经加载完成,在这里停止计算 CLS 或进行其他操作
    console.log("页面加载完成");
    clsObserver.disconnect();
  });

总阻塞时间(Total Blocking Time, TBT) :从 FCP 到页面完全互动(Time to Interactive, TTI)之间的所有长任务(长于50毫秒)的总时间。

js 复制代码
let longtaskObserver;
let TBT = 0;
if (
  PerformanceObserver &&
  PerformanceObserver.supportedEntryTypes &&
  PerformanceObserver.supportedEntryTypes.includes("paint")
) {
  const observer = new PerformanceObserver((list) => {
    list.getEntriesByName("first-contentful-paint").forEach((entry) => {
      longtaskObserver = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        entries.forEach((entry) => {
          if (entry.duration > 50) {
            // 长任务持续时间超过 50 毫秒
            console.log("Long task detected:", entry);
            // 在这里累加长任务的持续时间,用于计算 TBT
            TBT += entry.duration;
          }
        });
      });

      longtaskObserver.observe({ entryTypes: ["longtask"] });
    });
  });

  observer.observe({ type: "paint", buffered: true });
}
// 伪代码
  ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
    longtaskObserver.disconnect();
  });

Dom节点数量

  • DOM 节点的数量直接影响浏览器的内存消耗。过多的 DOM 节点会占用大量内存,导致页面性能下降。
  • 较大的 DOM 树会增加浏览器的渲染和重绘时间。减少 DOM 节点的数量可以提高页面的渲染性能,从而提升用户体验。

这里使用 MutationObserver 来获取整棵 dom 树的数量,这里后续上报采用定时上报的方式。

js 复制代码
    window.addEventListener("load", () => {
      // 目标节点
      const targetNode = document.body; // 你可以替换成你希望监听的任何节点
      console.log("targetNode", targetNode);
      // 配置选项
      const config = {
        childList: true, // 监听目标节点的子节点的变化
        subtree: true, // 监听整个子树的变化
      };

      // 回调函数,当 DOM 发生变化时会被调用
      const callback = function (mutationsList, observer) {
        let nodeCount = 0;
        for (let mutation of mutationsList) {
          if (mutation.type === "childList") {
            // 统计添加和删除的节点数量
            nodeCount +=
              mutation.addedNodes.length - mutation.removedNodes.length;
          }
        }
        if (nodeCount > 0) {
          console.log("DOM节点数量变化:", nodeCount);
          console.log(
            "当前DOM节点数量:",
            document.getElementsByTagName("*").length
          );
        }
      };

      // 创建一个 MutationObserver 实例并传入回调函数
      const domObserver = new MutationObserver(callback);

      // 开始监听
      domObserver.observe(targetNode, config);
    });

FPS

帧率(Frames Per Second, FPS)是评估页面动画和用户交互流畅度的重要指标。可以使用 requestAnimationFrame 来实现一个帧率监控。它会在浏览器准备好绘制下一帧时调用指定的回调函数。通过计算多个帧之间的时间差,可以得到帧率。

这里可以分成两个维度去上报,第一个维度是直接上报 FPS ,最终可以统计一个平均帧率,中位数等;第二个可以上报一个低帧率,比如 FPS 小于 30 。这样等我们做了一些性能优化之后,可以看平均帧率有无提高,低帧率总数有无降低。

js 复制代码
let lastTime = performance.now();
let frameCount = 0;
let fps = 0;

function monitorFPS() {
    const currentTime = performance.now();
    frameCount++;
    
    const deltaTime = currentTime - lastTime;
    
    if (deltaTime >= 1000) { // 每秒计算一次 FPS
        fps = (frameCount / deltaTime) * 1000;
        console.log(`当前 FPS: ${fps.toFixed(2)}`);
        frameCount = 0;
        lastTime = currentTime;
        
    }
    requestAnimationFrame(monitorFPS);
}

// 启动 FPS 监控
monitorFPS();

内存监控

使用 PerformaceAPI 来获取当前内存的使用情况,可以使用定时的方式来进行数据上报

js 复制代码
if (performance.memory) {
  console.log('JS Heap Size Limit:', performance.memory.jsHeapSizeLimit);
  console.log('Total JS Heap Size:', performance.memory.totalJSHeapSize);
  console.log('Used JS Heap Size:', performance.memory.usedJSHeapSize);
}

网络监控

navigator.connection API 提供了有关设备的网络连接信息,可以监听网络连接的变化。

js 复制代码
  if ("connection" in navigator) {
    const connection =
      navigator.connection ||
      navigator.mozConnection ||
      navigator.webkitConnection;

    function updateConnectionStatus() {
      console.log("Effective network type: " + connection.effectiveType);
      console.log("Downlink: " + connection.downlink + "Mbps");
      console.log("RTT: " + connection.rtt + "ms");
      console.log(
        "Save Data Mode: " + (connection.saveData ? "on" : "off")
      );
    }

    connection.addEventListener("change", updateConnectionStatus);
    updateConnectionStatus();
  }

SDK开发

介绍完上面的一些指标和采集的思路之后,我们开始来开发 SDK ,打包工具选择的是 rollup 。首先 npm init 创建一个项目。

然后安装一些必要的库

sql 复制代码
npm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-babel @rollup/plugin-json rollup-plugin-terser rollup-plugin-serve rollup-plugin-livereload --save-dev

看一下整个项目结构:

根目录下创建一个 rollup.config.js 文件,填入如下配置:

js 复制代码
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { babel } from "@rollup/plugin-babel";
import { terser } from "rollup-plugin-terser";
import json from "@rollup/plugin-json";
import serve from "rollup-plugin-serve";
import livereload from "rollup-plugin-livereload";
const isDev = process.env.NODE_ENV === "development";
export default {
  input: "src/index.js",
  output: {
    file: "dist/monitoring-sdk.js",
    format: "umd",
    name: "MonitoringSDK",
  },
  plugins: [
    resolve(),
    commonjs(),
    babel({ babelHelpers: "bundled" }),
    terser(),
    json(),
    ...(isDev
      ? [
          serve({
            open: true,
            contentBase: ["public", "dist"],
            port: 3000,
          }),
          livereload({
            watch: "dist",
          }),
        ]
      : []),
  ],
};

解释下上面的配置:

  • input :打包入口
  • ouput
    • file : 指定输出文件的路径和名称,这里是 dist/monitoring-sdk.js
    • format : 指定输出模块的格式,这里是 umd(通用模块定义),可以兼容多种模块系统(CommonJS、AMD、全局变量)。
    • name : 指定当以 <script> 标签引入时的全局变量名称,这里是 MonitoringSDK,即 window.MonitoringSDK
  • 插件:
    • resolve:打包npm模块
    • commonjs:将commonjs规范转换为ES6
    • babel:ES6+转换为ES5或者更兼容的版本
    • terser:打包压缩优化等
    • serve:启动一个本地服务器,开发调试使用
    • livereload:开发过程中的热重载

在public目录下新建一个index.html,填入如下内容:

html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>监控SDK调试</title>
    <script src="../monitoring-sdk.js"></script>
  </head>
  <body>
    <h1>监控SDK调试</h1>
    <script>
      console.log("MonitoringSDK", window.MonitoringSDK);
    </script>
  </body>
</html>

然后加入如下两个命令

开发调试时使用 npm run start ,我们随便写点东西:

可以看到开发服务器已经启动起来

然后我们就可以在 Performance.js 中编写性能监控相关的代码

FCP 为例,

采集到数据之后需要进行上报,我们先定义好一个 request 函数,后续再实现上报的逻辑。真正的埋点系统会有更多的参数预留,我们这里仅作讲解,就只上报了两个参数, typevalue

自定义埋点

同时需要对外暴露一个自定义埋点的函数,新建一个 Custom.js ,编写如下逻辑:

js 复制代码
import { MONITOR_TYPE } from "./constant";
import { request } from "./request";

class Custom {
  log = (value) => {
    request(MONITOR_TYPE.CUSTOM, value);
  };
}

export { Custom };

再改一下入口文件:

数据上报

现在我们就开始处理数据上报的逻辑,接触过数据上报的同学可能会比较清楚,对于数据上报。我们一般情况下会有三种方式:

sendBeacon

优点:

  1. 使用简单,适合发送较小的数据量。
  2. 不会阻塞页面卸载或影响页面性能。

缺点:

  1. 有大小限制(大约 64KB),不适合发送大数据。
  2. 不支持所有旧版浏览器。
js 复制代码
navigator.sendBeacon('/log', JSON.stringify({type: 'FCP',value:"1"}));

GIF

优点:

  1. 几乎所有浏览器都支持这种方式,包括一些非常老的浏览器。
  2. 实现简单,通过构造一个 Image 对象来发送数据。

缺点:

  1. URL 有长度限制,不适合发送大数据。
js 复制代码
const img = new Image(); 
img.src = '/log.gif?type=FCP&value=1';

AJAX

优点:

  1. 可以发送和接收大量数据,适合复杂的数据交互。
  2. 可以处理服务器响应,确认数据是否成功接收。

缺点:

  1. 在此场景下需要处理跨域问题

我们最后就采用gif的方式来作为埋点上报的方式,在具体请求函数中写入如下代码:

js 复制代码
const BASE_URL = "http://localhost:8080/monitor/log.gif?params=";
export const request = (type, value) => {
  let params = { type, value };
  params = JSON.stringify(params);
  params = encodeURIComponent(params);
  const url = `${BASE_URL}${params}`;
  const img = new Image();
  img.src = url;
};

OK,至此,我们已经把请求发出去。接下来我们就需要起一个后端服务来处理请求逻辑了。

我这边选的是Nest,没有接触过的同学可以看一下我的Nest专栏或者其他Nest相关的资料。

ts 复制代码
import { Controller, Get, Query, Res } from '@nestjs/common';
import { LogService } from './log.service';
import { Response } from 'express';
@Controller('monitor')
export class LogController {
  constructor(private readonly logService: LogService) {}
  private readonly base64Gif =
    'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
  @Get('log.gif')
  log(@Query('query') query: any, @Res() res: Response) {
    this.logService.handleLog(query);

    res.setHeader('Content-Type', 'image/gif');
    res.send(Buffer.from(this.base64Gif, 'base64'));
  }
}

访问 http://localhost:8080/monitor/log.gif?q=q 即可以上报数据。

数据存储

Loki、Grafana 和 Promtail 是一个现代日志处理和监控平台的组成部分。它们结合在一起,提供了日志收集、存储和可视化的功能。

1. Loki

简介

Loki 是由 Grafana Labs 开发的开源日志聚合系统。它被设计为轻量级、分布式、可扩展的日志存储解决方案。与传统的日志系统不同,Loki 专注于高效地存储、索引和检索日志数据。

主要功能

  • 日志收集:接收来自应用程序、服务和系统的日志数据。
  • 索引和存储:以低成本的方式存储日志数据,并为日志数据提供基本的索引功能。
  • 日志查询:支持灵活的查询语言,可以方便地从日志数据中提取有用的信息。
  • 集成 Grafana:无缝集成 Grafana 来进行日志可视化和分析。

2. Grafana

简介

Grafana 是一个开源的数据可视化和监控平台。它可以从各种数据源中获取数据,并提供图形化的仪表板来展示数据。Grafana 是日志数据可视化的核心工具。

主要功能

  • 数据可视化:支持各种图表、表格和面板来展示数据。
  • 仪表板创建:用户可以创建和定制仪表板,展示监控数据和日志信息。
  • 数据源管理:支持多种数据源,包括Prometheus、Loki、Elasticsearch等。
  • 报警系统:可以设置警报规则,并在触发时发送通知。

Promtail

简介

Promtail 是一个日志收集代理,用于将日志数据从文件系统中提取出来并发送到 Loki。它是Loki生态系统中的一个重要组件,负责从各种来源抓取日志。

主要功能

  • 日志收集:从文件系统、系统日志和其他日志来源中提取日志数据。
  • 日志处理:支持对日志进行标签处理、过滤和转换。
  • 数据推送:将处理后的日志数据发送到Loki进行存储和索引。

首先确保你的环境里已经安装好docker跟docker-compose,然后我们新建一个docker-compose.yml文件如下:

yml 复制代码
version: '3'
services:
  loki:
    image: grafana/loki:2.2.0
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - ./loki/config:/etc/loki
      - ./loki/data:/loki
    user: "1000:1000"

  promtail:
    image: grafana/promtail:2.2.0
    volumes:
      - /var/log:/var/log
      - ./promtail/config:/etc/promtail
    command: -config.file=/etc/promtail/promtail-config.yaml

  grafana:
    image: grafana/grafana:7.3.6
    environment:
      - GF_SECURITY_ADMIN_USER=myadminuser
      - GF_SECURITY_ADMIN_PASSWORD=myadminpassword
    ports:
      - "3000:3000"

再新建两个logstash的配置文件如下:

  1. loki/config/local-config.yaml
yml 复制代码
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9095

ingester:
  lifecycler:
    address: 127.0.0.1
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
    final_sleep: 0s
  chunk_idle_period: 5m
  chunk_retain_period: 30s
  max_transfer_retries: 0

schema_config:
  configs:
    - from: 2020-10-24
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  boltdb_shipper:
    active_index_directory: /loki/index
    cache_location: /loki/boltdb-cache
    cache_ttl: 24h
    shared_store: filesystem
  filesystem:
    directory: /loki/chunks

compactor:
  working_directory: /loki/compactor
  shared_store: filesystem
  compaction_interval: 1h

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h

chunk_store_config:
  max_look_back_period: 0s

table_manager:
  retention_deletes_enabled: false
  retention_period: 0s
  1. promtail/config/promtail-config.yaml
yml 复制代码
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: varlogs
          __path__: /var/log/*log

启动成功后打开http://localhost:3000 就可以进到Grafana的可视化界面:

登录时的账号密码就是docker-compose.yml的配置:

  • GF_SECURITY_ADMIN_USER=myadminuser
  • GF_SECURITY_ADMIN_PASSWORD=myadminpassword

其实一般收集数据来说,都是服务端先把日志写在本地或者容器的一个log文件里,然后通过日志采集工具,这里是promtail,去上报到日志引擎,这里是loki。

我这里为了方便,就直接使用loki的api来推送数据了:

然后打开grafana看所推送的数据:

同样的,也可以通过api来查询loki中的数据:

最后,再完整的实现一下nest到loki的接口:

写接口:

ts 复制代码
  async handleLog(query: any) {
    let decodeQuery = decodeURIComponent(query);
    const data: { type: string; value: any } = JSON.parse(decodeQuery);
    const { type, value } = data;
    const params = {
      streams: [
        {
          stream: {
            type,
          },
          values: [[`${Date.now() * 1000000}`, value + '']],
        },
      ],
    };
    const res = await axios.post(
      'http://localhost:3100/loki/api/v1/push',
      params,
      {
        headers: {
          'Content-Type': 'application/json',
        },
      },
    );
  }

读接口:

ts 复制代码
  async getLog(query: string, start: string, end: string) {
    const res = await axios.get(
      `http://localhost:3100/loki/api/v1/query_range?query=${query}&start=${start}&end=${end}`,
    );
    return res.data.data.result;
  }

数据可视化

拿到数据之后,做可视化其实也就比较简单了,横轴是时间,纵轴是具体的指标值,具体你也可以拓展一些99线,95线等值。

这里具体需要注意的是,纳秒值需要转换一下:

js 复制代码
const date = new Date(parseInt(item[0], 10) / 1000000);
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;

具体的实现可以使用echarts或者其他库去实现,相对来说不是太难的点,这里就不做展开。

最后

埋点也好,日志平台也好,要做完善不是一件简单的事情。希望这篇文章能帮助你对日志平台有一个较为全面的认识,细节之处未能尽全敬请谅解。

如果有哪些地方写的不对,也恳请指教。

最后,如果觉得对你有帮助的话,点点关注点点赞吧~

相关推荐
万物得其道者成6 分钟前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
小白小白从不日白7 分钟前
react hooks--useReducer
前端·javascript·react.js
下雪天的夏风19 分钟前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
danplus21 分钟前
node发送邮件:如何实现Node.js发信功能?
服务器·node.js·外贸开发信·邮件群发·蜂邮edm邮件营销·邮件接口·营销邮件
青稞儿25 分钟前
面试题高频之token无感刷新(vue3+node.js)
vue.js·node.js
diygwcom31 分钟前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan34 分钟前
electron react离线使用monaco-editor
javascript·react.js·electron
^^为欢几何^^43 分钟前
lodash中_.difference如何过滤数组
javascript·数据结构·算法
Hello-Mr.Wang1 小时前
vue3中开发引导页的方法
开发语言·前端·javascript
程序员凡尘1 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js