基于Vue3+Node.js實現(xiàn)實時可視化監(jiān)控系統(tǒng)
前言
在日常運維和開發(fā)工作中,服務(wù)器監(jiān)控是必不可少的環(huán)節(jié)。市面上有不少優(yōu)秀的監(jiān)控方案(如 Prometheus、Grafana、Zabbix 等),但對于中小型團隊或個人開發(fā)者來說,這些工具往往過于復(fù)雜,學(xué)習(xí)成本較高。
本文將介紹我自己開發(fā)的 ServWatch 監(jiān)控系統(tǒng)——一個輕量級、易部署、界面美觀的實時監(jiān)控解決方案。
一、系統(tǒng)架構(gòu)
1.1 整體架構(gòu)圖
┌─────────────────────────────────────────────────────────────────┐ │ ServWatch 系統(tǒng)架構(gòu) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ │ │ │ │ │ │ Browser │?────?│ Frontend │?────?│ Backend │ │ │ │ │ WS │ Vue 3 + │ HTTP │ Node.js │ │ │ │ │ │ ECharts │ │ Express │ │ │ └──────────────┘ └──────────────┘ └──────┬───────┘ │ │ │ │ │ ┌───────▼───────┐ │ │ ┌──────────────┐ ┌──────────────┐ │ Postgres │ │ │ │ │ │ │ │ TimescaleDB │ │ │ │ Agent │─────?│ Redis │─────?│ │ │ │ │ 采集器 │ HTTP │ 緩存 │ │ 時序數(shù)據(jù) │ │ │ │ │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘
1.2 技術(shù)棧選型
| 層級 | 技術(shù)選型 | 理由 |
|---|---|---|
| 前端框架 | Vue 3 | Composition API 開發(fā)體驗好 |
| 構(gòu)建工具 | Vite | 開發(fā)速度快,HMR 體驗優(yōu)秀 |
| 狀態(tài)管理 | Pinia | Vue 3 官方推薦,API 簡潔 |
| UI 組件 | Element Plus | 組件豐富,文檔完善 |
| 圖表庫 | Apache ECharts | 圖表功能強大,交互性好 |
| 后端框架 | Express | 輕量靈活,中間件豐富 |
| 實時通信 | Socket.IO | WebSocket 封裝,兼容性好 |
| 數(shù)據(jù)庫 | PostgreSQL + TimescaleDB | 關(guān)系型 + 時序數(shù)據(jù)擴展 |
| 緩存 | Redis | 高性能 KV 存儲 |
二、核心功能實現(xiàn)
2.1 實時監(jiān)控儀表板

儀表板是監(jiān)控系統(tǒng)的核心界面,展示所有監(jiān)控目標(biāo)的整體狀態(tài)。
WebSocket 實時推送實現(xiàn):
// 前端:建立 WebSocket 連接
import { io } from 'socket.io-client'
const socket = io('http://localhost:3001', {
auth: { token: localStorage.getItem('token') }
})
// 注冊為儀表板客戶端
socket.emit('dashboard:connect')
// 訂閱實時指標(biāo)
socket.emit('metrics:subscribe', { targetIds: ['1', '2', '3'] })
// 接收實時更新
socket.on('metrics:update', (data) => {
console.log('實時指標(biāo):', data)
// 更新圖表數(shù)據(jù)
})
// 后端:WebSocket 服務(wù)
class WebSocketService {
constructor(io) {
this.io = io
this.dashboardClients = new Set()
this.setupEventHandlers()
}
setupEventHandlers() {
this.io.on('connection', (socket) => {
socket.on('dashboard:connect', () => {
this.dashboardClients.add(socket.id)
socket.emit('dashboard:connected', { clientId: socket.id })
})
socket.on('metrics:subscribe', ({ targetIds }) => {
socket.join(`metrics:${targetIds.join(',')}`)
})
socket.on('disconnect', () => {
this.dashboardClients.delete(socket.id)
})
})
}
// 推送實時指標(biāo)到所有儀表板客戶端
broadcastMetrics(targetId, metrics) {
this.io.emit('metrics:update', {
targetId,
metrics,
timestamp: new Date().toISOString()
})
}
// 推送告警通知
broadcastAlert(alert, value) {
this.io.emit('alert:triggered', {
alert,
value,
timestamp: new Date().toISOString()
})
}
}
2.2 監(jiān)控數(shù)據(jù)可視化

使用 ECharts 實現(xiàn)實時折線圖:
<template>
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref(null)
let chart = null
const initChart = () => {
chart = echarts.init(chartRef.value)
chart.setOption({
title: { text: 'CPU 使用率' },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value',
max: 100,
axisLabel: { formatter: '{value}%' }
},
series: [{
name: 'CPU',
type: 'line',
smooth: true,
data: [],
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.5)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
])
}
}]
})
}
// 實時更新圖表數(shù)據(jù)
const updateChart = (timestamp, value) => {
const option = chart.getOption()
option.xAxis[0].data.push(timestamp)
option.series[0].data.push(value)
// 保持最近 60 個數(shù)據(jù)點
if (option.xAxis[0].data.length > 60) {
option.xAxis[0].data.shift()
option.series[0].data.shift()
}
chart.setOption(option)
}
onMounted(() => {
initChart()
// 監(jiān)聽 WebSocket 數(shù)據(jù)更新
socket.on('metrics:update', ({ metrics }) => {
updateChart(new Date().toLocaleTimeString(), metrics.cpu)
})
})
</script>
2.3 告警規(guī)則引擎

告警評估服務(wù)實現(xiàn):
class AlertService {
constructor() {
this.alertRules = new Map()
this.alertHistory = []
this.cooldowns = new Map()
}
// 添加告警規(guī)則
addRule(rule) {
this.alertRules.set(rule.id, rule)
}
// 評估指標(biāo)是否觸發(fā)告警
async evaluate(targetId, metricType, value) {
const rules = Array.from(this.alertRules.values())
.filter(r => r.targetId === targetId && r.metricType === metricType && r.enabled)
for (const rule of rules) {
const shouldAlert = this.checkCondition(value, rule)
if (shouldAlert && !this.isInCooldown(rule.id)) {
await this.triggerAlert(rule, value)
this.setCooldown(rule.id, rule.cooldown || 300)
}
}
}
// 檢查條件
checkCondition(value, rule) {
switch (rule.condition) {
case 'greater_than':
return value > rule.threshold
case 'less_than':
return value < rule.threshold
case 'equals':
return value === rule.threshold
default:
return false
}
}
// 觸發(fā)告警
async triggerAlert(rule, value) {
const alertHistory = {
id: generateId(),
alertId: rule.id,
alertName: rule.name,
severity: rule.severity,
status: 'active',
message: `${rule.name}觸發(fā): 當(dāng)前值 ${value}, 閾值 ${rule.threshold}`,
value,
threshold: rule.threshold,
createdAt: new Date().toISOString()
}
this.alertHistory.push(alertHistory)
rule.triggerCount++
// 通知 WebSocket 客戶端
websocketService.broadcastAlert(rule, value)
// TODO: 發(fā)送郵件/釘釘通知
}
isInCooldown(ruleId) {
const cooldownEnd = this.cooldowns.get(ruleId)
return cooldownEnd && Date.now() < cooldownEnd
}
setCooldown(ruleId, seconds) {
this.cooldowns.set(ruleId, Date.now() + seconds * 1000)
}
}
2.4 指標(biāo)采集 Agent
Agent 負責(zé)在被監(jiān)控服務(wù)器上采集系統(tǒng)指標(biāo):
const os = require('os')
const axios = require('axios')
class SystemCollector {
constructor(config) {
this.interval = config.collectInterval || 1000
}
// 采集 CPU 指標(biāo)
collectCPU() {
const cpus = os.cpus()
const totalIdle = cpus.reduce((acc, cpu) => acc + cpu.times.idle, 0)
const totalTick = cpus.reduce((acc, cpu) => {
return acc + Object.values(cpu.times).reduce((a, b) => a + b, 0)
}, 0)
return {
usage: ((1 - totalIdle / totalTick) * 100).toFixed(2),
cores: cpus.map(cpu => ({
usage: ((1 - cpu.times.idle / Object.values(cpu.times).reduce((a, b) => a + b, 0)) * 100).toFixed(2)
}))
}
}
// 采集內(nèi)存指標(biāo)
collectMemory() {
const total = os.totalmem()
const free = os.freemem()
const used = total - free
return {
total: (total / 1024 / 1024 / 1024).toFixed(2), // GB
used: (used / 1024 / 1024 / 1024).toFixed(2),
free: (free / 1024 / 1024 / 1024).toFixed(2),
usage: ((used / total) * 100).toFixed(2)
}
}
// 采集網(wǎng)絡(luò)指標(biāo)
async collectNetwork() {
const stats = await getNetworkStats()
return {
rx: stats.rx, // bytes/s
tx: stats.tx
}
}
// 采集所有指標(biāo)
async collectAll() {
return {
cpu: this.collectCPU(),
memory: this.collectMemory(),
network: await this.collectNetwork(),
disk: await this.collectDisk(),
timestamp: new Date().toISOString()
}
}
}
// 定時采集并發(fā)送
const collector = new SystemCollector(config)
const transmitter = new HttpTransmitter(config.serverUrl)
setInterval(async () => {
const metrics = await collector.collectAll()
await transmitter.send(metrics)
}, collector.interval)
三、API 設(shè)計
3.1 RESTful API
| 分類 | 端點 | 方法 | 說明 |
|---|---|---|---|
| 認(rèn)證 | /auth/login | POST | 用戶登錄 |
| 認(rèn)證 | /auth/register | POST | 用戶注冊 |
| 認(rèn)證 | /auth/refresh | POST | 刷新 Token |
| 目標(biāo) | /targets | GET | 獲取所有監(jiān)控目標(biāo) |
| 目標(biāo) | /targets | POST | 創(chuàng)建監(jiān)控目標(biāo) |
| 目標(biāo) | /targets/:id | PUT | 更新監(jiān)控目標(biāo) |
| 告警 | /alerts | GET | 獲取告警規(guī)則 |
| 告警 | /alerts | POST | 創(chuàng)建告警規(guī)則 |
| 指標(biāo) | /metrics/realtime | GET | 獲取實時指標(biāo) |
| 指標(biāo) | /metrics/aggregated | GET | 獲取聚合指標(biāo) |
| 統(tǒng)計 | /stats/overview | GET | 系統(tǒng)概覽統(tǒng)計 |
3.2 認(rèn)證機制
使用 JWT Bearer Token 認(rèn)證:
// 登錄獲取 Token
POST /auth/login
{
"identifier": "admin",
"password": "admin123"
}
// 響應(yīng)
{
"user": { "id": "1", "username": "admin", ... },
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
// 使用 Token 訪問 API
GET /targets
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
四、快速部署
4.1 模擬模式(最快體驗)
無需后端,直接體驗前端界面:
cd frontend npm install npm run dev -- --port 5175 # 訪問 http://localhost:5175 # 登錄賬號: admin / admin123
4.2 Docker Compose 部署
一鍵啟動所有服務(wù):
docker-compose -f docker/docker-compose.yml up -d # 服務(wù)列表: # - Frontend: http://localhost:5173 # - Backend: http://localhost:3001 # - PostgreSQL: localhost:5432 # - Redis: localhost:6379
4.3 本地開發(fā)部署
# 1. 啟動數(shù)據(jù)庫 docker run -d --name servwatch-postgres \ -e POSTGRES_DB=servwatch \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -p 5432:5432 \ timescale/timescaledb:latest-pg16 # 2. 啟動后端 cd backend npm install npm run dev # 3. 啟動前端 cd frontend cp .env.production .env npm install npm run dev -- --port 5175 # 4. (可選) 啟動 Agent cd agent npm install npm start
五、項目結(jié)構(gòu)
ServWatch/
├── backend/ # Node.js 后端服務(wù)
│ ├── src/
│ │ ├── config/ # 配置管理
│ │ ├── controllers/ # 請求處理器
│ │ ├── services/ # 業(yè)務(wù)邏輯
│ │ │ ├── websocketService.js # WebSocket連接管理
│ │ │ ├── alertService.js # 告警評估通知
│ │ │ └── metricsService.js # 指標(biāo)聚合處理
│ │ ├── models/ # 數(shù)據(jù)模型
│ │ ├── routes/ # API路由
│ │ ├── websocket/ # WebSocket處理器
│ │ └── app.js
│ └── package.json
│
├── agent/ # 指標(biāo)采集代理
│ ├── src/
│ │ ├── collectors/ # 采集器
│ │ │ ├── systemCollector.js # 系統(tǒng)指標(biāo)
│ │ │ ├── appCollector.js # 應(yīng)用指標(biāo)
│ │ │ └── apiCollector.js # API性能
│ │ ├── transmitters/ # 數(shù)據(jù)傳輸
│ │ └── agent.js
│ └── package.json
│
├── frontend/ # Vue.js 前端
│ ├── src/
│ │ ├── components/ # Vue組件
│ │ ├── composables/ # 組合式函數(shù)
│ │ ├── stores/ # Pinia狀態(tài)管理
│ │ ├── services/ # API服務(wù)
│ │ └── views/ # 頁面視圖
│ └── package.json
│
└── docker/ # Docker 配置
└── docker-compose.yml
六、界面預(yù)覽
6.1 登錄頁面

6.2 GPU 監(jiān)控

6.3 監(jiān)控目標(biāo)管理

七、后續(xù)規(guī)劃
- 基礎(chǔ)框架搭建
- 系統(tǒng)指標(biāo)采集
- 應(yīng)用性能監(jiān)控
- 告警系統(tǒng)
- WebSocket 實時通信
- Docker 容器化
- 郵件/釘釘通知功能
- 自定義儀表板
- 數(shù)據(jù)導(dǎo)出報表
- 多租戶支持
- 移動端適配
八、總結(jié)
ServWatch 是一個功能完整、易于部署的監(jiān)控系統(tǒng)。相比 Prometheus+Grafana 組合,它更加輕量,學(xué)習(xí)成本更低,適合中小型團隊和個人開發(fā)者使用。
以上就是基于Vue3+Node.js實現(xiàn)實時可視化監(jiān)控系統(tǒng)的詳細內(nèi)容,更多關(guān)于Vue3 Node.js實時可視化監(jiān)控的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
利用Vue的v-for和v-bind實現(xiàn)列表顏色切換
這篇文章主要介紹了利用Vue的v-for和v-bind實現(xiàn)列表顏色切換,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07
Vue3動態(tài)使用KeepAlive組件的實現(xiàn)步驟
在 Vue 3 項目中,我們有時需要根據(jù)路由的 meta 信息來動態(tài)決定是否使用 KeepAlive 組件,以控制組件的緩存行為,所以本文給大家介紹了Vue3動態(tài)使用KeepAlive組件的實現(xiàn)步驟,通過代碼示例講解的非常詳細,需要的朋友可以參考下2024-11-11
Vue3監(jiān)聽reactive對象中屬性變化的方法
在 Vue 3 中,如果你想監(jiān)聽 reactive 對象中的某個屬性發(fā)生的變化,你可以使用 watch 函數(shù)進行監(jiān)聽,watch 函數(shù)允許你觀察 reactive 對象的某個屬性或者整個對象,所以本文給大家介紹了Vue3監(jiān)聽reactive對象中屬性變化的方法,需要的朋友可以參考下2024-08-08
Vue 第三方字體圖標(biāo)引入 Font Awesome的方法
今天小編就為大家分享一篇Vue 第三方字體圖標(biāo)引入 Font Awesome的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09
VUE 實現(xiàn)復(fù)制內(nèi)容到剪貼板的兩種方法
這篇文章主要介紹了VUE 實現(xiàn)復(fù)制內(nèi)容到剪貼板功能,本文通過兩種方法,給大家介紹的非常詳細,具有一定的參考借鑒價值 ,需要的朋友可以參考下2019-04-04

