Files
membank/auto-check/pkg/stress/chart.go

296 lines
8.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package stress
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// metricSpec 单条曲线规格
type metricSpec struct {
Key string `json:"key"`
Label string `json:"label"`
Color string `json:"color"`
Unit string `json:"unit"`
}
// chartSpec 图表规格(左轴 / 右轴)
type chartSpec struct {
Left []metricSpec
Right []metricSpec
LeftPct bool
LeftTitle string
RightTitle string
}
// kvPair 指标明细
type kvPair struct {
Label string `json:"label"`
Value string `json:"value"`
}
// metricSeries 单条曲线序列
type metricSeries struct {
Label string `json:"label"`
Color string `json:"color"`
Data []float64 `json:"data"`
Unit string `json:"unit"`
}
// chartPayload 图表数据
type chartPayload struct {
Labels []string `json:"labels"`
LeftSets []metricSeries `json:"left_sets"`
RightSets []metricSeries `json:"right_sets"`
LeftTitle string `json:"left_title"`
RightTitle string `json:"right_title"`
LeftPct bool `json:"left_pct"`
}
// testCard 单个测试卡片数据
type testCard struct {
Name string `json:"name"`
Label string `json:"label"`
Status string `json:"status"`
Duration string `json:"duration"`
Score float64 `json:"score"`
ScoreUnit string `json:"score_unit"`
ScoreHint string `json:"score_hint"`
Metrics []kvPair `json:"metrics"`
Benchmarks []BenchItem `json:"benchmarks,omitempty"` // 性能/温度基准评级
OverallLevel Level `json:"overall_level,omitempty"` // 综合评级(各项最差)
OverallText string `json:"overall_text,omitempty"`
OverallColor string `json:"overall_color,omitempty"`
Output string `json:"output"`
Chart *chartPayload `json:"chart,omitempty"`
}
// reportHTMLData 报告整体数据
type reportHTMLData struct {
Title string `json:"title"`
IP string `json:"ip"`
Time string `json:"time"`
Duration string `json:"duration"`
Overall string `json:"overall"`
Summary string `json:"summary"`
SysInfo map[string]string `json:"sys_info"`
Cards []testCard `json:"cards"`
}
// WriteReportHTML 生成综合 HTML 报告(跑分卡片 + 多曲线图),返回文件路径
// 默认文件名report-<ip>.html
func WriteReportHTML(ip string, report *Report, reportDir string) (string, error) {
filename := fmt.Sprintf("report-%s.html", SanitizeFilename(ip))
return WriteReportHTMLTo(ip, report, reportDir, filename)
}
// WriteReportHTMLTo 生成综合 HTML 报告到指定文件名,返回完整文件路径
func WriteReportHTMLTo(ip string, report *Report, reportDir, filename string) (string, error) {
data := buildReportData(ip, report)
dataJSON, err := json.Marshal(data)
if err != nil {
return "", err
}
html := strings.Replace(reportHTMLTemplate, "__DATA__", string(dataJSON), 1)
if err := os.MkdirAll(reportDir, 0755); err != nil {
return "", err
}
path := filepath.Join(reportDir, filename)
if err := os.WriteFile(path, []byte(html), 0644); err != nil {
return "", err
}
return path, nil
}
func buildReportData(ip string, report *Report) reportHTMLData {
data := reportHTMLData{
Title: fmt.Sprintf("%s · 硬件压力测试报告", ip),
IP: ip,
Time: report.StartTime.Format("2006-01-02 15:04:05"),
Duration: report.Duration.Round(time.Millisecond).String(),
Overall: Status(report),
Summary: report.Summary(),
SysInfo: report.SysInfo,
}
// 硬件型号:从 sysInfo["cpu"] 读取,用于按硬件库匹配基准
cpuModel := ""
if report.SysInfo != nil {
cpuModel = report.SysInfo["cpu"]
}
for _, res := range report.Results {
data.Cards = append(data.Cards, buildCard(cpuModel, res))
}
return data
}
func buildCard(cpuModel string, res Result) testCard {
card := testCard{
Name: string(res.Type),
Label: testLabel(res.Type),
Status: res.Status,
Duration: res.Duration.Round(time.Millisecond).String(),
Score: res.Score,
ScoreUnit: ScoreUnit(res.Type),
ScoreHint: scoreHint(res.Type),
Output: res.Output,
}
for _, name := range sortedMetricNames(res.Metrics) {
card.Metrics = append(card.Metrics, kvPair{
Label: MetricLabel(name),
Value: fmt.Sprintf("%.1f%s", res.Metrics[name], MetricUnit(name)),
})
}
// 性能/温度基准评级(仅在有数据时填充;按硬件库匹配专属阈值)
if items := EvaluateResult(res, cpuModel); len(items) > 0 {
card.Benchmarks = items
if lvl := OverallLevel(items); lvl != "" {
d := LevelMeta(lvl)
card.OverallLevel = lvl
card.OverallText = d.Text
card.OverallColor = d.Color
}
}
if len(res.Samples) > 0 {
if payload, ok := buildChart(res); ok {
card.Chart = &payload
}
}
return card
}
func buildChart(res Result) (chartPayload, bool) {
spec := chartSpecFor(res.Type)
if len(spec.Left) == 0 && len(spec.Right) == 0 {
return chartPayload{}, false
}
payload := chartPayload{
LeftTitle: spec.LeftTitle,
RightTitle: spec.RightTitle,
LeftPct: spec.LeftPct,
}
for _, s := range res.Samples {
payload.Labels = append(payload.Labels, s.Time)
}
hasData := func(key string) bool {
for _, s := range res.Samples {
if s.HasMetric(key) {
return true
}
}
return false
}
buildSeries := func(specs []metricSpec) []metricSeries {
var out []metricSeries
for _, m := range specs {
if !hasData(m.Key) {
continue
}
series := metricSeries{Label: m.Label, Color: m.Color, Unit: m.Unit}
for _, s := range res.Samples {
series.Data = append(series.Data, s.Values[m.Key])
}
out = append(out, series)
}
return out
}
payload.LeftSets = buildSeries(spec.Left)
payload.RightSets = buildSeries(spec.Right)
return payload, true
}
// testLabel 测试项中文名
func testLabel(t TestType) string {
switch t {
case TestCPU:
return "CPU 压力测试"
case TestMemory:
return "内存压力测试"
case TestDiskIO:
return "磁盘 IO 测试"
case TestMemNative:
return "内存精压测试"
case TestFull:
return "综合工况测试"
case MonitorTemp:
return "温度监控"
case MonitorDmesg:
return "内核日志检查"
default:
return string(t)
}
}
// scoreHint 跑分含义说明
func scoreHint(t TestType) string {
switch t {
case TestCPU:
return "每秒操作数bogo ops/s"
case TestMemory:
return "每秒操作数bogo ops/s"
case TestDiskIO:
return "读写带宽合计MB/s"
case TestFull:
return "综合每秒操作数bogo ops/s"
case TestMemNative:
return "内存带宽MB/s"
default:
return ""
}
}
// chartSpecFor 返回某类测试的曲线规格
func chartSpecFor(t TestType) chartSpec {
switch t {
case TestCPU:
return chartSpec{
Left: []metricSpec{{Key: "cpu", Label: "CPU 利用率", Color: "#f97316", Unit: "%"}},
Right: []metricSpec{{Key: "temp", Label: "温度", Color: "#ef4444", Unit: "°C"}},
LeftPct: true,
LeftTitle: "利用率 (%)",
RightTitle: "温度 (°C)",
}
case TestMemory:
return chartSpec{
Left: []metricSpec{{Key: "mem_used_pct", Label: "内存使用率", Color: "#8b5cf6", Unit: "%"}},
Right: []metricSpec{{Key: "temp", Label: "温度", Color: "#ef4444", Unit: "°C"}},
LeftPct: true,
LeftTitle: "使用率 (%)",
RightTitle: "温度 (°C)",
}
case TestDiskIO:
return chartSpec{
Left: []metricSpec{{Key: "iops_total", Label: "IOPS", Color: "#22c55e", Unit: ""}},
Right: []metricSpec{{Key: "bw_total", Label: "吞吐", Color: "#3b82f6", Unit: "MB/s"}},
LeftPct: false,
LeftTitle: "IOPS",
RightTitle: "吞吐 (MB/s)",
}
case TestFull:
return chartSpec{
Left: []metricSpec{
{Key: "cpu", Label: "CPU 利用率", Color: "#f97316", Unit: "%"},
{Key: "mem_used_pct", Label: "内存使用率", Color: "#8b5cf6", Unit: "%"},
},
Right: []metricSpec{{Key: "temp", Label: "温度", Color: "#ef4444", Unit: "°C"}},
LeftPct: true,
LeftTitle: "利用率 (%)",
RightTitle: "温度 (°C)",
}
default:
return chartSpec{}
}
}
// SanitizeFilename 清理 IP 中的特殊字符,用于文件名
func SanitizeFilename(ip string) string {
return strings.ReplaceAll(ip, ":", "_")
}