diff --git a/beszel/internal/agent/agent.go b/beszel/internal/agent/agent.go index a622189..ce699e8 100644 --- a/beszel/internal/agent/agent.go +++ b/beszel/internal/agent/agent.go @@ -15,6 +15,7 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" "sync" "time" @@ -23,6 +24,7 @@ import ( "github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/host" "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/sensors" sshServer "github.com/gliderlabs/ssh" psutilNet "github.com/shirou/gopsutil/v4/net" @@ -140,6 +142,21 @@ func (a *Agent) getSystemStats() (*system.Info, *system.Stats) { a.netIoStats.Time = time.Now() } + // temperatures + if temps, err := sensors.SensorsTemperatures(); err == nil { + systemStats.Temperatures = make(map[string]float64) + // log.Printf("Temperatures: %+v\n", temps) + for i, temp := range temps { + if _, ok := systemStats.Temperatures[temp.SensorKey]; ok { + // if key already exists, append int to key + systemStats.Temperatures[temp.SensorKey+"_"+strconv.Itoa(i)] = twoDecimals(temp.Temperature) + } else { + systemStats.Temperatures[temp.SensorKey] = twoDecimals(temp.Temperature) + } + } + // log.Printf("Temperature map: %+v\n", systemStats.Temperatures) + } + systemInfo := &system.Info{ Cpu: systemStats.Cpu, MemPct: systemStats.MemPct, diff --git a/beszel/internal/entities/system/system.go b/beszel/internal/entities/system/system.go index 154ab1a..2284dd0 100644 --- a/beszel/internal/entities/system/system.go +++ b/beszel/internal/entities/system/system.go @@ -6,20 +6,21 @@ import ( ) type Stats struct { - Cpu float64 `json:"cpu"` - Mem float64 `json:"m"` - MemUsed float64 `json:"mu"` - MemPct float64 `json:"mp"` - MemBuffCache float64 `json:"mb"` - Swap float64 `json:"s"` - SwapUsed float64 `json:"su"` - Disk float64 `json:"d"` - DiskUsed float64 `json:"du"` - DiskPct float64 `json:"dp"` - DiskRead float64 `json:"dr"` - DiskWrite float64 `json:"dw"` - NetworkSent float64 `json:"ns"` - NetworkRecv float64 `json:"nr"` + Cpu float64 `json:"cpu"` + Mem float64 `json:"m"` + MemUsed float64 `json:"mu"` + MemPct float64 `json:"mp"` + MemBuffCache float64 `json:"mb"` + Swap float64 `json:"s"` + SwapUsed float64 `json:"su"` + Disk float64 `json:"d"` + DiskUsed float64 `json:"du"` + DiskPct float64 `json:"dp"` + DiskRead float64 `json:"dr"` + DiskWrite float64 `json:"dw"` + NetworkSent float64 `json:"ns"` + NetworkRecv float64 `json:"nr"` + Temperatures map[string]float64 `json:"t,omitempty"` } type DiskIoStats struct { diff --git a/beszel/internal/records/records.go b/beszel/internal/records/records.go index 673c820..dcff1fd 100644 --- a/beszel/internal/records/records.go +++ b/beszel/internal/records/records.go @@ -166,7 +166,11 @@ func (rm *RecordManager) CreateLongerRecords() { // Calculate the average stats of a list of system_stats records without reflect func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats { var sum system.Stats + sum.Temperatures = make(map[string]float64) + count := float64(len(records)) + // use different counter for temps in case some records don't have them + tempCount := float64(0) var stats system.Stats for _, record := range records { @@ -185,9 +189,18 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta sum.DiskWrite += stats.DiskWrite sum.NetworkSent += stats.NetworkSent sum.NetworkRecv += stats.NetworkRecv + if stats.Temperatures != nil { + tempCount++ + for key, value := range stats.Temperatures { + if _, ok := sum.Temperatures[key]; !ok { + sum.Temperatures[key] = 0 + } + sum.Temperatures[key] += value + } + } } - return system.Stats{ + stats = system.Stats{ Cpu: twoDecimals(sum.Cpu / count), Mem: twoDecimals(sum.Mem / count), MemUsed: twoDecimals(sum.MemUsed / count), @@ -203,6 +216,15 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta NetworkSent: twoDecimals(sum.NetworkSent / count), NetworkRecv: twoDecimals(sum.NetworkRecv / count), } + + if len(sum.Temperatures) != 0 { + stats.Temperatures = make(map[string]float64) + for key, value := range sum.Temperatures { + stats.Temperatures[key] = twoDecimals(value / tempCount) + } + } + + return stats } // Calculate the average stats of a list of container_stats records diff --git a/beszel/site/src/components/add-system.tsx b/beszel/site/src/components/add-system.tsx index a420cff..6f37500 100644 --- a/beszel/site/src/components/add-system.tsx +++ b/beszel/site/src/components/add-system.tsx @@ -62,7 +62,7 @@ export function AddSystemButton({ className }: { className?: string }) { className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')} > - Add System + Add System diff --git a/beszel/site/src/components/charts/temperature-chart.tsx b/beszel/site/src/components/charts/temperature-chart.tsx new file mode 100644 index 0000000..a6d0122 --- /dev/null +++ b/beszel/site/src/components/charts/temperature-chart.tsx @@ -0,0 +1,129 @@ +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts' + +import { + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart' +import { + chartTimeData, + cn, + formatShortDate, + toFixedWithoutTrailingZeros, + useYaxisWidth, +} from '@/lib/utils' +import { useStore } from '@nanostores/react' +import { $chartTime } from '@/lib/stores' +import { SystemStatsRecord } from '@/types' +import { useMemo, useRef } from 'react' + +export default function TemperatureChart({ + ticks, + systemData, +}: { + ticks: number[] + systemData: SystemStatsRecord[] +}) { + const chartRef = useRef(null) + const yAxisWidth = useYaxisWidth(chartRef) + const chartTime = useStore($chartTime) + + /** Format temperature data for chart and assign colors */ + const newChartData = useMemo(() => { + const chartData = { data: [], colors: {} } as { + data: Record[] + colors: Record + } + const tempSums = {} as Record + for (let data of systemData) { + let newData = { created: data.created } as Record + let keys = Object.keys(data.stats?.t ?? {}) + for (let i = 0; i < keys.length; i++) { + let key = keys[i] + newData[key] = data.stats.t![key] + tempSums[key] = (tempSums[key] ?? 0) + newData[key] + } + chartData.data.push(newData) + } + const keys = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a]) + for (let key of keys) { + chartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)` + } + return chartData + }, [systemData]) + + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) + + return ( +
+ {/* {!yAxisSet && } */} + + + + toFixedWithoutTrailingZeros(value, 2)} + tickLine={false} + axisLine={false} + unit={' °C'} + /> + + b.value - a.value} + content={ + formatShortDate(data[0].payload.created)} + indicator="line" + /> + } + /> + {Object.keys(newChartData.colors).map((key) => ( + + ))} + } /> + + +
+ ) +} diff --git a/beszel/site/src/components/routes/system.tsx b/beszel/site/src/components/routes/system.tsx index e7e8c11..7d384e9 100644 --- a/beszel/site/src/components/routes/system.tsx +++ b/beszel/site/src/components/routes/system.tsx @@ -20,6 +20,7 @@ const DiskIoChart = lazy(() => import('../charts/disk-io-chart')) const BandwidthChart = lazy(() => import('../charts/bandwidth-chart')) const ContainerNetChart = lazy(() => import('../charts/container-net-chart')) const SwapChart = lazy(() => import('../charts/swap-chart')) +const TemperatureChart = lazy(() => import('../charts/temperature-chart')) export default function SystemDetail({ name }: { name: string }) { const systems = useStore($systems) @@ -271,6 +272,12 @@ export default function SystemDetail({ name }: { name: string }) { )} + {systemStats.at(-1)?.stats.t && ( + + + + )} + (({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => { - const { config } = useChart() + // const { config } = useChart() if (!payload?.length) { return null @@ -268,33 +268,35 @@ const ChartLegendContent = React.forwardRef<
{payload.map((item) => { - const key = `${nameKey || item.dataKey || 'value'}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) + // const key = `${nameKey || item.dataKey || 'value'}` + // const itemConfig = getPayloadConfigFromPayload(config, item, key) return (
svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground' + // 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground text-muted-foreground' + 'flex items-center gap-1.5 text-muted-foreground' )} > - {itemConfig?.icon && !hideIcon ? ( + {/* {itemConfig?.icon && !hideIcon ? ( - ) : ( -
- )} - {itemConfig?.label} + ) : ( */} +
+ {item.value} + {/* )} */} + {/* {itemConfig?.label} */}
) })} diff --git a/beszel/site/src/types.d.ts b/beszel/site/src/types.d.ts index 2f078c1..07200f5 100644 --- a/beszel/site/src/types.d.ts +++ b/beszel/site/src/types.d.ts @@ -57,6 +57,8 @@ export interface SystemStats { ns: number /** network received (mb) */ nr: number + /** temperatures */ + t?: Record } export interface ContainerStatsRecord extends RecordModel {