269 lines
7.1 KiB
Go
269 lines
7.1 KiB
Go
package stress
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"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"`
|
||
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 报告(跑分卡片 + 多曲线图),返回文件路径
|
||
func WriteReportHTML(ip string, report *Report, reportDir 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
|
||
}
|
||
filename := fmt.Sprintf("%s/report-%s.html", reportDir, SanitizeFilename(ip))
|
||
if err := os.WriteFile(filename, []byte(html), 0644); err != nil {
|
||
return "", err
|
||
}
|
||
return filename, 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,
|
||
}
|
||
for _, res := range report.Results {
|
||
data.Cards = append(data.Cards, buildCard(res))
|
||
}
|
||
return data
|
||
}
|
||
|
||
func buildCard(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 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, ":", "_")
|
||
}
|