add docker container net stats

This commit is contained in:
Henry Dollman
2024-08-04 13:26:17 -04:00
parent c3e3d483b0
commit 2a3b228668
11 changed files with 276 additions and 67 deletions

View File

@@ -17,7 +17,6 @@ import { Copy, Plus } from 'lucide-react'
import { useState, useRef, MutableRefObject, useEffect } from 'react'
import { useStore } from '@nanostores/react'
import { copyToClipboard } from '@/lib/utils'
import { SystemStats } from '@/types'
export function AddSystemButton() {
const [open, setOpen] = useState(false)
@@ -75,7 +74,7 @@ export function AddSystemButton() {
<DialogHeader>
<DialogTitle className="mb-2">Add New System</DialogTitle>
<DialogDescription>
The agent must be running on the server to connect. Copy the{' '}
The agent must be running on the system to connect. Copy the{' '}
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent
below.
</DialogDescription>

View File

@@ -1,5 +1,3 @@
'use client'
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,

View File

@@ -1,5 +1,3 @@
'use client'
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
@@ -103,7 +101,7 @@ export default function ContainerMemChart({
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
// @ts-ignore
itemSorter={(a, b) => b.value - a.value}
content={<ChartTooltipContent unit=" MiB" indicator="line" />}
content={<ChartTooltipContent unit=" MB" indicator="line" />}
/>
{Object.keys(chartConfig).map((key) => (
<Area

View File

@@ -0,0 +1,147 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { useMemo } from 'react'
import { chartTimeData, formatShortDate } from '@/lib/utils'
import Spinner from '../spinner'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { Separator } from '@/components/ui/separator'
export default function ContainerCpuChart({
chartData,
ticks,
}: {
chartData: Record<string, number | number[]>[]
ticks: number[]
}) {
const chartTime = useStore($chartTime)
const chartConfig = useMemo(() => {
let config = {} as Record<
string,
{
label: string
color: string
}
>
const totalUsage = {} as Record<string, number>
for (let stats of chartData) {
for (let key in stats) {
if (!Array.isArray(stats[key])) {
continue
}
if (!(key in totalUsage)) {
totalUsage[key] = 0
}
totalUsage[key] += stats[key][2] ?? 0
}
}
let keys = Object.keys(totalUsage)
keys.sort((a, b) => (totalUsage[a] > totalUsage[b] ? -1 : 1))
const length = keys.length
for (let i = 0; i < length; i++) {
const key = keys[i]
const hue = ((i * 360) / length) % 360
config[key] = {
label: key,
color: `hsl(${hue}, 60%, 55%)`,
}
}
return config satisfies ChartConfig
}, [chartData])
if (!chartData.length || !ticks.length) {
return <Spinner />
}
return (
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto">
<AreaChart
accessibilityLayer
data={chartData}
margin={{
top: 10,
}}
reverseStackOrder={true}
>
<CartesianGrid vertical={false} />
<YAxis
// domain={[0, (max: number) => Math.max(Math.ceil(max), 0.4)]}
width={75}
tickLine={false}
axisLine={false}
unit={' MB'}
tickFormatter={(x) => (x % 1 === 0 ? x : x.toFixed(1))}
/>
<XAxis
dataKey="time"
domain={[ticks[0], ticks.at(-1)!]}
ticks={ticks}
type="number"
scale={'time'}
minTickGap={35}
tickMargin={8}
axisLine={false}
tickFormatter={chartTimeData[chartTime].format}
/>
<ChartTooltip
// cursor={false}
animationEasing="ease-out"
animationDuration={150}
labelFormatter={(_, data) => {
return (
<span>
{formatShortDate(data[0].payload.time)}
<br />
<small className="opacity-70">Total MB received / transmitted</small>
</span>
)
}}
// @ts-ignore
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
indicator="line"
contentFormatter={(item, key) => {
try {
const sent = item?.payload?.[key][0] ?? 0
const received = item?.payload?.[key][1] ?? 0
return (
<span className="flex">
{received.toLocaleString()} MB<span className="opacity-70 ml-0.5"> rx </span>
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
{sent.toLocaleString()} MB<span className="opacity-70 ml-0.5"> tx</span>
</span>
)
} catch (e) {
return null
}
}}
/>
}
/>
{Object.keys(chartConfig).map((key) => (
<Area
key={key}
name={key}
// isAnimationActive={chartData.length < 20}
animateNewValues={false}
animationDuration={1200}
dataKey={(data) => data?.[key]?.[2] ?? 0}
type="monotoneX"
fill={chartConfig[key].color}
fillOpacity={0.4}
stroke={chartConfig[key].color}
stackId="a"
/>
))}
</AreaChart>
</ChartContainer>
)
}

View File

@@ -18,6 +18,7 @@ const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
const DiskChart = lazy(() => import('../charts/disk-chart'))
const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
export default function ServerDetail({ name }: { name: string }) {
const systems = useStore($systems)
@@ -32,6 +33,9 @@ export default function ServerDetail({ name }: { name: string }) {
const [dockerMemChartData, setDockerMemChartData] = useState(
[] as Record<string, number | string>[]
)
const [dockerNetChartData, setDockerNetChartData] = useState(
[] as Record<string, number | number[]>[]
)
useEffect(() => {
document.title = `${name} / Beszel`
@@ -45,6 +49,7 @@ export default function ServerDetail({ name }: { name: string }) {
setSystemStats([])
setDockerCpuChartData([])
setDockerMemChartData([])
setDockerNetChartData([])
}, [])
useEffect(resetCharts, [chartTime])
@@ -124,22 +129,30 @@ export default function ServerDetail({ name }: { name: string }) {
// container stats for charts
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
// console.log('containers', containers)
const dockerCpuData = [] as Record<string, number | string>[]
const dockerMemData = [] as Record<string, number | string>[]
const dockerCpuData = [] as typeof dockerCpuChartData
const dockerMemData = [] as typeof dockerMemChartData
const dockerNetData = [] as typeof dockerNetChartData
for (let { created, stats } of containers) {
const time = new Date(created).getTime()
let cpuData = { time } as (typeof dockerCpuChartData)[0]
let memData = { time } as (typeof dockerMemChartData)[0]
let netData = { time } as (typeof dockerNetChartData)[0]
for (let container of stats) {
cpuData[container.n] = container.c
memData[container.n] = container.m
netData[container.n] = [container.ns, container.nr, container.ns + container.nr] // sent, received, total
}
dockerCpuData.push(cpuData)
dockerMemData.push(memData)
dockerNetData.push(netData)
}
console.log('dockerCpuData', dockerCpuData)
// console.log('dockerMemData', dockerMemData)
console.log('dockerNetData', dockerNetData)
setDockerCpuChartData(dockerCpuData)
setDockerMemChartData(dockerMemData)
setDockerNetChartData(dockerNetData)
}, [])
const uptime = useMemo(() => {
@@ -243,6 +256,15 @@ export default function ServerDetail({ name }: { name: string }) {
<ChartCard title="Bandwidth" description="Network traffic of public interfaces">
<BandwidthChart ticks={ticks} systemData={systemStats} />
</ChartCard>
{dockerNetChartData.length > 0 && (
<ChartCard
title="Docker Network I/O"
description="Includes traffic between internal services"
>
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
</ChartCard>
)}
</div>
)
}

View File

@@ -100,6 +100,7 @@ const ChartTooltipContent = React.forwardRef<
nameKey?: string
labelKey?: string
unit?: string
contentFormatter?: (item: any, key: string) => React.ReactNode | string
}
>(
(
@@ -119,6 +120,7 @@ const ChartTooltipContent = React.forwardRef<
labelKey,
unit,
itemSorter,
contentFormatter: content = undefined,
},
ref
) => {
@@ -180,7 +182,7 @@ const ChartTooltipContent = React.forwardRef<
return (
<div
key={item.dataKey}
key={item?.name || item.dataKey}
className={cn(
'flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
indicator === 'dot' && 'items-center'
@@ -228,7 +230,9 @@ const ChartTooltipContent = React.forwardRef<
</div>
{item.value !== undefined && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString() + (unit ? unit : '')}
{content && typeof content === 'function'
? content(item, key)
: item.value.toLocaleString() + (unit ? unit : '')}
</span>
)}
</div>

View File

@@ -67,6 +67,10 @@ interface ContainerStats {
c: number
/** memory used (gb) */
m: number
// network sent (mb)
ns: number
// network received (mb)
nr: number
}
export interface SystemStatsRecord extends RecordModel {