From d492ff87f261062ed608ac6243a11965e6537cfd Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 2 Feb 2025 19:35:36 -0600 Subject: [PATCH] Fix/monitoring (#1270) * refactor: make request to dokploy server to proxy requests * refactor: lint * refactor: use dokploy/monitoring tag image --- .github/workflows/deploy.yml | 2 +- .../show-paid-container-monitoring.tsx | 115 +++++----------- .../paid/servers/show-paid-monitoring.tsx | 127 +++++++----------- .../services/application/[applicationId].tsx | 2 + .../services/compose/[composeId].tsx | 2 +- .../services/mariadb/[mariadbId].tsx | 2 +- .../[projectId]/services/mongo/[mongoId].tsx | 2 +- .../[projectId]/services/mysql/[mysqlId].tsx | 2 +- .../services/postgres/[postgresId].tsx | 2 +- .../[projectId]/services/redis/[redisId].tsx | 2 +- apps/dokploy/server/api/routers/admin.ts | 118 ++++++++++++++++ packages/server/src/setup/monitoring-setup.ts | 4 +- 12 files changed, 212 insertions(+), 168 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f5826da4..86bf0628 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -98,5 +98,5 @@ jobs: file: ./Dockerfile.monitoring push: true tags: | - siumauricio/monitoring:${{ github.ref_name == 'main' && 'latest' || 'canary' }} + dokploy/monitoring:${{ github.ref_name == 'main' && 'latest' || 'canary' }} platforms: linux/amd64 diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx index c49be0c4..f45c7382 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx @@ -6,6 +6,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { api } from "@/utils/api"; import { Cpu, HardDrive, Loader2, MemoryStick, Network } from "lucide-react"; import { useEffect, useState } from "react"; import { ContainerBlockChart } from "./container-block-chart"; @@ -70,84 +71,36 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { const [metrics, setMetrics] = useState( {} as ContainerMetric, ); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); const [dataPoints, setDataPoints] = useState("50"); const [refreshInterval, setRefreshInterval] = useState("5000"); - const fetchMetrics = async () => { - try { - const url = new URL(`${baseUrl}/metrics/containers`); - - // if (dataPoints !== "all") { - url.searchParams.append("limit", dataPoints); - // } - - if (!appName) { - throw new Error( - [ - "No Application Selected:", - "", - "Make Sure to select an application to monitor.", - ].join("\n"), - ); - } - - url.searchParams.append("appName", appName); - - const response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (!response.ok) { - throw new Error( - `Error ${response.status}: ${response.statusText}. Please verify that the application "${appName}" is running and this service is included in the monitoring configuration.`, - ); - } - - const data = await response.json(); - if (!Array.isArray(data) || data.length === 0) { - throw new Error( - [ - `No monitoring data available for "${appName}". This could be because:`, - "", - "1. The container was recently started - wait a few minutes for data to be collected", - "2. The container is not running - verify its status", - "3. The service is not included in your monitoring configuration", - ].join("\n"), - ); - } - - setHistoricalData(data); - setMetrics(data[data.length - 1]); - setError(null); - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly.", - ); - } finally { - setIsLoading(false); - } - }; + const { + data, + isLoading, + error: queryError, + } = api.admin.getContainerMetrics.useQuery( + { + url: baseUrl, + token, + dataPoints, + appName, + }, + { + refetchInterval: + dataPoints === "all" ? undefined : Number.parseInt(refreshInterval), + enabled: !!appName, + }, + ); useEffect(() => { - fetchMetrics(); + if (!data) return; - if (dataPoints === "all") { - return; - } - - const interval = setInterval(() => { - fetchMetrics(); - }, Number(refreshInterval)); - - return () => clearInterval(interval); - }, [dataPoints, appName, token, refreshInterval]); + // @ts-ignore + setHistoricalData(data); + // @ts-ignore + setMetrics(data[data.length - 1]); + }, [data]); if (isLoading) { return ( @@ -157,7 +110,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { ); } - if (error) { + if (queryError) { return (
@@ -166,7 +119,9 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { {appName}

- {error} + {queryError instanceof Error + ? queryError.message + : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}

URL: {baseUrl}

@@ -238,11 +193,11 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Memory Usage

- {metrics.Memory.percentage}% + {metrics?.Memory?.percentage}%

- {metrics.Memory.used} {metrics.Memory.unit} / {metrics.Memory.total}{" "} - {metrics.Memory.unit} + {metrics?.Memory?.used} {metrics?.Memory?.unit} /{" "} + {metrics?.Memory?.total} {metrics?.Memory?.unit}

@@ -252,8 +207,8 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Network I/O

- {metrics.Network.input} {metrics.Network.inputUnit} /{" "} - {metrics.Network.output} {metrics.Network.outputUnit} + {metrics?.Network?.input} {metrics?.Network?.inputUnit} /{" "} + {metrics?.Network?.output} {metrics?.Network?.outputUnit}

@@ -263,8 +218,8 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Block I/O

- {metrics.BlockIO.read} {metrics.BlockIO.readUnit} /{" "} - {metrics.BlockIO.write} {metrics.BlockIO.writeUnit} + {metrics?.BlockIO?.read} {metrics?.BlockIO?.readUnit} /{" "} + {metrics?.BlockIO?.write} {metrics?.BlockIO?.writeUnit}

diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx index 90148dfa..8f09ab12 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx @@ -5,6 +5,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { api } from "@/utils/api"; import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react"; import type React from "react"; import { useEffect, useState } from "react"; @@ -64,76 +65,56 @@ export const ShowPaidMonitoring = ({ }: 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 [refreshInterval, setRefreshInterval] = useState("5000"); - const fetchMetrics = async () => { - try { - const url = new URL(BASE_URL); - url.searchParams.append("limit", dataPoints); - const response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const { + data, + isLoading, + error: queryError, + } = api.admin.getServerMetrics.useQuery( + { + url: BASE_URL, + token, + dataPoints, + }, + { + refetchInterval: + dataPoints === "all" ? undefined : Number.parseInt(refreshInterval), + enabled: true, + }, + ); - if (!response.ok) { - throw new Error( - `Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`, - ); - } + useEffect(() => { + if (!data) return; - const data = await response.json(); - if (!Array.isArray(data) || data.length === 0) { - throw new Error( - [ - "No monitoring data available. This could be because:", - "", - "1. You don't have setup the monitoring service, you can do in web server section.", - "2. If you already have setup the monitoring service, wait a few minutes and refresh the page.", - ].join("\n"), - ); - } + 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, + })); - 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, - })); - - // @ts-ignore - setHistoricalData(formattedData); - // @ts-ignore - setMetrics(formattedData[formattedData.length - 1] || {}); - setError(null); - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly.", - ); - } finally { - setIsLoading(false); - } - }; + // @ts-ignore + setHistoricalData(formattedData); + // @ts-ignore + setMetrics(formattedData[formattedData.length - 1] || {}); + }, [data]); const formatUptime = (seconds: number): string => { const days = Math.floor(seconds / (24 * 60 * 60)); @@ -143,20 +124,6 @@ export const ShowPaidMonitoring = ({ return `${days}d ${hours}h ${minutes}m`; }; - useEffect(() => { - fetchMetrics(); - - if (dataPoints === "all") { - return; - } - - const interval = setInterval(() => { - fetchMetrics(); - }, Number(refreshInterval)); - - return () => clearInterval(interval); - }, [dataPoints, token, refreshInterval]); - if (isLoading) { return (
@@ -165,7 +132,7 @@ export const ShowPaidMonitoring = ({ ); } - if (error) { + if (queryError) { return (
@@ -173,7 +140,9 @@ export const ShowPaidMonitoring = ({ Error fetching metrics{" "}

- {error} + {queryError instanceof Error + ? queryError.message + : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}

URL: {BASE_URL}

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 781854f8..9d3ffe4f 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -399,6 +399,8 @@ export async function getServerSideProps( applicationId: params?.applicationId, }); + await helpers.settings.isCloud.prefetch(); + return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index b9ed1d8c..c6840214 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -394,7 +394,7 @@ export async function getServerSideProps( await helpers.compose.one.fetch({ composeId: params?.composeId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx index d441b08c..a2ee9051 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx @@ -343,7 +343,7 @@ export async function getServerSideProps( await helpers.mariadb.one.fetch({ mariadbId: params?.mariadbId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx index 7fdb1e77..4f3947c2 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx @@ -345,7 +345,7 @@ export async function getServerSideProps( await helpers.mongo.one.fetch({ mongoId: params?.mongoId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx index ba4efecc..baf7e6f8 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx @@ -350,7 +350,7 @@ export async function getServerSideProps( await helpers.mysql.one.fetch({ mysqlId: params?.mysqlId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx index 047d1c57..e3fd8b44 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx @@ -346,7 +346,7 @@ export async function getServerSideProps( await helpers.postgres.one.fetch({ postgresId: params?.postgresId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx index 1b041640..3421e759 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx @@ -337,7 +337,7 @@ export async function getServerSideProps( await helpers.redis.one.fetch({ redisId: params?.redisId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/server/api/routers/admin.ts b/apps/dokploy/server/api/routers/admin.ts index f10bc759..31612fe0 100644 --- a/apps/dokploy/server/api/routers/admin.ts +++ b/apps/dokploy/server/api/routers/admin.ts @@ -22,6 +22,7 @@ import { } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { z } from "zod"; import { adminProcedure, createTRPCRouter, @@ -169,4 +170,121 @@ export const adminRouter = createTRPCRouter({ metricsConfig: admin?.metricsConfig, }; }), + + getServerMetrics: protectedProcedure + .input( + z.object({ + url: z.string(), + token: z.string(), + dataPoints: z.string(), + }), + ) + .query(async ({ ctx, input }) => { + try { + const url = new URL(input.url); + url.searchParams.append("limit", input.dataPoints); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${input.token}`, + }, + }); + if (!response.ok) { + throw new Error( + `Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`, + ); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error( + [ + "No monitoring data available. This could be because:", + "", + "1. You don't have setup the monitoring service, you can do in web server section.", + "2. If you already have setup the monitoring service, wait a few minutes and refresh the page.", + ].join("\n"), + ); + } + return data as { + cpu: string; + cpuModel: string; + cpuCores: number; + cpuPhysicalCores: number; + cpuSpeed: number; + os: string; + distro: string; + kernel: string; + arch: string; + memUsed: string; + memUsedGB: string; + memTotal: string; + uptime: number; + diskUsed: string; + totalDisk: string; + networkIn: string; + networkOut: string; + timestamp: string; + }[]; + } catch (error) { + throw error; + } + }), + getContainerMetrics: protectedProcedure + .input( + z.object({ + url: z.string(), + token: z.string(), + appName: z.string(), + dataPoints: z.string(), + }), + ) + .query(async ({ ctx, input }) => { + try { + if (!input.appName) { + throw new Error( + [ + "No Application Selected:", + "", + "Make Sure to select an application to monitor.", + ].join("\n"), + ); + } + const url = new URL(`${input.url}/metrics/containers`); + url.searchParams.append("limit", input.dataPoints); + url.searchParams.append("appName", input.appName); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${input.token}`, + }, + }); + if (!response.ok) { + throw new Error( + `Error ${response.status}: ${response.statusText}. Please verify that the application "${input.appName}" is running and this service is included in the monitoring configuration.`, + ); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error( + [ + `No monitoring data available for "${input.appName}". This could be because:`, + "", + "1. The container was recently started - wait a few minutes for data to be collected", + "2. The container is not running - verify its status", + "3. The service is not included in your monitoring configuration", + ].join("\n"), + ); + } + return data as { + containerId: string; + containerName: string; + containerImage: string; + containerLabels: string; + containerCommand: string; + containerCreated: string; + }[]; + } catch (error) { + throw error; + } + }), }); diff --git a/packages/server/src/setup/monitoring-setup.ts b/packages/server/src/setup/monitoring-setup.ts index e7cd941d..bf50cdb9 100644 --- a/packages/server/src/setup/monitoring-setup.ts +++ b/packages/server/src/setup/monitoring-setup.ts @@ -9,7 +9,7 @@ export const setupMonitoring = async (serverId: string) => { const server = await findServerById(serverId); const containerName = "mauricio-monitoring"; - const imageName = "siumauricio/monitoring:canary"; + const imageName = "dokploy/monitoring:canary"; const settings: ContainerCreateOptions = { name: containerName, @@ -74,7 +74,7 @@ export const setupWebMonitoring = async (adminId: string) => { const admin = await findAdminById(adminId); const containerName = "mauricio-monitoring"; - const imageName = "siumauricio/monitoring:canary"; + const imageName = "dokploy/monitoring:canary"; const settings: ContainerCreateOptions = { name: containerName,