优化温度采集和渲染
This commit is contained in:
1 parent
307d8e4913
commit
6b3fd9e730
4 files changed
+225
-128
No files matched your search
@@ -194,8 +194,7 @@ type GPUData struct {
|
||||
type TemperatureData struct {
|
||||
SensorKey string `json:"sensorKey"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
High float64 `json:"high,omitempty"`
|
||||
Critical float64 `json:"critical,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// CommandRequest 指令请求
|
||||
|
||||
@@ -358,10 +358,16 @@ func (s *MetricService) HandleMetricData(ctx context.Context, agentID string, me
|
||||
// 保存每个温度传感器的数据
|
||||
var tempMetrics []models.TemperatureMetric
|
||||
for _, tempData := range tempDataList {
|
||||
// 使用 Type 作为 SensorLabel,更易读
|
||||
sensorLabel := tempData.Type
|
||||
if sensorLabel == "" {
|
||||
sensorLabel = tempData.SensorKey // 降级使用 SensorKey
|
||||
}
|
||||
|
||||
metric := models.TemperatureMetric{
|
||||
AgentID: agentID,
|
||||
SensorKey: tempData.SensorKey,
|
||||
SensorLabel: tempData.SensorKey, // protocol 中没有 SensorLabel,使用 SensorKey
|
||||
SensorLabel: sensorLabel,
|
||||
Temperature: tempData.Temperature,
|
||||
Timestamp: now,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dushixiang/pika/internal/protocol"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
|
||||
// TemperatureCollector 温度监控采集器
|
||||
@@ -19,107 +19,140 @@ func NewTemperatureCollector() *TemperatureCollector {
|
||||
|
||||
// Collect 采集温度数据(某些系统可能不支持)
|
||||
func (t *TemperatureCollector) Collect() ([]*protocol.TemperatureData, error) {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return t.collectLinux()
|
||||
case "darwin":
|
||||
return t.collectDarwin()
|
||||
default:
|
||||
// 不支持的系统,返回空数组
|
||||
return []*protocol.TemperatureData{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// collectLinux 在 Linux 系统上采集温度数据
|
||||
func (t *TemperatureCollector) collectLinux() ([]*protocol.TemperatureData, error) {
|
||||
// 尝试使用 sensors 命令
|
||||
_, err := exec.LookPath("sensors")
|
||||
// 使用 gopsutil 的 sensors 包采集温度数据
|
||||
temps, err := sensors.TemperaturesWithContext(nil)
|
||||
if err != nil {
|
||||
// sensors 命令不可用,返回空数组
|
||||
// 如果获取失败,返回空数组(某些系统可能不支持)
|
||||
return []*protocol.TemperatureData{}, nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("sensors", "-A")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return []*protocol.TemperatureData{}, nil
|
||||
// 按类型聚合温度数据(同类型取最大值)
|
||||
typeMaxTemp := make(map[string]*protocol.TemperatureData)
|
||||
|
||||
for _, temp := range temps {
|
||||
// 跳过温度值为 0 的传感器(可能是未激活或不支持的传感器)
|
||||
if temp.Temperature == 0 {
|
||||
continue
|
||||
}
|
||||
// https://github.com/shirou/gopsutil/issues/1832
|
||||
if runtime.GOOS == "darwin" && !utf8.ValidString(temp.SensorKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 识别传感器类型
|
||||
sensorType := guessSensorType(temp.SensorKey)
|
||||
if sensorType == "OTHER" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 对于同一类型的传感器,取温度最大值
|
||||
if existing, ok := typeMaxTemp[sensorType]; !ok || temp.Temperature > existing.Temperature {
|
||||
typeMaxTemp[sensorType] = &protocol.TemperatureData{
|
||||
SensorKey: temp.SensorKey,
|
||||
Temperature: temp.Temperature,
|
||||
Type: sensorType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将聚合后的数据转换为列表
|
||||
var tempDataList []*protocol.TemperatureData
|
||||
lines := strings.Split(string(output), "\n")
|
||||
currentSensor := ""
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检测传感器名称
|
||||
if !strings.Contains(line, ":") {
|
||||
currentSensor = line
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析温度行
|
||||
if strings.Contains(line, "°C") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
label := strings.TrimSpace(parts[0])
|
||||
valueStr := strings.TrimSpace(parts[1])
|
||||
|
||||
// 提取温度值
|
||||
tempStr := strings.Split(valueStr, "°C")[0]
|
||||
tempStr = strings.TrimSpace(strings.TrimPrefix(tempStr, "+"))
|
||||
|
||||
temp, err := strconv.ParseFloat(tempStr, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sensorKey := currentSensor + "_" + label
|
||||
tempData := &protocol.TemperatureData{
|
||||
SensorKey: sensorKey,
|
||||
Temperature: temp,
|
||||
}
|
||||
|
||||
tempDataList = append(tempDataList, tempData)
|
||||
}
|
||||
for _, tempData := range typeMaxTemp {
|
||||
tempDataList = append(tempDataList, tempData)
|
||||
}
|
||||
|
||||
return tempDataList, nil
|
||||
}
|
||||
|
||||
// collectDarwin 在 macOS 系统上采集温度数据
|
||||
func (t *TemperatureCollector) collectDarwin() ([]*protocol.TemperatureData, error) {
|
||||
// macOS 的温度监控需要特殊工具,如 osx-cpu-temp
|
||||
_, err := exec.LookPath("osx-cpu-temp")
|
||||
if err != nil {
|
||||
// 工具不可用,返回空数组
|
||||
return []*protocol.TemperatureData{}, nil
|
||||
// guessSensorType 根据操作系统和传感器名称推断类型
|
||||
func guessSensorType(key string) string {
|
||||
keyLower := strings.ToLower(key)
|
||||
|
||||
// ---------------- Linux 常见规则 ----------------
|
||||
if runtime.GOOS == "linux" {
|
||||
// CPU 温度传感器
|
||||
if strings.Contains(keyLower, "coretemp") || // Intel Core 系列
|
||||
strings.Contains(keyLower, "k10temp") || // AMD Ryzen/EPYC
|
||||
strings.Contains(keyLower, "zenpower") || // AMD Zen 架构
|
||||
strings.Contains(keyLower, "cpu_thermal") || // 通用 CPU 热传感器
|
||||
strings.Contains(keyLower, "soc_thermal") || // ARM SoC
|
||||
strings.Contains(keyLower, "tctl") || // AMD Tctl (控制温度)
|
||||
strings.Contains(keyLower, "tdie") || // AMD Tdie (芯片温度)
|
||||
strings.Contains(keyLower, "package id") || // Intel 封装温度
|
||||
strings.Contains(keyLower, "core ") { // CPU 核心温度
|
||||
return "CPU"
|
||||
}
|
||||
|
||||
// GPU 温度传感器
|
||||
if strings.Contains(keyLower, "amdgpu") || // AMD GPU
|
||||
strings.Contains(keyLower, "radeon") || // AMD Radeon
|
||||
strings.Contains(keyLower, "nvidia") || // NVIDIA GPU
|
||||
strings.Contains(keyLower, "nouveau") || // NVIDIA 开源驱动
|
||||
strings.Contains(keyLower, "gpu") { // 通用 GPU
|
||||
return "GPU"
|
||||
}
|
||||
|
||||
// 硬盘/SSD 温度传感器
|
||||
if strings.Contains(keyLower, "nvme") || // NVMe SSD
|
||||
strings.Contains(keyLower, "drivetemp") || // 硬盘温度
|
||||
strings.Contains(keyLower, "sata") || // SATA 设备
|
||||
strings.Contains(keyLower, "scsi") || // SCSI 设备
|
||||
strings.Contains(keyLower, "composite") { // NVMe Composite 温度
|
||||
return "DISK"
|
||||
}
|
||||
|
||||
// 主板芯片组温度
|
||||
if strings.Contains(keyLower, "pch") || // Platform Controller Hub
|
||||
strings.Contains(keyLower, "chipset") {
|
||||
return "CHIPSET"
|
||||
}
|
||||
|
||||
// ACPI 温度区域 (通常是主板/系统温度)
|
||||
if strings.Contains(keyLower, "acpitz") || // ACPI Thermal Zone
|
||||
strings.Contains(keyLower, "thermal_zone") {
|
||||
return "SYSTEM"
|
||||
}
|
||||
|
||||
// 电源温度
|
||||
if strings.Contains(keyLower, "psu") ||
|
||||
strings.Contains(keyLower, "power supply") {
|
||||
return "PSU"
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("osx-cpu-temp")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return []*protocol.TemperatureData{}, nil
|
||||
// ---------------- macOS 常见规则 ----------------
|
||||
if runtime.GOOS == "darwin" {
|
||||
// 电池温度传感器
|
||||
if strings.Contains(keyLower, "battery") || strings.Contains(keyLower, "gas gauge") {
|
||||
return "BATTERY"
|
||||
}
|
||||
|
||||
// 存储设备温度传感器
|
||||
if strings.Contains(keyLower, "nand") || strings.Contains(keyLower, "disk") {
|
||||
return "DISK"
|
||||
}
|
||||
|
||||
// GPU 温度传感器
|
||||
if strings.Contains(keyLower, "gpu") {
|
||||
return "GPU"
|
||||
}
|
||||
|
||||
// PMU tdie* 系列是 CPU 核心温度 (die temperature)
|
||||
// PMU tdev* 系列是设备/外围温度 (device temperature)
|
||||
// PMU tcal 是校准温度
|
||||
if strings.HasPrefix(keyLower, "pmu") {
|
||||
if strings.Contains(keyLower, "tdie") {
|
||||
return "CPU"
|
||||
}
|
||||
// tdev 和 tcal 等其他 PMU 传感器归类为 OTHER
|
||||
return "OTHER"
|
||||
}
|
||||
|
||||
// 其他包含 die 的可能是 CPU
|
||||
if strings.Contains(keyLower, "die") || strings.Contains(keyLower, "cpu") {
|
||||
return "CPU"
|
||||
}
|
||||
}
|
||||
|
||||
// 解析输出,格式类似:60.0°C
|
||||
outputStr := strings.TrimSpace(string(output))
|
||||
tempStr := strings.Split(outputStr, "°C")[0]
|
||||
temp, err := strconv.ParseFloat(tempStr, 64)
|
||||
if err != nil {
|
||||
return []*protocol.TemperatureData{}, nil
|
||||
}
|
||||
|
||||
tempData := &protocol.TemperatureData{
|
||||
SensorKey: "CPU",
|
||||
Temperature: temp,
|
||||
}
|
||||
|
||||
return []*protocol.TemperatureData{tempData}, nil
|
||||
return "OTHER"
|
||||
}
|
||||
@@ -512,6 +512,7 @@ const ServerDetail = () => {
|
||||
|
||||
const [timeRange, setTimeRange] = useState<string>('15m');
|
||||
const [selectedInterface, setSelectedInterface] = useState<string>('all');
|
||||
const [selectedTempType, setSelectedTempType] = useState<string>('all');
|
||||
const {agent, latestMetrics, loading} = useAgentOverview(id);
|
||||
const metricsData = useAggregatedMetrics(id, timeRange, selectedInterface);
|
||||
|
||||
@@ -670,9 +671,9 @@ const ServerDetail = () => {
|
||||
}));
|
||||
}, [metricsData.gpu]);
|
||||
|
||||
// Temperature 图表数据(所有传感器的平均温度)
|
||||
// Temperature 图表数据(按类型分组显示各类型的温度)
|
||||
const temperatureChartData = useMemo(() => {
|
||||
const aggregated: Record<string, { time: string; temperature: number; count: number; timestamp: number }> = {};
|
||||
const aggregated: Record<string, any> = {};
|
||||
|
||||
metricsData.temperature.forEach((item) => {
|
||||
const time = new Date(item.timestamp).toLocaleTimeString('zh-CN', {
|
||||
@@ -681,20 +682,58 @@ const ServerDetail = () => {
|
||||
});
|
||||
|
||||
if (!aggregated[time]) {
|
||||
aggregated[time] = {time, temperature: 0, count: 0, timestamp: item.timestamp};
|
||||
aggregated[time] = {time, timestamp: item.timestamp};
|
||||
}
|
||||
|
||||
aggregated[time].temperature += item.maxTemperature;
|
||||
aggregated[time].count += 1;
|
||||
// 使用 sensorLabel 作为数据键(类型名称,如 CPU、GPU 等)
|
||||
const label = item.sensorLabel || 'Unknown';
|
||||
// 对于同一时间点的同一类型,取最大温度
|
||||
if (!aggregated[time][label] || item.maxTemperature > aggregated[time][label]) {
|
||||
aggregated[time][label] = Number(item.maxTemperature.toFixed(2));
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(aggregated).map((item) => ({
|
||||
time: item.time,
|
||||
temperature: Number((item.temperature / item.count).toFixed(2)),
|
||||
timestamp: item.timestamp,
|
||||
}));
|
||||
return Object.values(aggregated);
|
||||
}, [metricsData.temperature]);
|
||||
|
||||
// 提取所有唯一的温度类型(用于图表 Line 渲染和下拉选择器)
|
||||
const temperatureTypes = useMemo(() => {
|
||||
const types = new Set<string>();
|
||||
metricsData.temperature.forEach((item) => {
|
||||
const label = item.sensorLabel || 'Unknown';
|
||||
types.add(label);
|
||||
});
|
||||
return Array.from(types).sort();
|
||||
}, [metricsData.temperature]);
|
||||
|
||||
// 根据选中的类型过滤温度数据
|
||||
const filteredTemperatureTypes = useMemo(() => {
|
||||
if (selectedTempType === 'all') {
|
||||
return temperatureTypes;
|
||||
}
|
||||
return temperatureTypes.filter(type => type === selectedTempType);
|
||||
}, [temperatureTypes, selectedTempType]);
|
||||
|
||||
// 当温度类型列表变化时,如果当前选中的类型不在列表中,重置为 'all'
|
||||
useEffect(() => {
|
||||
if (selectedTempType !== 'all' && temperatureTypes.length > 0) {
|
||||
if (!temperatureTypes.includes(selectedTempType)) {
|
||||
setSelectedTempType('all');
|
||||
}
|
||||
}
|
||||
}, [temperatureTypes, selectedTempType]);
|
||||
|
||||
// 温度类型颜色映射
|
||||
const temperatureColors: Record<string, string> = {
|
||||
'CPU': '#f97316', // 橙色
|
||||
'GPU': '#8b5cf6', // 紫色
|
||||
'DISK': '#06b6d4', // 青色
|
||||
'BATTERY': '#10b981', // 绿色
|
||||
'CHIPSET': '#f59e0b', // 琥珀色
|
||||
'SYSTEM': '#6366f1', // 靛蓝色
|
||||
'PSU': '#ec4899', // 粉色
|
||||
};
|
||||
|
||||
const snapshotCards: SnapshotCardData[] = useMemo(() => {
|
||||
if (!latestMetrics) {
|
||||
return [] as Array<{
|
||||
@@ -1401,23 +1440,33 @@ const ServerDetail = () => {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{temperatureChartData.length > 0 && (
|
||||
{temperatureChartData.length > 0 && temperatureTypes.length > 0 && (
|
||||
<section>
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300">
|
||||
<span
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/40 text-orange-600 dark:text-orange-400">
|
||||
<Thermometer className="h-4 w-4"/>
|
||||
</span>
|
||||
系统温度
|
||||
</h3>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300">
|
||||
<span
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/40 text-orange-600 dark:text-orange-400">
|
||||
<Thermometer className="h-4 w-4"/>
|
||||
</span>
|
||||
系统温度
|
||||
</h3>
|
||||
{temperatureTypes.length > 1 && (
|
||||
<select
|
||||
value={selectedTempType}
|
||||
onChange={(e) => setSelectedTempType(e.target.value)}
|
||||
className="rounded-lg border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 px-3 py-1.5 text-sm text-slate-700 dark:text-slate-200 hover:border-orange-300 dark:hover:border-orange-500 focus:border-orange-500 dark:focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-200 dark:focus:ring-orange-500/40"
|
||||
>
|
||||
<option value="all">所有类型</option>
|
||||
{temperatureTypes.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<AreaChart data={temperatureChartData}>
|
||||
<defs>
|
||||
<linearGradient id="tempAreaGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#f97316" stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor="#f97316" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<LineChart data={temperatureChartData}>
|
||||
<CartesianGrid stroke="currentColor" strokeDasharray="4 4"
|
||||
className="stroke-slate-200 dark:stroke-slate-600"/>
|
||||
<XAxis
|
||||
@@ -1433,16 +1482,26 @@ const ServerDetail = () => {
|
||||
tickFormatter={(value) => `${value}°C`}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip unit="°C"/>}/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="temperature"
|
||||
name="平均温度"
|
||||
stroke="#f97316"
|
||||
strokeWidth={2}
|
||||
fill="url(#tempAreaGradient)"
|
||||
activeDot={{r: 3}}
|
||||
/>
|
||||
</AreaChart>
|
||||
<Legend/>
|
||||
{/* 为选中的温度类型渲染线条 */}
|
||||
{filteredTemperatureTypes.map((type, index) => {
|
||||
// 使用预定义颜色,如果没有则使用默认颜色
|
||||
const color = temperatureColors[type] || `hsl(${(index * 60) % 360}, 70%, 50%)`;
|
||||
return (
|
||||
<Line
|
||||
key={type}
|
||||
type="monotone"
|
||||
dataKey={type}
|
||||
name={type}
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{r: 3}}
|
||||
connectNulls
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
)}
|
||||
@@ -1507,7 +1566,7 @@ const ServerDetail = () => {
|
||||
{latestMetrics?.temperature && latestMetrics.temperature.length > 0 && (
|
||||
<Card title="温度监控" description="系统各部件温度传感器数据">
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{latestMetrics.temperature.map((temp) => (
|
||||
{latestMetrics.temperature.sort((a, b) => a.sensorKey.localeCompare(b.sensorKey)).map((temp) => (
|
||||
<div
|
||||
key={temp.sensorKey}
|
||||
className="rounded-xl border border-slate-100 dark:border-slate-800 bg-gradient-to-br from-slate-50 to-white dark:from-slate-700 dark:to-slate-800 p-4"
|
||||
|
||||
Reference in new issue
Block a user