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-.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, ":", "_") }