diff --git a/apps/monitoring/__test__/containers.test.ts b/apps/monitoring/__test__/containers.test.ts new file mode 100644 index 000000000..62adf0149 --- /dev/null +++ b/apps/monitoring/__test__/containers.test.ts @@ -0,0 +1,178 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { + shouldMonitorContainer, + getContainerConfig, +} from "../src/containers/config"; + +describe("Container monitoring", () => { + beforeEach(() => { + // Reset process.env before each test + process.env.CONTAINER_MONITORING_CONFIG = ""; + }); + + describe("shouldMonitorContainer", () => { + it("should match exact container names", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [{ appName: "dokploy-postgres", maxFileSizeMB: 15 }], + excludeServices: [], + }); + + expect( + shouldMonitorContainer("dokploy-postgres.1.2rfdhwsjhm82wai9hm9dp4sqn"), + ).toBe(true); + expect(shouldMonitorContainer("testing-postgres-123")).toBe(false); + }); + + it("should handle multiple postgres instances correctly", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [{ appName: "postgres", maxFileSizeMB: 15 }], + excludeServices: [], + }); + + // Este test demuestra el bug actual: + // Al usar 'postgres' como appName, coincide con cualquier contenedor que tenga 'postgres' en su nombre + expect( + shouldMonitorContainer("dokploy-postgres.1.2rfdhwsjhm82wai9hm9dp4sqn"), + ).toBe(false); // debería ser false + expect( + shouldMonitorContainer("testing-plausible-9cc9fd-plausible_db-1"), + ).toBe(false); + expect( + shouldMonitorContainer( + "testing-fdghdfgh-pukqqe.1.31mn5ve9qs2xmdadahwtl659r", + ), + ).toBe(false); + }); + + it("should handle compose project monitoring", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [ + { appName: "testing-plausible-9cc9fd", maxFileSizeMB: 15 }, + ], + excludeServices: [], + }); + + // Debería coincidir con todos los contenedores del proyecto compose + expect( + shouldMonitorContainer("testing-plausible-9cc9fd-plausible-1"), + ).toBe(true); + expect( + shouldMonitorContainer("testing-plausible-9cc9fd-plausible_db-1"), + ).toBe(true); + expect( + shouldMonitorContainer( + "testing-plausible-9cc9fd-plausible_events_db-1", + ), + ).toBe(true); + expect( + shouldMonitorContainer("dokploy-postgres.1.2rfdhwsjhm82wai9hm9dp4sqn"), + ).toBe(false); + }); + + it("should handle service replicas", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [ + { appName: "testing-testing-1cosrk", maxFileSizeMB: 15 }, + ], + excludeServices: [], + }); + + // Ambas réplicas deberían coincidir + expect( + shouldMonitorContainer( + "testing-testing-1cosrk.1.klqzvggx7382en2itijsvd199", + ), + ).toBe(true); + expect( + shouldMonitorContainer( + "testing-testing-1cosrk.2.piajmzb7v7uclfdgz8lar4wbw", + ), + ).toBe(true); + }); + + it("should handle docker swarm services", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [{ appName: "dokploy-traefik", maxFileSizeMB: 15 }], + excludeServices: [], + }); + + expect( + shouldMonitorContainer("dokploy-traefik.1.u3uplxcs58rjiv4s4ty7gfidz"), + ).toBe(true); + expect(shouldMonitorContainer("other-traefik.1.xyz")).toBe(false); + }); + + it("should handle specific service monitoring", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [ + { appName: "testing-plausible-9cc9fd", maxFileSizeMB: 15 }, + ], + excludeServices: [], + }); + + // Debería coincidir con todos los contenedores del servicio plausible + expect( + shouldMonitorContainer("testing-plausible-9cc9fd-plausible-1"), + ).toBe(true); + expect( + shouldMonitorContainer("testing-plausible-9cc9fd-plausible_db-1"), + ).toBe(true); + expect( + shouldMonitorContainer( + "testing-plausible-9cc9fd-plausible_events_db-1", + ), + ).toBe(true); + expect( + shouldMonitorContainer("dokploy-postgres.1.2rfdhwsjhm82wai9hm9dp4sqn"), + ).toBe(false); + }); + + it("should handle docker compose service replicas", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [ + { appName: "testing-testing-1cosrk", maxFileSizeMB: 15 }, + ], + excludeServices: [], + }); + + // Ambas réplicas deberían coincidir + expect( + shouldMonitorContainer( + "testing-testing-1cosrk.1.klqzvggx7382en2itijsvd199", + ), + ).toBe(true); + expect( + shouldMonitorContainer( + "testing-testing-1cosrk.2.piajmzb7v7uclfdgz8lar4wbw", + ), + ).toBe(true); + }); + }); + + describe("getContainerConfig", () => { + it("should return correct config for matched container", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [{ appName: "dokploy-postgres", maxFileSizeMB: 15 }], + excludeServices: [], + }); + + const config = getContainerConfig( + "dokploy-postgres.1.2rfdhwsjhm82wai9hm9dp4sqn", + ); + expect(config).toEqual({ + appName: "dokploy-postgres", + maxFileSizeMB: 15, + }); + }); + + it("should return default config for non-matched container", () => { + process.env.CONTAINER_MONITORING_CONFIG = JSON.stringify({ + includeServices: [{ appName: "dokploy-postgres", maxFileSizeMB: 15 }], + excludeServices: [], + }); + + const config = getContainerConfig("some-other-container"); + expect(config).toEqual({ appName: "*", maxFileSizeMB: 10 }); + }); + }); +}); diff --git a/apps/monitoring/__test__/utils.test.ts b/apps/monitoring/__test__/utils.test.ts index 7e83a3348..cb4a6c56b 100644 --- a/apps/monitoring/__test__/utils.test.ts +++ b/apps/monitoring/__test__/utils.test.ts @@ -35,8 +35,16 @@ invalid json line {"timestamp": "2024-12-28T00:01:00Z", "value": 2}`; const result = parseLog(logContent); - expect(result).toHaveLength(3); - expect(result[1]).toEqual({ raw: "invalid json line" }); + expect(result).toHaveLength(2); + + expect(result[0]).toEqual({ + timestamp: "2024-12-28T00:00:00Z", + value: 1, + }); + expect(result[1]).toEqual({ + timestamp: "2024-12-28T00:01:00Z", + value: 2, + }); }); it("should handle empty log content", () => { diff --git a/apps/monitoring/src/containers/config.ts b/apps/monitoring/src/containers/config.ts index 11336add1..924ecd0a5 100644 --- a/apps/monitoring/src/containers/config.ts +++ b/apps/monitoring/src/containers/config.ts @@ -18,8 +18,6 @@ export const loadConfig = (): MonitoringConfig => { try { const configJson = process.env.CONTAINER_MONITORING_CONFIG; - console.log(configJson); - if (!configJson) { return DEFAULT_CONFIG; } @@ -35,54 +33,41 @@ export const loadConfig = (): MonitoringConfig => { }; export const getServiceName = (containerName: string): string => { - const match = containerName.match( - /^([\w-]+(?:_[\w-]+)*)(?:\.\d+\.[a-z0-9]+)?$/, - ); - return match ? match[1] : containerName; + // Para contenedores de Docker Swarm (ej: dokploy-postgres.1.2rfdhwsjhm82wai9hm9dp4sqn) + const swarmMatch = containerName.match(/^([^.]+)(?:\.\d+\.[a-z0-9]+)?$/); + if (swarmMatch) return swarmMatch[1]; + + return containerName; }; -export const shouldMonitorContainer = ( - containerName: string, - config: MonitoringConfig, -): boolean => { - const { includeServices, excludeServices } = config; +export const shouldMonitorContainer = (containerName: string): boolean => { + const config = loadConfig(); + const { includeServices } = config; const serviceName = getServiceName(containerName); - // If specifically included, always monitor - if (includeServices.some((service) => service.appName === serviceName)) { - return true; - } + // Verificar si el nombre del servicio coincide exactamente con alguno configurado + return includeServices.some((service) => { + // Si el appName es exactamente igual al nombre del servicio + if (service.appName === serviceName) return true; - // If there's a wildcard in includeServices and not specifically excluded - if ( - includeServices.some((service) => service.appName === "*") && - !excludeServices.includes(serviceName) - ) { - return true; - } + // Si el servicio comienza con el appName + if (serviceName.startsWith(service.appName)) return true; - // In any other case, don't monitor - return false; + return false; + }); }; -export const getContainerConfig = ( - containerName: string, - config: MonitoringConfig, -): ServiceConfig => { - const serviceName = getServiceName(containerName); +export const getContainerConfig = (containerName: string): ServiceConfig => { + const config = loadConfig(); const { includeServices } = config; + const serviceName = getServiceName(containerName); - // If it has specific configuration, use it - const specificConfig = includeServices.find( - (service) => service.appName === serviceName, - ); - if (specificConfig) { - return specificConfig; - } + // Buscar la configuración que coincida con el servicio + const specificConfig = includeServices.find((service) => { + if (service.appName === serviceName) return true; + if (serviceName.startsWith(service.appName)) return true; + return false; + }); - // If not, use default configuration (wildcard) - const wildcardConfig = includeServices.find( - (service) => service.appName === "*", - ); - return wildcardConfig || { appName: "*", maxFileSizeMB: 10 }; + return specificConfig || { appName: "*", maxFileSizeMB: 10 }; }; diff --git a/apps/monitoring/src/containers/index.ts b/apps/monitoring/src/containers/index.ts index 135d1c486..d9c2a1e83 100644 --- a/apps/monitoring/src/containers/index.ts +++ b/apps/monitoring/src/containers/index.ts @@ -6,7 +6,6 @@ import console from "node:console"; import { containerLogFile } from "../constants.js"; import type { Container } from "./types.js"; import { - loadConfig, shouldMonitorContainer, getContainerConfig, getServiceName, @@ -15,9 +14,6 @@ import { processContainerData } from "./utils.js"; export const execAsync = util.promisify(exec); -const config = loadConfig(); - -console.log(config); const REFRESH_RATE_CONTAINER = Number( process.env.CONTAINER_REFRESH_RATE || 10000, ); @@ -43,15 +39,18 @@ export const logContainerMetrics = () => { return; } + const seenServices = new Set(); const filteredContainer = containers.filter((container) => { - const shouldMonitor = shouldMonitorContainer(container.Name, config); - console.log( - `Service ${getServiceName(container.Name)} (${container.Name}): ${shouldMonitor ? "monitored" : "filtered out"}`, - ); - return shouldMonitor; + if (!shouldMonitorContainer(container.Name)) return false; + + const serviceName = getServiceName(container.Name); + if (seenServices.has(serviceName)) return false; + + seenServices.add(serviceName); + return true; }); - console.log(`Monitoring ${filteredContainer.length} containers`); + console.log(`Writing metrics for ${filteredContainer.length} containers`); for (const container of filteredContainer) { try { @@ -59,7 +58,7 @@ export const logContainerMetrics = () => { const containerPath = join(containerLogFile, `${serviceName}.log`); const processedData = processContainerData(container); const logLine = `${JSON.stringify(processedData)}\n`; - const containerConfig = getContainerConfig(container.Name, config); + const containerConfig = getContainerConfig(container.Name); const { maxFileSizeMB = 10 } = containerConfig;