优化温度采集和渲染

This commit is contained in:
dushixiang committed 2025-12-06 17:49:46 +08:00
1 parent 307d8e4913
commit 6b3fd9e730
4 files changed
+225 -128

No files matched your search

+1 -2
View File
@@ -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 指令请求
+7 -1
View File
@@ -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,
}
+122 -89
View File
@@ -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"
}
+95 -36
View File
@@ -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"