simplify network stats

This commit is contained in:
mbecker20
2025-01-18 13:01:18 -08:00
parent 7486e7466b
commit 3e8d6e401b
8 changed files with 105 additions and 195 deletions

View File

@@ -28,9 +28,6 @@ pub async fn record_server_stats(ts: i64) {
disks: stats.disks.clone(),
network_ingress_bytes: stats.network_ingress_bytes,
network_egress_bytes: stats.network_egress_bytes,
network_usage_interface: stats
.network_usage_interface
.clone(),
})
})
.collect::<Vec<_>>();

View File

@@ -2,8 +2,7 @@ use std::{cmp::Ordering, sync::OnceLock};
use async_timing_util::wait_until_timelength;
use komodo_client::entities::stats::{
SingleDiskUsage, SingleNetworkInterfaceUsage, SystemInformation,
SystemProcess, SystemStats,
SingleDiskUsage, SystemInformation, SystemProcess, SystemStats,
};
use sysinfo::{ProcessesToUpdate, System};
use tokio::sync::RwLock;
@@ -84,40 +83,21 @@ impl StatsClient {
let total_mem = self.system.total_memory();
let available_mem = self.system.available_memory();
let mut total_ingress: u64 = 0;
let mut total_egress: u64 = 0;
let mut network_ingress_bytes: u64 = 0;
let mut network_egress_bytes: u64 = 0;
// Fetch network data (Ingress and Egress)
let network_usage: Vec<SingleNetworkInterfaceUsage> = self
.networks
.iter()
.map(|(interface_name, network)| {
let ingress = network.received();
let egress = network.transmitted();
// Update total ingress and egress
total_ingress += ingress;
total_egress += egress;
// Return per-interface network stats
SingleNetworkInterfaceUsage {
name: interface_name.clone(),
ingress_bytes: ingress as f64,
egress_bytes: egress as f64,
}
})
.collect();
for (_, network) in self.networks.iter() {
network_ingress_bytes += network.received();
network_egress_bytes += network.transmitted();
}
SystemStats {
cpu_perc: self.system.global_cpu_usage(),
mem_free_gb: self.system.free_memory() as f64 / BYTES_PER_GB,
mem_used_gb: (total_mem - available_mem) as f64 / BYTES_PER_GB,
mem_total_gb: total_mem as f64 / BYTES_PER_GB,
// Added total ingress and egress
network_ingress_bytes: total_ingress as f64,
network_egress_bytes: total_egress as f64,
network_usage_interface: network_usage,
network_ingress_bytes: network_ingress_bytes as f64,
network_egress_bytes: network_egress_bytes as f64,
disks: self.get_disks(),
polling_rate: self.stats.polling_rate,
refresh_ts: self.stats.refresh_ts,

View File

@@ -49,17 +49,17 @@ pub struct SystemStatsRecord {
pub disk_used_gb: f64,
/// Total disk size in GB
pub disk_total_gb: f64,
/// Breakdown of individual disks, ie their usages, sizes, and mount points
/// Breakdown of individual disks, including their usage, total size, and mount point
pub disks: Vec<SingleDiskUsage>,
/// Network ingress usage in bytes
/// Total network ingress in bytes
#[serde(default)]
pub network_ingress_bytes: f64,
/// Network egress usage in bytes
/// Total network egress in bytes
#[serde(default)]
pub network_egress_bytes: f64,
/// Network usage by interface name (ingress, egress in bytes)
#[serde(default)]
pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
// /// Network usage by interface name (ingress, egress in bytes)
// #[serde(default)]
// pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
}
/// Realtime system stats data.
@@ -86,9 +86,9 @@ pub struct SystemStats {
/// Network egress usage in MB
#[serde(default)]
pub network_egress_bytes: f64,
/// Network usage by interface name (ingress, egress in bytes)
#[serde(default)]
pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
// /// Network usage by interface name (ingress, egress in bytes)
// #[serde(default)]
// pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
// metadata
/// The rate the system stats are being polled from the system
pub polling_rate: Timelength,

View File

@@ -1820,16 +1820,6 @@ export interface SingleDiskUsage {
total_gb: number;
}
/** Info for network interface usage. */
export interface SingleNetworkInterfaceUsage {
/** The network interface name */
name: string;
/** The ingress in bytes */
ingress_bytes: number;
/** The egress in bytes */
egress_bytes: number;
}
export enum Timelength {
OneSecond = "1-sec",
FiveSeconds = "5-sec",
@@ -1875,8 +1865,6 @@ export interface SystemStats {
network_ingress_bytes?: number;
/** Network egress usage in MB */
network_egress_bytes?: number;
/** Network usage by interface name (ingress, egress in bytes) */
network_usage_interface?: SingleNetworkInterfaceUsage[];
/** The rate the system stats are being polled from the system */
polling_rate: Timelength;
/** Unix timestamp in milliseconds when stats were last polled */
@@ -5099,14 +5087,12 @@ export interface SystemStatsRecord {
disk_used_gb: number;
/** Total disk size in GB */
disk_total_gb: number;
/** Breakdown of individual disks, ie their usages, sizes, and mount points */
/** Breakdown of individual disks, including their usage, total size, and mount point */
disks: SingleDiskUsage[];
/** Network ingress usage in bytes */
/** Total network ingress in bytes */
network_ingress_bytes?: number;
/** Network egress usage in bytes */
/** Total network egress in bytes */
network_egress_bytes?: number;
/** Network usage by interface name (ingress, egress in bytes) */
network_usage_interface?: SingleNetworkInterfaceUsage[];
}
/** Response to [GetHistoricalServerStats]. */
@@ -6833,6 +6819,16 @@ export interface SetUsersInUserGroup {
users: string[];
}
/** Info for network interface usage. */
export interface SingleNetworkInterfaceUsage {
/** The network interface name */
name: string;
/** The ingress in bytes */
ingress_bytes: number;
/** The egress in bytes */
egress_bytes: number;
}
/** Configuration for a Slack alerter. */
export interface SlackAlerterEndpoint {
/** The Slack app webhook url */

View File

@@ -1910,15 +1910,6 @@ export interface SingleDiskUsage {
/** Total size of the disk in GB */
total_gb: number;
}
/** Info for network interface usage. */
export interface SingleNetworkInterfaceUsage {
/** The network interface name */
name: string;
/** The ingress in bytes */
ingress_bytes: number;
/** The egress in bytes */
egress_bytes: number;
}
export declare enum Timelength {
OneSecond = "1-sec",
FiveSeconds = "5-sec",
@@ -1963,8 +1954,6 @@ export interface SystemStats {
network_ingress_bytes?: number;
/** Network egress usage in MB */
network_egress_bytes?: number;
/** Network usage by interface name (ingress, egress in bytes) */
network_usage_interface?: SingleNetworkInterfaceUsage[];
/** The rate the system stats are being polled from the system */
polling_rate: Timelength;
/** Unix timestamp in milliseconds when stats were last polled */
@@ -4852,14 +4841,12 @@ export interface SystemStatsRecord {
disk_used_gb: number;
/** Total disk size in GB */
disk_total_gb: number;
/** Breakdown of individual disks, ie their usages, sizes, and mount points */
/** Breakdown of individual disks, including their usage, total size, and mount point */
disks: SingleDiskUsage[];
/** Network ingress usage in bytes */
/** Total network ingress in bytes */
network_ingress_bytes?: number;
/** Network egress usage in bytes */
/** Total network egress in bytes */
network_egress_bytes?: number;
/** Network usage by interface name (ingress, egress in bytes) */
network_usage_interface?: SingleNetworkInterfaceUsage[];
}
/** Response to [GetHistoricalServerStats]. */
export interface GetHistoricalServerStatsResponse {
@@ -6411,6 +6398,15 @@ export interface SetUsersInUserGroup {
/** The user ids or usernames to hard set as the group's users. */
users: string[];
}
/** Info for network interface usage. */
export interface SingleNetworkInterfaceUsage {
/** The network interface name */
name: string;
/** The ingress in bytes */
ingress_bytes: number;
/** The egress in bytes */
egress_bytes: number;
}
/** Configuration for a Slack alerter. */
export interface SlackAlerterEndpoint {
/** The Slack app webhook url */

View File

@@ -8,10 +8,3 @@ const statsGranularityAtom = atomWithStorage<Types.Timelength>(
);
export const useStatsGranularity = () => useAtom(statsGranularityAtom);
const selectedNetworkInterfaceAtom = atomWithStorage<string | undefined>(
"selected-network-interface-v0",
undefined // Default value is `undefined` (Global view)
);
export const useSelectedNetworkInterface = () => useAtom(selectedNetworkInterfaceAtom);

View File

@@ -2,14 +2,14 @@ import { hex_color_by_intention } from "@lib/color";
import { useRead } from "@lib/hooks";
import { Types } from "komodo_client";
import { useMemo } from "react";
import { useStatsGranularity, useSelectedNetworkInterface } from "./hooks";
import { useStatsGranularity } from "./hooks";
import { Loader2 } from "lucide-react";
import { AxisOptions, Chart } from "react-charts";
import { convertTsMsToLocalUnixTsInMs } from "@lib/utils";
import { useTheme } from "@ui/theme";
import { fmt_utc_date } from "@lib/formatting";
type StatType = "cpu" | "mem" | "disk" | "network_ingress" | "network_egress" | "network_interface_ingress" | "network_interface_egress";
type StatType = "cpu" | "mem" | "disk" | "network_ingress" | "network_egress";
type StatDatapoint = { date: number; value: number };
@@ -22,13 +22,11 @@ export const StatChart = ({
type: StatType;
className?: string;
}) => {
const [selectedInterface] = useSelectedNetworkInterface();
const [granularity] = useStatsGranularity();
const { data, isPending } = useRead("GetHistoricalServerStats", {
server: server_id,
granularity,
selectedInterface,
});
const stats = useMemo(
@@ -37,7 +35,7 @@ export const StatChart = ({
.map((stat) => {
return {
date: convertTsMsToLocalUnixTsInMs(stat.ts),
value: getStat(stat, type, selectedInterface),
value: getStat(stat, type),
};
})
.reverse(),
@@ -58,6 +56,10 @@ export const StatChart = ({
);
};
const BYTES_PER_GB = 1073741824.0;
const BYTES_PER_MB = 1048576.0;
const BYTES_PER_KB = 1024.0;
export const InnerStatChart = ({
type,
stats,
@@ -72,12 +74,11 @@ export const InnerStatChart = ({
? "dark"
: "light"
: _theme;
const BYTES_PER_GB = 1073741824.0;
const BYTES_PER_MB = 1048576.0;
const BYTES_PER_KB = 1024.0;
const min = stats?.[0]?.date ?? 0;
const max = stats?.[stats.length - 1]?.date ?? 0;
const diff = max - min;
const timeAxis = useMemo((): AxisOptions<StatDatapoint> => {
return {
getValue: (datum) => new Date(datum.date),
@@ -177,27 +178,14 @@ export const InnerStatChart = ({
}}
/>
);
};
const getStat = (stat: Types.SystemStatsRecord, type: StatType, selectedInterface?: string) => {
const getStat = (stat: Types.SystemStatsRecord, type: StatType) => {
if (type === "cpu") return stat.cpu_perc || 0;
if (type === "mem") return (100 * stat.mem_used_gb) / stat.mem_total_gb;
if (type === "disk") return (100 * stat.disk_used_gb) / stat.disk_total_gb;
if (type === "network_ingress") return stat.network_ingress_bytes || 0;
if (type === "network_egress") return stat.network_egress_bytes || 0;
if (type === "network_interface_ingress")
return selectedInterface
? stat.network_usage_interface?.find(
(networkInterface) => networkInterface.name === selectedInterface
)?.ingress_bytes || 0
: stat.network_ingress_bytes || 0;
if (type === "network_interface_egress")
return selectedInterface
? stat.network_usage_interface?.find(
(networkInterface) => networkInterface.name === selectedInterface
)?.egress_bytes || 0
: stat.network_egress_bytes || 0;
return 0;
};
@@ -205,7 +193,7 @@ const getColor = (type: StatType) => {
if (type === "cpu") return hex_color_by_intention("Good");
if (type === "mem") return hex_color_by_intention("Warning");
if (type === "disk") return hex_color_by_intention("Neutral");
if (type === "network_interface_ingress") return hex_color_by_intention("Critical");
if (type === "network_interface_egress") return hex_color_by_intention("Unknown");
if (type === "network_ingress") return hex_color_by_intention("Good");
if (type === "network_egress") return hex_color_by_intention("Critical");
return hex_color_by_intention("Unknown");
};
};

View File

@@ -1,11 +1,5 @@
import { Section } from "@components/layouts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
import { Progress } from "@ui/progress";
import { Cpu, Database, Loader2, MemoryStick } from "lucide-react";
import { useRead } from "@lib/hooks";
@@ -14,7 +8,7 @@ import { DataTable, SortableHeader } from "@ui/data-table";
import { ReactNode, useMemo, useState } from "react";
import { Input } from "@ui/input";
import { StatChart } from "./stat-chart";
import { useStatsGranularity, useSelectedNetworkInterface } from "./hooks";
import { useStatsGranularity } from "./hooks";
import {
Select,
SelectContent,
@@ -32,7 +26,6 @@ export const ServerStats = ({
titleOther?: ReactNode;
}) => {
const [interval, setInterval] = useStatsGranularity();
const [networkInterface, setNetworkInterface] = useSelectedNetworkInterface();
const stats = useRead(
"GetSystemStats",
@@ -75,6 +68,10 @@ export const ServerStats = ({
header: "Kernel",
accessorKey: "kernel",
},
{
header: "CPU",
accessorKey: "cpu_brand",
},
{
header: "Core Count",
accessorFn: ({ core_count }) =>
@@ -93,11 +90,11 @@ export const ServerStats = ({
</Section>
<Section title="Current">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex flex-col xl:flex-row gap-4">
<CPU stats={stats} />
<RAM stats={stats} />
<NETWORK stats={stats} />
<DISK stats={stats} />
<NETWORK stats={stats} />
</div>
</Section>
@@ -136,39 +133,6 @@ export const ServerStats = ({
</SelectContent>
</Select>
</div>
{/* Network Interface Dropdown */}
<div className="flex items-center gap-2">
<div className="text-muted-foreground">Interface:</div>
<Select
value={networkInterface ?? "all"} // Show "all" if networkInterface is undefined
onValueChange={(interfaceName) => {
if (interfaceName === "all") {
setNetworkInterface(undefined); // Set undefined for "All" option
} else {
setNetworkInterface(interfaceName);
}
}}
>
<SelectTrigger className="w-[150px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
{/* Iterate over the vector and access the `name` property */}
{(stats?.network_usage_interface ?? []).map(
(networkInterface) => (
<SelectItem
key={networkInterface.name}
value={networkInterface.name}
>
{networkInterface.name}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
</div>
}
>
@@ -355,44 +319,62 @@ const ProcessesInner = ({
);
};
const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
const perc = stats?.cpu_perc;
const StatBar = ({
title,
icon,
percentage,
}: {
title: string;
icon: ReactNode;
percentage: number | undefined;
}) => {
return (
<Card className="w-full">
<CardHeader className="flex-row justify-between">
<CardTitle>CPU Usage</CardTitle>
<CardHeader className="flex-row items-center justify-between">
<CardTitle>{title}</CardTitle>
<div className="flex gap-2 items-center">
<CardDescription>{perc?.toFixed(2)}%</CardDescription>
<Cpu className="w-4 h-4" />
<div className="text-lg">{percentage?.toFixed(2)}%</div>
{icon}
</div>
</CardHeader>
<CardContent>
<Progress value={perc} className="h-4" />
<Progress value={percentage} className="h-4" />
</CardContent>
</Card>
);
};
const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
return (
<StatBar
title="CPU Usage"
icon={<Cpu className="w-5 h-5" />}
percentage={stats?.cpu_perc}
/>
);
};
const RAM = ({ stats }: { stats: Types.SystemStats | undefined }) => {
const used = stats?.mem_used_gb;
const total = stats?.mem_total_gb;
const perc = ((used ?? 0) / (total ?? 0)) * 100;
return (
<Card className="w-full">
<CardHeader className="flex-row justify-between">
<CardTitle>RAM Usage</CardTitle>
<div className="flex gap-2 items-center">
<CardDescription>{perc.toFixed(2)}%</CardDescription>
<MemoryStick className="w-4 h-4" />
</div>
</CardHeader>
<CardContent>
<Progress value={perc} className="h-4" />
</CardContent>
</Card>
<StatBar
title="RAM Usage"
icon={<MemoryStick className="w-5 h-5" />}
percentage={((used ?? 0) / (total ?? 0)) * 100}
/>
);
};
const DISK = ({ stats }: { stats: Types.SystemStats | undefined }) => {
const used = stats?.disks.reduce((acc, curr) => (acc += curr.used_gb), 0);
const total = stats?.disks.reduce((acc, curr) => (acc += curr.total_gb), 0);
return (
<StatBar
title="Disk Usage"
icon={<Database className="w-5 h-5" />}
percentage={((used ?? 0) / (total ?? 0)) * 100}
/>
);
};
@@ -441,25 +423,3 @@ const NETWORK = ({ stats }: { stats: Types.SystemStats | undefined }) => {
</Card>
);
};
const DISK = ({ stats }: { stats: Types.SystemStats | undefined }) => {
const used = stats?.disks.reduce((acc, curr) => (acc += curr.used_gb), 0);
const total = stats?.disks.reduce((acc, curr) => (acc += curr.total_gb), 0);
const perc = ((used ?? 0) / (total ?? 0)) * 100;
return (
<Card className="w-full">
<CardHeader className="flex-row justify-between">
<CardTitle>Disk Usage</CardTitle>
<div className="flex gap-2 items-center">
<CardDescription>{perc?.toFixed(2)}%</CardDescription>
<Database className="w-4 h-4" />
</div>
</CardHeader>
<CardContent>
<Progress value={perc} className="h-4" />
</CardContent>
</Card>
);
};