refactor: update docker stats

This commit is contained in:
Mauricio Siu
2025-02-01 19:27:10 -06:00
parent ee2fed07b2
commit 96bb72eb99
9 changed files with 92 additions and 134 deletions

View File

@@ -90,9 +90,11 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length && payload[0]) { if (active && payload && payload.length && payload[0]) {
return ( return (
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border"> <div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p> {payload[0].payload.time && (
<p>{`Read ${payload[0].payload.readMb.toFixed(2)} MB`}</p> <p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
<p>{`Write: ${payload[0].payload.writeMb.toFixed(3)} MB`}</p> )}
<p>{`Read ${payload[0].payload.readMb} `}</p>
<p>{`Write: ${payload[0].payload.writeMb} `}</p>
</div> </div>
); );
} }

View File

@@ -19,7 +19,7 @@ export const DockerCpuChart = ({ acummulativeData }: Props) => {
return { return {
name: `Point ${index + 1}`, name: `Point ${index + 1}`,
time: item.time, time: item.time,
usage: item.value.toFixed(2), usage: item.value.toString().split("%")[0],
}; };
}); });
return ( return (
@@ -75,7 +75,9 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length && payload[0]) { if (active && payload && payload.length && payload[0]) {
return ( return (
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border"> <div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p> {payload[0].payload.time && (
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
)}
<p>{`CPU Usage: ${payload[0].payload.usage}%`}</p> <p>{`CPU Usage: ${payload[0].payload.usage}%`}</p>
</div> </div>
); );

View File

@@ -9,6 +9,7 @@ import {
YAxis, YAxis,
} from "recharts"; } from "recharts";
import type { DockerStatsJSON } from "./show"; import type { DockerStatsJSON } from "./show";
import { convertMemoryToBytes } from "./show";
interface Props { interface Props {
acummulativeData: DockerStatsJSON["memory"]; acummulativeData: DockerStatsJSON["memory"];
@@ -23,7 +24,8 @@ export const DockerMemoryChart = ({
return { return {
time: item.time, time: item.time,
name: `Point ${index + 1}`, name: `Point ${index + 1}`,
usage: (item.value.used / 1024 ** 3).toFixed(2), // @ts-ignore
usage: (convertMemoryToBytes(item.value.used) / 1024 ** 3).toFixed(2),
}; };
}); });
return ( return (
@@ -75,10 +77,13 @@ interface CustomTooltipProps {
} }
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => { const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length && payload[0]) { if (active && payload && payload.length && payload[0] && payload[0].payload) {
return ( return (
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border"> <div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p> {payload[0].payload.time && (
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
)}
<p>{`Memory usage: ${payload[0].payload.usage} GB`}</p> <p>{`Memory usage: ${payload[0].payload.usage} GB`}</p>
</div> </div>
); );

View File

@@ -19,8 +19,8 @@ export const DockerNetworkChart = ({ acummulativeData }: Props) => {
return { return {
time: item.time, time: item.time,
name: `Point ${index + 1}`, name: `Point ${index + 1}`,
inMB: item.value.inputMb.toFixed(2), inMB: item.value.inputMb,
outMB: item.value.outputMb.toFixed(2), outMB: item.value.outputMb,
}; };
}); });
return ( return (
@@ -86,9 +86,11 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length && payload[0]) { if (active && payload && payload.length && payload[0]) {
return ( return (
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border"> <div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p> {payload[0].payload.time && (
<p>{`In MB Usage: ${payload[0].payload.inMB} MB`}</p> <p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
<p>{`Out MB Usage: ${payload[0].payload.outMB} MB`}</p> )}
<p>{`In Usage: ${payload[0].payload.inMB} `}</p>
<p>{`Out Usage: ${payload[0].payload.outMB} `}</p>
</div> </div>
); );
} }

View File

@@ -22,8 +22,6 @@ const defaultData = {
memory: { memory: {
value: { value: {
used: 0, used: 0,
free: 0,
usedPercentage: 0,
total: 0, total: 0,
}, },
time: "", time: "",
@@ -60,8 +58,6 @@ export interface DockerStats {
memory: { memory: {
value: { value: {
used: number; used: number;
free: number;
usedPercentage: number;
total: number; total: number;
}; };
time: string; time: string;
@@ -100,6 +96,30 @@ export type DockerStatsJSON = {
disk: DockerStats["disk"][]; disk: DockerStats["disk"][];
}; };
export const convertMemoryToBytes = (
memoryString: string | undefined,
): number => {
if (!memoryString || typeof memoryString !== "string") {
return 0;
}
const value = Number.parseFloat(memoryString) || 0;
const unit = memoryString.replace(/[0-9.]/g, "").trim();
switch (unit) {
case "KiB":
return value * 1024;
case "MiB":
return value * 1024 * 1024;
case "GiB":
return value * 1024 * 1024 * 1024;
case "TiB":
return value * 1024 * 1024 * 1024 * 1024;
default:
return value;
}
};
export const DockerMonitoring = ({ export const DockerMonitoring = ({
appName, appName,
appType = "application", appType = "application",
@@ -208,7 +228,7 @@ export const DockerMonitoring = ({
<CardContent> <CardContent>
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Used: {currentData.cpu.value.toFixed(2)}% Used: {currentData.cpu.value}%
</span> </span>
<Progress <Progress
value={currentData.cpu.value} value={currentData.cpu.value}
@@ -218,7 +238,6 @@ export const DockerMonitoring = ({
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">
@@ -228,20 +247,26 @@ export const DockerMonitoring = ({
<CardContent> <CardContent>
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`} {`Used: ${currentData.memory.value.used} / Limit: ${currentData.memory.value.total} `}
</span> </span>
<Progress <Progress
value={currentData.memory.value.usedPercentage} value={
(convertMemoryToBytes(currentData.memory.value.used) /
convertMemoryToBytes(currentData.memory.value.total)) *
100
}
className="w-[100%]" className="w-[100%]"
/> />
<DockerMemoryChart <DockerMemoryChart
acummulativeData={acummulativeData.memory} acummulativeData={acummulativeData.memory}
memoryLimitGB={currentData.memory.value.total / 1024 ** 3} memoryLimitGB={
convertMemoryToBytes(currentData.memory.value.total) /
1024 ** 3
}
/> />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{appName === "dokploy" && ( {appName === "dokploy" && (
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
@@ -274,17 +299,12 @@ export const DockerMonitoring = ({
<CardContent> <CardContent>
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{`Read: ${currentData.block.value.readMb.toFixed( {`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
2,
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
3,
)} MB`}
</span> </span>
<DockerBlockChart acummulativeData={acummulativeData.block} /> <DockerBlockChart acummulativeData={acummulativeData.block} />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">
@@ -294,11 +314,7 @@ export const DockerMonitoring = ({
<CardContent> <CardContent>
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{`In MB: ${currentData.network.value.inputMb.toFixed( {`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
2,
)} MB / Out MB: ${currentData.network.value.outputMb.toFixed(
2,
)} MB`}
</span> </span>
<DockerNetworkChart <DockerNetworkChart
acummulativeData={acummulativeData.network} acummulativeData={acummulativeData.network}

View File

@@ -1,6 +1,7 @@
import type http from "node:http"; import type http from "node:http";
import { import {
docker, docker,
execAsync,
getLastAdvancedStatsFile, getLastAdvancedStatsFile,
recordAdvancedStats, recordAdvancedStats,
validateWebSocketRequest, validateWebSocketRequest,
@@ -70,12 +71,16 @@ export const setupDockerStatsMonitoringSocketServer = (
ws.close(4000, "Container not running"); ws.close(4000, "Container not running");
return; return;
} }
const { stdout, stderr } = await execAsync(
`docker stats ${container.Id} --no-stream --format \'{"BlockIO":"{{.BlockIO}}","CPUPerc":"{{.CPUPerc}}","Container":"{{.Container}}","ID":"{{.ID}}","MemPerc":"{{.MemPerc}}","MemUsage":"{{.MemUsage}}","Name":"{{.Name}}","NetIO":"{{.NetIO}}"}\'`,
);
if (stderr) {
console.error("Docker stats error:", stderr);
return;
}
const stat = JSON.parse(stdout);
const stats = await docker.getContainer(container.Id).stats({ await recordAdvancedStats(stat, appName);
stream: false,
});
await recordAdvancedStats(stats, appName);
const data = await getLastAdvancedStatsFile(appName); const data = await getLastAdvancedStatsFile(appName);
ws.send( ws.send(

View File

@@ -110,7 +110,7 @@ export * from "./utils/access-log/types";
export * from "./utils/access-log/utils"; export * from "./utils/access-log/utils";
export * from "./constants/index"; export * from "./constants/index";
export * from "./monitoring/utilts"; export * from "./monitoring/utils";
export * from "./db/validations/domain"; export * from "./db/validations/domain";
export * from "./db/validations/index"; export * from "./db/validations/index";

View File

@@ -1,10 +1,19 @@
import { promises } from "node:fs"; import { promises } from "node:fs";
import type Dockerode from "dockerode";
import osUtils from "node-os-utils"; import osUtils from "node-os-utils";
import { paths } from "../constants"; import { paths } from "../constants";
export interface Container {
BlockIO: string;
CPUPerc: string;
Container: string;
ID: string;
MemPerc: string;
MemUsage: string;
Name: string;
NetIO: string;
}
export const recordAdvancedStats = async ( export const recordAdvancedStats = async (
stats: Dockerode.ContainerStats, stats: Container,
appName: string, appName: string,
) => { ) => {
const { MONITORING_PATH } = paths(); const { MONITORING_PATH } = paths();
@@ -12,29 +21,20 @@ export const recordAdvancedStats = async (
await promises.mkdir(path, { recursive: true }); await promises.mkdir(path, { recursive: true });
const cpuPercent = calculateCpuUsagePercent( await updateStatsFile(appName, "cpu", stats.CPUPerc);
stats.cpu_stats,
stats.precpu_stats,
);
const memoryStats = calculateMemoryStats(stats.memory_stats);
const blockIO = calculateBlockIO(stats.blkio_stats);
const networkUsage = calculateNetworkUsage(stats.networks);
await updateStatsFile(appName, "cpu", cpuPercent);
await updateStatsFile(appName, "memory", { await updateStatsFile(appName, "memory", {
used: memoryStats.used, used: stats.MemUsage.split(" ")[0],
free: memoryStats.free, total: stats.MemUsage.split(" ")[2],
usedPercentage: memoryStats.usedPercentage,
total: memoryStats.total,
}); });
await updateStatsFile(appName, "block", { await updateStatsFile(appName, "block", {
readMb: blockIO.readMb, readMb: stats.BlockIO.split(" ")[0],
writeMb: blockIO.writeMb, writeMb: stats.BlockIO.split(" ")[2],
}); });
await updateStatsFile(appName, "network", { await updateStatsFile(appName, "network", {
inputMb: networkUsage.inputMb, inputMb: stats.NetIO.split(" ")[0],
outputMb: networkUsage.outputMb, outputMb: stats.NetIO.split(" ")[2],
}); });
if (appName === "dokploy") { if (appName === "dokploy") {
@@ -122,77 +122,3 @@ export const getLastAdvancedStatsFile = async (appName: string) => {
block: await readLastValueStatsFile(appName, "block"), block: await readLastValueStatsFile(appName, "block"),
}; };
}; };
const calculateCpuUsagePercent = (
cpu_stats: Dockerode.ContainerStats["cpu_stats"],
precpu_stats: Dockerode.ContainerStats["precpu_stats"],
) => {
const cpuDelta =
cpu_stats.cpu_usage.total_usage - precpu_stats.cpu_usage.total_usage;
const systemDelta =
cpu_stats.system_cpu_usage - precpu_stats.system_cpu_usage;
const numberCpus =
cpu_stats.online_cpus ||
(cpu_stats.cpu_usage.percpu_usage
? cpu_stats.cpu_usage.percpu_usage.length
: 1);
if (systemDelta > 0 && cpuDelta > 0) {
return (cpuDelta / systemDelta) * numberCpus * 100.0;
}
return 0;
};
const calculateMemoryStats = (
memory_stats: Dockerode.ContainerStats["memory_stats"],
) => {
const usedMemory = memory_stats.usage - (memory_stats.stats.cache || 0);
const availableMemory = memory_stats.limit;
const memoryUsedPercentage = (usedMemory / availableMemory) * 100.0;
return {
used: usedMemory,
free: availableMemory - usedMemory,
usedPercentage: memoryUsedPercentage,
total: availableMemory,
};
};
const calculateBlockIO = (
blkio_stats: Dockerode.ContainerStats["blkio_stats"],
) => {
let readIO = 0;
let writeIO = 0;
if (blkio_stats?.io_service_bytes_recursive) {
for (const io of blkio_stats.io_service_bytes_recursive) {
if (io.op === "read") {
readIO += io.value;
} else if (io.op === "write") {
writeIO += io.value;
}
}
}
return {
readMb: readIO / (1024 * 1024),
writeMb: writeIO / (1024 * 1024),
};
};
const calculateNetworkUsage = (
networks: Dockerode.ContainerStats["networks"],
) => {
let totalRx = 0;
let totalTx = 0;
const stats = Object.keys(networks);
for (const interfaceName of stats) {
const net = networks[interfaceName];
totalRx += net?.rx_bytes || 0;
totalTx += net?.tx_bytes || 0;
}
return {
inputMb: totalRx / (1024 * 1024),
outputMb: totalTx / (1024 * 1024),
};
};

View File

@@ -6,7 +6,7 @@ import {
buildAppName, buildAppName,
cleanAppName, cleanAppName,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { getAdvancedStats } from "@dokploy/server/monitoring/utilts"; import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
import { import {
buildApplication, buildApplication,
getBuildCommand, getBuildCommand,