From 69dac0f673688e7fb36355b21d59022762eaeed2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:51:37 -0600 Subject: [PATCH] feat: add docker monitoring --- .../container/container-block-chart.tsx | 181 +++++++ .../container/container-cpu-chart.tsx | 125 +++++ .../container/container-memory-chart.tsx | 133 ++++++ .../container/container-network-chart.tsx | 183 +++++++ .../dashboard/monitoring/container/show.tsx | 211 ++++++++ .../dashboard/monitoring/docker/show.tsx | 452 +++++++++--------- .../dashboard/monitoring/show-monitoring.tsx | 2 +- .../services/application/[applicationId].tsx | 6 +- apps/monitoring/src/index.ts | 41 +- apps/monitoring/src/monitoring/containers.ts | 47 +- apps/monitoring/src/utils.ts | 36 +- 11 files changed, 1139 insertions(+), 278 deletions(-) create mode 100644 apps/dokploy/components/dashboard/monitoring/container/container-block-chart.tsx create mode 100644 apps/dokploy/components/dashboard/monitoring/container/container-cpu-chart.tsx create mode 100644 apps/dokploy/components/dashboard/monitoring/container/container-memory-chart.tsx create mode 100644 apps/dokploy/components/dashboard/monitoring/container/container-network-chart.tsx create mode 100644 apps/dokploy/components/dashboard/monitoring/container/show.tsx diff --git a/apps/dokploy/components/dashboard/monitoring/container/container-block-chart.tsx b/apps/dokploy/components/dashboard/monitoring/container/container-block-chart.tsx new file mode 100644 index 000000000..70c312219 --- /dev/null +++ b/apps/dokploy/components/dashboard/monitoring/container/container-block-chart.tsx @@ -0,0 +1,181 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, +} from "@/components/ui/chart"; +import { formatTimestamp } from "@/lib/utils"; +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; + +interface ContainerMetric { + timestamp: string; + BlockIO: string; +} + +interface Props { + data: ContainerMetric[]; +} + +const chartConfig = { + read: { + label: "Read", + color: "hsl(var(--chart-5))", + }, + write: { + label: "Write", + color: "hsl(var(--chart-6))", + }, +} satisfies ChartConfig; + +const parseBlockIO = (blockIO: string) => { + const [read, write] = blockIO.split(" / "); + return { + read: Number.parseFloat(read), + write: parseFloat(write), + readUnit: read?.replace(/[\d.]/g, ""), + writeUnit: write?.replace(/[\d.]/g, ""), + }; +}; + +export const ContainerBlockChart = ({ data }: Props) => { + const formattedData = data.map((metric) => { + const { read, write, readUnit, writeUnit } = parseBlockIO(metric.BlockIO); + return { + timestamp: metric.timestamp, + read, + write, + readUnit, + writeUnit, + }; + }); + + const latestData = formattedData[formattedData.length - 1] || {}; + + return ( + + + Block I/O + + Read: {latestData.read} + {latestData.readUnit} / Write: {latestData.write} + {latestData.writeUnit} + + + + + + + + + + + + + + + + + formatTimestamp(value)} + /> + + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+
+
+ + Time + + + {formatTimestamp(label)} + +
+
+ + Read + + + {data.read} + {data.readUnit} + +
+
+ + Write + + + {data.write} + {data.writeUnit} + +
+
+
+ ); + } + return null; + }} + /> + + + } + verticalAlign="bottom" + align="center" + /> +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/monitoring/container/container-cpu-chart.tsx b/apps/dokploy/components/dashboard/monitoring/container/container-cpu-chart.tsx new file mode 100644 index 000000000..656313057 --- /dev/null +++ b/apps/dokploy/components/dashboard/monitoring/container/container-cpu-chart.tsx @@ -0,0 +1,125 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, +} from "@/components/ui/chart"; +import { formatTimestamp } from "@/lib/utils"; +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; + +interface ContainerMetric { + timestamp: string; + CPUPerc: string; +} + +interface Props { + data: ContainerMetric[]; +} + +const chartConfig = { + cpu: { + label: "CPU", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig; + +export const ContainerCPUChart = ({ data }: Props) => { + const formattedData = data.map((metric) => ({ + timestamp: metric.timestamp, + cpu: parseFloat(metric.CPUPerc.replace("%", "")), + })); + + const latestData = formattedData[formattedData.length - 1] || {}; + + return ( + + + CPU + CPU Usage: {latestData.cpu}% + + + + + + + + + + + + formatTimestamp(value)} + /> + `${value}%`} domain={[0, 100]} /> + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+
+
+ + Time + + + {formatTimestamp(label)} + +
+
+ + CPU + + {data.cpu}% +
+
+
+ ); + } + return null; + }} + /> + + } + verticalAlign="bottom" + align="center" + /> +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/monitoring/container/container-memory-chart.tsx b/apps/dokploy/components/dashboard/monitoring/container/container-memory-chart.tsx new file mode 100644 index 000000000..1d59641f1 --- /dev/null +++ b/apps/dokploy/components/dashboard/monitoring/container/container-memory-chart.tsx @@ -0,0 +1,133 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, +} from "@/components/ui/chart"; +import { formatTimestamp } from "@/lib/utils"; +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; + +interface ContainerMetric { + timestamp: string; + MemPerc: string; + MemUsage: string; +} + +interface Props { + data: ContainerMetric[]; +} + +const chartConfig = { + memory: { + label: "Memory", + color: "hsl(var(--chart-2))", + }, +} satisfies ChartConfig; + +export const ContainerMemoryChart = ({ data }: Props) => { + const formattedData = data.map((metric) => ({ + timestamp: metric.timestamp, + memory: parseFloat(metric.MemPerc.replace("%", "")), + usage: metric.MemUsage, + })); + + const latestData = formattedData[formattedData.length - 1] || {}; + + return ( + + + Memory + Memory Usage: {latestData.usage} + + + + + + + + + + + + formatTimestamp(value)} + /> + `${value}%`} domain={[0, 100]} /> + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+
+
+ + Time + + + {formatTimestamp(label)} + +
+
+ + Memory + + {data.memory}% +
+
+ + Usage + + {data.usage} +
+
+
+ ); + } + return null; + }} + /> + + } + verticalAlign="bottom" + align="center" + /> +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/monitoring/container/container-network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/container/container-network-chart.tsx new file mode 100644 index 000000000..42b697cc8 --- /dev/null +++ b/apps/dokploy/components/dashboard/monitoring/container/container-network-chart.tsx @@ -0,0 +1,183 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, +} from "@/components/ui/chart"; +import { formatTimestamp } from "@/lib/utils"; +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; + +interface ContainerMetric { + timestamp: string; + NetIO: string; +} + +interface Props { + data: ContainerMetric[]; +} + +const chartConfig = { + input: { + label: "Input", + color: "hsl(var(--chart-3))", + }, + output: { + label: "Output", + color: "hsl(var(--chart-4))", + }, +} satisfies ChartConfig; + +const parseNetworkIO = (netIO: string) => { + const [input, output] = netIO.split(" / "); + return { + input: parseFloat(input), + output: parseFloat(output), + inputUnit: input.replace(/[\d.]/g, ""), + outputUnit: output.replace(/[\d.]/g, ""), + }; +}; + +export const ContainerNetworkChart = ({ data }: Props) => { + const formattedData = data.map((metric) => { + const { input, output, inputUnit, outputUnit } = parseNetworkIO( + metric.NetIO, + ); + return { + timestamp: metric.timestamp, + input, + output, + inputUnit, + outputUnit, + }; + }); + + const latestData = formattedData[formattedData.length - 1] || {}; + + return ( + + + Network I/O + + Input: {latestData.input} + {latestData.inputUnit} / Output: {latestData.output} + {latestData.outputUnit} + + + + + + + + + + + + + + + + + formatTimestamp(value)} + /> + + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+
+
+ + Time + + + {formatTimestamp(label)} + +
+
+ + Input + + + {data.input} + {data.inputUnit} + +
+
+ + Output + + + {data.output} + {data.outputUnit} + +
+
+
+ ); + } + return null; + }} + /> + + + } + verticalAlign="bottom" + align="center" + /> +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/monitoring/container/show.tsx b/apps/dokploy/components/dashboard/monitoring/container/show.tsx new file mode 100644 index 000000000..26e0f05f3 --- /dev/null +++ b/apps/dokploy/components/dashboard/monitoring/container/show.tsx @@ -0,0 +1,211 @@ +import { Card } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Cpu, HardDrive, Loader2, MemoryStick, Network } from "lucide-react"; +import { useEffect, useState } from "react"; +import { ContainerBlockChart } from "./container-block-chart"; +import { ContainerCPUChart } from "./container-cpu-chart"; +import { ContainerMemoryChart } from "./container-memory-chart"; +import { ContainerNetworkChart } from "./container-network-chart"; + +const REFRESH_INTERVAL = 4500; + +const DATA_POINTS_OPTIONS = { + "50": "50 points", + "200": "200 points", + "500": "500 points", + "800": "800 points", + all: "All points", +} as const; + +interface ContainerMetric { + timestamp: string; + BlockIO: string; + CPUPerc: string; + Container: string; + ID: string; + MemPerc: string; + MemUsage: string; + Name: string; + NetIO: string; + PIDs: string; +} + +interface Props { + appName: string; + BASE_URL: string; +} + +export const ContainerMonitoring = ({ appName, BASE_URL }: Props) => { + const [historicalData, setHistoricalData] = useState([]); + const [metrics, setMetrics] = useState( + {} as ContainerMetric, + ); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [dataPoints, setDataPoints] = + useState("50"); + + const fetchMetrics = async () => { + try { + const url = new URL(`${BASE_URL}/metrics/containers`); + + if (dataPoints !== "all") { + url.searchParams.append("limit", dataPoints); + } + + url.searchParams.append("appName", appName); + + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error(`Failed to fetch metrics: ${response.statusText}`); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error("No hay datos disponibles"); + } + + setHistoricalData(data); + setMetrics(data[data.length - 1]); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch metrics"); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchMetrics(); + + const interval = setInterval(() => { + fetchMetrics(); + }, REFRESH_INTERVAL); + + return () => clearInterval(interval); + }, [dataPoints, appName]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ + Error fetching metrics from: {BASE_URL}{" "} + {error} + +
+ ); + } + + return ( +
+ {/* Header con selector de puntos de datos */} +
+

+ Container Monitoring +

+
+ Data points: + +
+
+ + {/* Stats Cards */} +
+ +
+ +

CPU Usage

+
+

{metrics.CPUPerc}

+
+ + +
+ +

Memory Usage

+
+

{metrics.MemPerc}

+

+ {metrics.MemUsage} +

+
+ + +
+ +

Network I/O

+
+

{metrics.NetIO}

+
+ + +
+ +

Block I/O

+
+

{metrics.BlockIO}

+
+
+ + {/* Container Information */} + +

Container Information

+
+
+

+ Container ID +

+

{metrics.ID}

+
+
+

Name

+

{metrics.Name}

+
+
+

PIDs

+

{metrics.PIDs}

+
+
+
+ + {/* Charts Grid */} +
+ + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/monitoring/docker/show.tsx b/apps/dokploy/components/dashboard/monitoring/docker/show.tsx index d9deaa351..c1a379d63 100644 --- a/apps/dokploy/components/dashboard/monitoring/docker/show.tsx +++ b/apps/dokploy/components/dashboard/monitoring/docker/show.tsx @@ -13,256 +13,232 @@ import { DockerCpuChart } from "./docker-cpu-chart"; import { DockerDiskChart } from "./docker-disk-chart"; import { DockerMemoryChart } from "./docker-memory-chart"; import { DockerNetworkChart } from "./docker-network-chart"; - -const defaultData = { - cpu: { - value: 0, - time: "", - }, - memory: { - value: { - used: 0, - free: 0, - usedPercentage: 0, - total: 0, - }, - time: "", - }, - block: { - value: { - readMb: 0, - writeMb: 0, - }, - time: "", - }, - network: { - value: { - inputMb: 0, - outputMb: 0, - }, - time: "", - }, - disk: { - value: { diskTotal: 0, diskUsage: 0, diskUsedPercentage: 0, diskFree: 0 }, - time: "", - }, -}; - +import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { CPUChart } from "@/components/metrics/cpu-chart"; +import { DiskChart } from "@/components/metrics/disk-chart"; +import { MemoryChart } from "@/components/metrics/memory-chart"; +import { NetworkChart } from "@/components/metrics/network-chart"; + +const REFRESH_INTERVAL = 4500; +// const BASE_URL = +// process.env.NEXT_PUBLIC_METRICS_URL || "http://localhost:3001/metrics"; + +const DATA_POINTS_OPTIONS = { + "50": "50 points", + "200": "200 points", + "500": "500 points", + "800": "800 points", + all: "All points", +} as const; interface Props { appName: string; - appType?: "application" | "stack" | "docker-compose"; + BASE_URL: string; } -export interface DockerStats { - cpu: { - value: number; - time: string; - }; - memory: { - value: { - used: number; - free: number; - usedPercentage: number; - total: number; - }; - time: string; - }; - block: { - value: { - readMb: number; - writeMb: number; - }; - time: string; - }; - network: { - value: { - inputMb: number; - outputMb: number; - }; - time: string; - }; - disk: { - value: { - diskTotal: number; - diskUsage: number; - diskUsedPercentage: number; - diskFree: number; - }; - time: string; - }; -} - -export type DockerStatsJSON = { - cpu: DockerStats["cpu"][]; - memory: DockerStats["memory"][]; - block: DockerStats["block"][]; - network: DockerStats["network"][]; - disk: DockerStats["disk"][]; -}; - -export const DockerMonitoring = ({ - appName, - appType = "application", -}: Props) => { - const { data } = api.application.readAppMonitoring.useQuery( - { appName }, - { - refetchOnWindowFocus: false, - }, - ); - const [acummulativeData, setAcummulativeData] = useState({ - cpu: [], - memory: [], - block: [], - network: [], - disk: [], - }); - const [currentData, setCurrentData] = useState(defaultData); - - useEffect(() => { - setCurrentData(defaultData); +export const DockerMonitoring = ({ appName, BASE_URL }: Props) => { + const [historicalData, setHistoricalData] = useState([]); + const [metrics, setMetrics] = useState({} as SystemMetrics); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [dataPoints, setDataPoints] = + useState("50"); + + const fetchMetrics = async () => { + try { + const url = new URL("http://localhost:3001/metrics/containers"); + + // Solo añadir el parámetro limit si no es "all" + if (dataPoints !== "all") { + url.searchParams.append("limit", dataPoints); + } + + url.searchParams.append("appName", appName); + + console.log("url", url.toString()); + + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error(`Failed to fetch metrics: ${response.statusText}`); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error("No hay datos disponibles"); + } + + const formattedData = data.map((metric: SystemMetrics) => ({ + timestamp: metric.timestamp, + cpu: Number.parseFloat(metric.cpu), + cpuModel: metric.cpuModel, + cpuCores: metric.cpuCores, + cpuPhysicalCores: metric.cpuPhysicalCores, + cpuSpeed: metric.cpuSpeed, + os: metric.os, + distro: metric.distro, + kernel: metric.kernel, + arch: metric.arch, + memUsed: Number.parseFloat(metric.memUsed), + memUsedGB: Number.parseFloat(metric.memUsedGB), + memTotal: Number.parseFloat(metric.memTotal), + networkIn: Number.parseFloat(metric.networkIn), + networkOut: Number.parseFloat(metric.networkOut), + diskUsed: Number.parseFloat(metric.diskUsed), + totalDisk: Number.parseFloat(metric.totalDisk), + uptime: metric.uptime, + })); - setAcummulativeData({ - cpu: [], - memory: [], - block: [], - network: [], - disk: [], - }); - }, [appName]); + setHistoricalData(formattedData); + setMetrics(formattedData[formattedData.length - 1] || {}); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch metrics"); + } finally { + setIsLoading(false); + } + }; - useEffect(() => { - if (!data) return; + const formatUptime = (seconds: number): string => { + const days = Math.floor(seconds / (24 * 60 * 60)); + const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60)); + const minutes = Math.floor((seconds % (60 * 60)) / 60); - setCurrentData({ - cpu: data.cpu[data.cpu.length - 1] ?? currentData.cpu, - memory: data.memory[data.memory.length - 1] ?? currentData.memory, - block: data.block[data.block.length - 1] ?? currentData.block, - network: data.network[data.network.length - 1] ?? currentData.network, - disk: data.disk[data.disk.length - 1] ?? currentData.disk, - }); - setAcummulativeData({ - block: data?.block || [], - cpu: data?.cpu || [], - disk: data?.disk || [], - memory: data?.memory || [], - network: data?.network || [], - }); - }, [data]); + return `${days}d ${hours}h ${minutes}m`; + }; useEffect(() => { - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/listen-docker-stats-monitoring?appName=${appName}&appType=${appType}`; - const ws = new WebSocket(wsUrl); - - ws.onmessage = (e) => { - const value = JSON.parse(e.data); - if (!value) return; - - const data = { - cpu: value.data.cpu ?? currentData.cpu, - memory: value.data.memory ?? currentData.memory, - block: value.data.block ?? currentData.block, - disk: value.data.disk ?? currentData.disk, - network: value.data.network ?? currentData.network, - }; - - setCurrentData(data); - - setAcummulativeData((prevData) => ({ - cpu: [...prevData.cpu, data.cpu], - memory: [...prevData.memory, data.memory], - block: [...prevData.block, data.block], - network: [...prevData.network, data.network], - disk: [...prevData.disk, data.disk], - })); - }; - - ws.onclose = (e) => { - console.log(e.reason); - }; - - return () => ws.close(); - }, [appName]); + fetchMetrics(); + + const interval = setInterval(() => { + fetchMetrics(); + }, REFRESH_INTERVAL); + + return () => clearInterval(interval); + }, [dataPoints]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ + Error fetching metrics to: {BASE_URL} + {error} + +
+ ); + } return ( -
- - - Monitoring - - Watch the usage of your server in the current app. - - - -
-
-
- CPU - - Used: {currentData.cpu.value.toFixed(2)}% - - - -
-
- Memory - - {`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`} - - - -
- {appName === "dokploy" && ( -
- Space - - {`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`} - - - -
- )} -
- Block I/O - - {`Read: ${currentData.block.value.readMb.toFixed( - 2, - )} MB / Write: ${currentData.block.value.writeMb.toFixed( - 3, - )} MB`} - - -
-
- Network - - {`In MB: ${currentData.network.value.inputMb.toFixed( - 2, - )} MB / Out MB: ${currentData.network.value.outputMb.toFixed( - 2, - )} MB`} - - -
-
+
+ {/* Header con selector de puntos de datos */} +
+

System Monitoring

+
+ Data points: + +
+
+ + {/* Stats Cards */} +
+
+
+ +

Uptime

+
+

+ {formatUptime(metrics.uptime || 0)} +

+
+ +
+
+ +

CPU Usage

+
+

{metrics.cpu}%

+
+ +
+
+ +

Memory Usage

+
+

+ {metrics.memUsedGB} GB / {metrics.memTotal} GB +

+
+ +
+
+ +

Disk Usage

+
+

{metrics.diskUsed}%

+
+
+ + {/* System Information */} +
+

System Information

+
+
+

CPU

+

{metrics.cpuModel}

+

+ {metrics.cpuPhysicalCores} Physical Cores ({metrics.cpuCores}{" "} + Threads) @ {metrics.cpuSpeed}GHz +

+
+
+

+ Operating System +

+

{metrics.distro}

+

+ Kernel: {metrics.kernel} ({metrics.arch}) +

- - +
+
+ + {/* Charts Grid */} +
+ + + + +
); }; diff --git a/apps/dokploy/components/dashboard/monitoring/show-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/show-monitoring.tsx index b9be9a1b2..6f248376e 100644 --- a/apps/dokploy/components/dashboard/monitoring/show-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/show-monitoring.tsx @@ -81,7 +81,7 @@ export const ShowMonitoring = ({ const data = await response.json(); if (!Array.isArray(data) || data.length === 0) { - throw new Error("No hay datos disponibles"); + throw new Error("No data available"); } const formattedData = data.map((metric: SystemMetrics) => ({ diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index c8ffd0367..3eceb3f62 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -14,6 +14,7 @@ import { ShowGeneralApplication } from "@/components/dashboard/application/gener import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments"; import { UpdateApplication } from "@/components/dashboard/application/update-application"; +import { ContainerMonitoring } from "@/components/dashboard/monitoring/container/show"; import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show"; import { ProjectLayout } from "@/components/layouts/project-layout"; import { StatusTooltip } from "@/components/shared/status-tooltip"; @@ -244,7 +245,10 @@ const Service = ( {!data?.serverId && (
- +
)} diff --git a/apps/monitoring/src/index.ts b/apps/monitoring/src/index.ts index 2229b0714..215189695 100644 --- a/apps/monitoring/src/index.ts +++ b/apps/monitoring/src/index.ts @@ -3,9 +3,10 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import { logServerMetrics } from "./monitoring/server.js"; import { config } from "dotenv"; -import { serverLogFile } from "./constants.js"; +import { serverLogFile, containerLogFile } from "./constants.js"; import { processMetricsFromFile } from "./utils.js"; import { logContainerMetrics } from "./monitoring/containers.js"; +import { existsSync } from "fs"; config(); const TOKEN = process.env.TOKEN || "default-token"; @@ -63,20 +64,30 @@ app.get("/metrics", async (c) => { } }); -// app.get("/metrics/containers", async (c) => { -// try { -// const metrics = await processMetricsFromFile(containerLogFile, { -// start: c.req.query("start"), -// end: c.req.query("end"), -// limit: Number(c.req.query("limit")) || undefined, -// }); - -// return c.json(metrics); -// } catch (error) { -// console.error("Error reading metrics:", error); -// return c.json({ error: "Error reading metrics" }, 500); -// } -// }); +app.get("/metrics/containers", async (c) => { + try { + const appName = c.req.query("appName"); + if (!appName) { + return c.json({ error: "No appName provided" }, 400); + } + + const logPath = `${containerLogFile}/${appName}.log`; + + if (existsSync(logPath) === false) { + return c.json([]); + } + const metrics = await processMetricsFromFile(logPath, { + start: c.req.query("start"), + end: c.req.query("end"), + limit: Number(c.req.query("limit")) || undefined, + }); + + return c.json(metrics); + } catch (error) { + console.error("Error reading metrics:", error); + return c.json({ error: "Error reading metrics" }, 500); + } +}); app.get("/health", (c) => { return c.text("OK"); diff --git a/apps/monitoring/src/monitoring/containers.ts b/apps/monitoring/src/monitoring/containers.ts index f0eb348bc..c97d0ee52 100644 --- a/apps/monitoring/src/monitoring/containers.ts +++ b/apps/monitoring/src/monitoring/containers.ts @@ -38,6 +38,21 @@ function getServiceName(containerName: string): string { export const logContainerMetrics = () => { console.log("Initialized container metrics"); + + // Mantener un handle del archivo abierto para cada contenedor + const fileHandles = new Map(); + + const cleanup = async () => { + for (const [_, handle] of fileHandles) { + await handle.close(); + } + fileHandles.clear(); + }; + + // Asegurar que cerramos los archivos al terminar + process.on("SIGTERM", cleanup); + process.on("SIGINT", cleanup); + setInterval(async () => { try { const { stdout } = await execAsync( @@ -52,20 +67,34 @@ export const logContainerMetrics = () => { } for (const container of containers) { - const serviceName = getServiceName(container.Name); - const logLine = `${JSON.stringify({ - timestamp: new Date().toISOString(), - ...container, - })}\n`; + try { + const serviceName = getServiceName(container.Name); + const containerPath = join(containerLogFile, `${serviceName}.log`); + + // Obtener o crear el handle del archivo + let fileHandle = fileHandles.get(serviceName); + if (!fileHandle) { + fileHandle = await fs.promises.open(containerPath, "a"); + fileHandles.set(serviceName, fileHandle); + } - const containerPath = join(containerLogFile, `${serviceName}.log`); + const logLine = `${JSON.stringify({ + timestamp: new Date().toISOString(), + ...container, + })}\n`; - fs.appendFile(containerPath, logLine, (err) => { - if (err) console.error("Error writing container metrics:", err); - }); + await fileHandle.write(logLine); + } catch (error) { + console.error( + `Error writing metrics for container ${container.Name}:`, + error, + ); + } } } catch (error) { console.error("Error getting containers:", error); } }, REFRESH_RATE_CONTAINER); + + return cleanup; }; diff --git a/apps/monitoring/src/utils.ts b/apps/monitoring/src/utils.ts index 4596512e6..ebf0414b9 100644 --- a/apps/monitoring/src/utils.ts +++ b/apps/monitoring/src/utils.ts @@ -15,13 +15,17 @@ export function parseLog(logContent: string) { if (!logContent.trim()) return []; const lines = logContent.trim().split("\n"); - return lines.map((line) => { - try { - return JSON.parse(line); - } catch { - return { raw: line }; - } - }); + return lines + .map((line) => { + try { + return JSON.parse(line); + } catch (error) { + console.error(`Error parsing log line: ${error}`); + console.error(`Problematic line: ${line}`); + return null; + } + }) + .filter((entry): entry is Record => entry !== null); } /** @@ -92,13 +96,17 @@ async function readLastNLines(filePath: string, limit: number) { const lines = content.split("\n").filter((line) => line.trim()); const lastLines = lines.slice(-limit); - return lastLines.map((line) => { - try { - return JSON.parse(line); - } catch { - return { raw: line }; - } - }); + return lastLines + .map((line) => { + try { + return JSON.parse(line); + } catch (error) { + console.error(`Error parsing log line: ${error}`); + console.error(`Problematic line: ${line}`); + return null; + } + }) + .filter((entry): entry is Record => entry !== null); } finally { await fd.close(); }