add temperature chart

This commit is contained in:
Henry Dollman
2024-08-21 22:14:56 -04:00
parent 88db920ebe
commit 130c9bd696
9 changed files with 211 additions and 32 deletions

View File

@@ -15,6 +15,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -23,6 +24,7 @@ import (
"github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host" "github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/mem" "github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/sensors"
sshServer "github.com/gliderlabs/ssh" sshServer "github.com/gliderlabs/ssh"
psutilNet "github.com/shirou/gopsutil/v4/net" psutilNet "github.com/shirou/gopsutil/v4/net"
@@ -140,6 +142,21 @@ func (a *Agent) getSystemStats() (*system.Info, *system.Stats) {
a.netIoStats.Time = time.Now() 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{ systemInfo := &system.Info{
Cpu: systemStats.Cpu, Cpu: systemStats.Cpu,
MemPct: systemStats.MemPct, MemPct: systemStats.MemPct,

View File

@@ -6,20 +6,21 @@ import (
) )
type Stats struct { type Stats struct {
Cpu float64 `json:"cpu"` Cpu float64 `json:"cpu"`
Mem float64 `json:"m"` Mem float64 `json:"m"`
MemUsed float64 `json:"mu"` MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"` MemPct float64 `json:"mp"`
MemBuffCache float64 `json:"mb"` MemBuffCache float64 `json:"mb"`
Swap float64 `json:"s"` Swap float64 `json:"s"`
SwapUsed float64 `json:"su"` SwapUsed float64 `json:"su"`
Disk float64 `json:"d"` Disk float64 `json:"d"`
DiskUsed float64 `json:"du"` DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"` DiskPct float64 `json:"dp"`
DiskRead float64 `json:"dr"` DiskRead float64 `json:"dr"`
DiskWrite float64 `json:"dw"` DiskWrite float64 `json:"dw"`
NetworkSent float64 `json:"ns"` NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"` NetworkRecv float64 `json:"nr"`
Temperatures map[string]float64 `json:"t,omitempty"`
} }
type DiskIoStats struct { type DiskIoStats struct {

View File

@@ -166,7 +166,11 @@ func (rm *RecordManager) CreateLongerRecords() {
// Calculate the average stats of a list of system_stats records without reflect // Calculate the average stats of a list of system_stats records without reflect
func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats { func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats {
var sum system.Stats var sum system.Stats
sum.Temperatures = make(map[string]float64)
count := float64(len(records)) count := float64(len(records))
// use different counter for temps in case some records don't have them
tempCount := float64(0)
var stats system.Stats var stats system.Stats
for _, record := range records { for _, record := range records {
@@ -185,9 +189,18 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
sum.DiskWrite += stats.DiskWrite sum.DiskWrite += stats.DiskWrite
sum.NetworkSent += stats.NetworkSent sum.NetworkSent += stats.NetworkSent
sum.NetworkRecv += stats.NetworkRecv 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), Cpu: twoDecimals(sum.Cpu / count),
Mem: twoDecimals(sum.Mem / count), Mem: twoDecimals(sum.Mem / count),
MemUsed: twoDecimals(sum.MemUsed / count), MemUsed: twoDecimals(sum.MemUsed / count),
@@ -203,6 +216,15 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
NetworkSent: twoDecimals(sum.NetworkSent / count), NetworkSent: twoDecimals(sum.NetworkSent / count),
NetworkRecv: twoDecimals(sum.NetworkRecv / 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 // Calculate the average stats of a list of container_stats records

View File

@@ -62,7 +62,7 @@ export function AddSystemButton({ className }: { className?: string }) {
className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')} className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')}
> >
<PlusIcon className="h-4 w-4 -ml-1" /> <PlusIcon className="h-4 w-4 -ml-1" />
Add <span className="hidden sm:inline">System</span> Add <span className="hidden xs:inline">System</span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg"> <DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg">

View File

@@ -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<HTMLDivElement>(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<string, number | string>[]
colors: Record<string, string>
}
const tempSums = {} as Record<string, number>
for (let data of systemData) {
let newData = { created: data.created } as Record<string, number | string>
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 (
<div ref={chartRef}>
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<LineChart
accessibilityLayer
data={newChartData.data}
margin={{
left: 0,
right: 0,
top: 10,
bottom: 0,
}}
>
<CartesianGrid vertical={false} />
<YAxis
className="tracking-tighter"
width={yAxisWidth}
tickFormatter={(value) => toFixedWithoutTrailingZeros(value, 2)}
tickLine={false}
axisLine={false}
unit={' °C'}
/>
<XAxis
dataKey="created"
domain={[ticks[0], ticks.at(-1)!]}
ticks={ticks}
type="number"
scale={'time'}
minTickGap={35}
tickMargin={8}
axisLine={false}
tickFormatter={chartTimeData[chartTime].format}
/>
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-ignore
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
unit=" °C"
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
indicator="line"
/>
}
/>
{Object.keys(newChartData.colors).map((key) => (
<Line
key={key}
dataKey={key}
name={key}
type="monotoneX"
dot={false}
strokeWidth={1.5}
fill="hsl(360, 60%, 55%)"
stroke={newChartData.colors[key]}
isAnimationActive={false}
/>
))}
<ChartLegend content={<ChartLegendContent />} />
</LineChart>
</ChartContainer>
</div>
)
}

View File

@@ -20,6 +20,7 @@ const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart')) const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
const ContainerNetChart = lazy(() => import('../charts/container-net-chart')) const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
const SwapChart = lazy(() => import('../charts/swap-chart')) const SwapChart = lazy(() => import('../charts/swap-chart'))
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
export default function SystemDetail({ name }: { name: string }) { export default function SystemDetail({ name }: { name: string }) {
const systems = useStore($systems) const systems = useStore($systems)
@@ -271,6 +272,12 @@ export default function SystemDetail({ name }: { name: string }) {
</ChartCard> </ChartCard>
)} )}
{systemStats.at(-1)?.stats.t && (
<ChartCard title="Temperature" description="Temperature of system components">
<TemperatureChart ticks={ticks} systemData={systemStats} />
</ChartCard>
)}
<ChartCard <ChartCard
title="Disk Usage" title="Disk Usage"
description="Usage of partition where the root filesystem is mounted" description="Usage of partition where the root filesystem is mounted"

View File

@@ -21,7 +21,6 @@ import {
} from '@/components/ui/table' } from '@/components/ui/table'
import { Button, buttonVariants } from '@/components/ui/button' import { Button, buttonVariants } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { import {
DropdownMenu, DropdownMenu,

View File

@@ -258,7 +258,7 @@ const ChartLegendContent = React.forwardRef<
nameKey?: string nameKey?: string
} }
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => { >(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
const { config } = useChart() // const { config } = useChart()
if (!payload?.length) { if (!payload?.length) {
return null return null
@@ -268,33 +268,35 @@ const ChartLegendContent = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
'flex items-center justify-center gap-4', 'flex items-center justify-center gap-4 gap-y-1 flex-wrap',
verticalAlign === 'top' ? 'pb-3' : 'pt-3', verticalAlign === 'top' ? 'pb-3' : 'pt-3',
className className
)} )}
> >
{payload.map((item) => { {payload.map((item) => {
const key = `${nameKey || item.dataKey || 'value'}` // const key = `${nameKey || item.dataKey || 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) // const itemConfig = getPayloadConfigFromPayload(config, item, key)
return ( return (
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
'flex items-center gap-1.5 [&>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.icon /> <itemConfig.icon />
) : ( ) : ( */}
<div <div
className="h-2 w-2 shrink-0 rounded-[2px]" className="h-2 w-2 shrink-0 rounded-[2px]"
style={{ style={{
backgroundColor: item.color, backgroundColor: item.color,
}} }}
/> />
)} {item.value}
{itemConfig?.label} {/* )} */}
{/* {itemConfig?.label} */}
</div> </div>
) )
})} })}

View File

@@ -57,6 +57,8 @@ export interface SystemStats {
ns: number ns: number
/** network received (mb) */ /** network received (mb) */
nr: number nr: number
/** temperatures */
t?: Record<string, number>
} }
export interface ContainerStatsRecord extends RecordModel { export interface ContainerStatsRecord extends RecordModel {