第7讲:数据可视化与仪表板——让数据会说话

一、概述

本讲将前三讲的日志、追踪、指标数据,以及第4-6讲的LLM/Agent监控和告警,整合为直观的仪表板。我们将实现一个轻量级实时仪表板系统。


二、架构

复制代码
前端 (Chart.js + WebSocket) ←→ Go 后端 (数据聚合 + 推送) ←→ 数据存储

三、核心数据结构

复制代码
// DashboardPanel 仪表板面板
type DashboardPanel struct {
	ID      string            `json:"id"`
	Title   string            `json:"title"`
	Type    string            `json:"type"` // timeseries, stat, table
	Width   int               `json:"width"`  // 1-12 网格列
	Height  int               `json:"height"`
	Series  []DashboardSeries `json:"series"`
	Options map[string]interface{} `json:"options,omitempty"`
}

type DashboardSeries struct {
	Name  string `json:"name"`
	Color string `json:"color"`
	Unit  string `json:"unit"`
	Type  string `json:"type"` // line, bar, area
}

// Dashboard 仪表板
type Dashboard struct {
	ID       string            `json:"id"`
	Title    string            `json:"title"`
	Category string            `json:"category"` // llm, agent, cost, alert
	Panels   []DashboardPanel  `json:"panels"`
	Refresh  time.Duration     `json:"refresh"`
}

四、预置仪表板

仪表板 用途 刷新频率 面板数
LLM 总览 Token 速率、延迟、错误率、统计 10s 4
Agent 监控 步数、时长、工具调用 15s 3
成本分析 会话成本、日累计、构成 30s 3
告警中心 活跃告警、趋势、MTTR 5s 3

五、后端关键代码

复制代码
// 数据存储
type DataStore struct {
	mu        sync.RWMutex
	metrics   map[string][]DataPoint
	maxPoints int // 默认 3600
}

// WebSocket 实时推送
type WSHub struct {
	clients    map[*WSClient]bool
	broadcast  chan []byte
}

// 每 5 秒广播一次指标数据
func (h *WSHub) PushMetrics(store *DataStore) {
	ticker := time.NewTicker(5 * time.Second)
	for range ticker.C {
		data, _ := json.Marshal(map[string]interface{}{
			"type":    "metrics_update",
			"metrics": store.GetRecent(5 * time.Minute),
			"time":    time.Now(),
		})
		h.broadcast <- data
	}
}

// 模拟数据生成(昼夜模式)
func (dg *DataGenerator) generateMetrics() {
	hour := time.Now().Hour()
	baseLoad := 30000.0
	if hour >= 8 && hour <= 22 {
		baseLoad = 70000.0
	}
	
	// Token 速率
	dg.store.AddMetric("llm_token_rate", baseLoad + rand.Float64()*20000 - 10000)
	// 延迟 P50/P95/P99
	dg.store.AddMetric("latency_p50", 150 + rand.Float64()*100)
	dg.store.AddMetric("latency_p95", 350 + rand.Float64()*200)
	dg.store.AddMetric("latency_p99", 600 + rand.Float64()*400)
	// 错误率(5% 概率飙升)
	errRate := 0.02 + rand.Float64()*0.08
	if rand.Float64() < 0.05 { errRate = 0.15 + rand.Float64()*0.25 }
	dg.store.AddMetric("llm_error_rate", errRate)
	// Agent 指标
	dg.store.AddMetric("agent_steps_per_session", float64(5 + rand.Intn(15)))
	dg.store.AddMetric("agent_session_duration_ms", 1200 + rand.Float64()*1800)
	dg.store.AddMetric("tool_calls_per_session", float64(3 + rand.Intn(10)))
	// 成本
	dg.store.AddMetric("cost_per_session_usd", 0.005 + rand.Float64()*0.025)
	dg.store.AddMetric("daily_cost_usd", 50.0 + rand.Float64()*30)
}

六、API 端点

路径 方法 说明
/api/dashboards?category= GET 获取仪表板列表
/api/dashboard?id= GET 获取单个仪表板
/api/metrics?name=&duration= GET 获取指标数据
/ws WebSocket 实时数据推送
/ GET 前端页面

七、前端核心逻辑

复制代码
// WebSocket 连接
function connectWebSocket() {
    ws = new WebSocket(`ws://${host}/ws`);
    ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        if (data.type === 'metrics_update') updateCharts(data.metrics);
    };
}

// 初始化 Chart.js 图表
function initChart(panelId, seriesNames) {
    charts[panelId] = new Chart(ctx, {
        type: 'line',
        data: {
            labels: [],
            datasets: seriesNames.map((name, i) => ({
                label: name,
                data: [],
                borderColor: COLORS[i],
                tension: 0.4,
                pointRadius: 2
            }))
        },
        options: {
            responsive: true,
            animation: { duration: 300 },
            scales: {
                x: { type: 'time', time: { unit: 'second' }, grid: { color: '#2a3a4a' } },
                y: { beginAtZero: true, grid: { color: '#2a3a4a' } }
            }
        }
    });
}

// 每 5 秒更新图表
function updateCharts(metrics) {
    Object.keys(charts).forEach(panelId => {
        const chart = charts[panelId];
        chart.data.labels.push(new Date().toLocaleTimeString());
        if (chart.data.labels.length > 60) chart.data.labels.shift();
        
        // 更新每个序列的最新值
        panel.series.forEach((name, i) => {
            const points = metrics[name] || [];
            chart.data.datasets[i].data.push(points[points.length - 1]?.value || 0);
            if (chart.data.datasets[i].data.length > 60) chart.data.datasets[i].data.shift();
        });
        chart.update('none');
    });
}

八、仪表板设计原则

原则 说明 反例
黄金信号 展示 Rate/Errors/Duration 展示 CPU 温度
层次化 总览 → 钻取详情 一张图显示所有
可操作 看到问题能直接定位 只有数字没有上下文
简洁 每张图 ≤ 5 条线 彩虹色堆叠
一致性 红色=错误,绿色=正常 颜色随意使用

九、启动方式

复制代码
go run main.go
# 访问 http://localhost:8080

十、下讲预告

第8讲:Trace 与 Span 深度分析------链路数据的价值挖掘

我们将深入分析分布式追踪数据,学习如何从 Trace 中提取业务洞察,包括 Trace 采样策略、Span 标签分析、瓶颈识别、依赖关系图谱等。


推荐工具站:zz365.top - 在线 JSON 格式化、正则测试、时间戳转换等开发者常用工具

相关推荐
云烟成雨TD1 小时前
LlamaIndex 系列【32】检索增强策略:自问自答(Self Ask)
ai·agent·rag·llamaindex
slacker-kian2 小时前
[实践]-让 SAP 工程 Skill 脱离 opencode跑在自定义Agent 上
ai·llm·sap·agent·abap·adt·opencode
云烟成雨TD2 小时前
LlamaIndex 系列【35】节点后处理器(Node Postprocessor)
ai·agent·rag·llamaindex
Chukai1232 小时前
AI智能体:会思考会干活的下一代AI
人工智能·agent
roamingcode3 小时前
DeepSeek Harness 记忆召回插件的“PRD”设计拆解
agent·memory·deepseek·harness·dsh·dsh-plugin
leoZ2313 小时前
第 16 篇 转型路线图与求职实战
人工智能·大模型·agent
0566464 小时前
用大模型构建答疑机器人:从 API 调用到上下文工程
python·学习·agent
武子康5 小时前
CLAUDE.md 越写越长,哪些规则该放到子目录?
人工智能·llm·agent