From 875b874a7b305c3fc22d8848912bd224b2688e5e Mon Sep 17 00:00:00 2001 From: Elizandro Pacheco Date: Tue, 16 Sep 2025 19:34:44 -0300 Subject: [PATCH 1/4] feat: add Prometheus-compatible /metrics endpoint (gated by PROMETHEUS_METRICS) --- src/api/routes/index.router.ts | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index 48954ea0..d28c05f2 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -6,6 +6,7 @@ import { ChatbotRouter } from '@api/integrations/chatbot/chatbot.router'; import { EventRouter } from '@api/integrations/event/event.router'; import { StorageRouter } from '@api/integrations/storage/storage.router'; import { configService } from '@config/env.config'; +import { waMonitor } from '@api/server.module'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; import { Router } from 'express'; import fs from 'fs'; @@ -42,6 +43,65 @@ const telemetry = new Telemetry(); const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +// Expose Prometheus metrics when enabled by env flag +if (process.env.PROMETHEUS_METRICS === 'true') { + router.get('/metrics', async (req, res) => { + res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); + res.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + + const escapeLabel = (value: unknown) => + String(value ?? '') + .replace(/\\/g, '\\\\') + .replace(/\n/g, '\\n') + .replace(/"/g, '\\"'); + + const lines: string[] = []; + + const clientName = process.env.DATABASE_CONNECTION_CLIENT_NAME || ''; + const serverUrl = serverConfig.URL || ''; + + // environment info + lines.push('# HELP evolution_environment_info Environment information'); + lines.push('# TYPE evolution_environment_info gauge'); + lines.push( + `evolution_environment_info{version="${escapeLabel(packageJson.version)}",clientName="${escapeLabel( + clientName, + )}",serverUrl="${escapeLabel(serverUrl)}"} 1`, + ); + + const instances = (waMonitor && waMonitor.waInstances) || {}; + const instanceEntries = Object.entries(instances); + + // total instances + lines.push('# HELP evolution_instances_total Total number of instances'); + lines.push('# TYPE evolution_instances_total gauge'); + lines.push(`evolution_instances_total ${instanceEntries.length}`); + + // per-instance status + lines.push('# HELP evolution_instance_up 1 if instance state is open, else 0'); + lines.push('# TYPE evolution_instance_up gauge'); + lines.push('# HELP evolution_instance_state Instance state as a labelled metric'); + lines.push('# TYPE evolution_instance_state gauge'); + + for (const [name, instance] of instanceEntries) { + const state = instance?.connectionStatus?.state || 'unknown'; + const integration = instance?.integration || ''; + const up = state === 'open' ? 1 : 0; + + lines.push( + `evolution_instance_up{instance="${escapeLabel(name)}",integration="${escapeLabel(integration)}"} ${up}`, + ); + lines.push( + `evolution_instance_state{instance="${escapeLabel(name)}",integration="${escapeLabel( + integration, + )}",state="${escapeLabel(state)}"} 1`, + ); + } + + res.send(lines.join('\n') + '\n'); + }); +} + if (!serverConfig.DISABLE_MANAGER) router.use('/manager', new ViewsRouter().router); router.get('/assets/*', (req, res) => { From a3223ec890808048fa659bece84979d660137361 Mon Sep 17 00:00:00 2001 From: Elizandro Pacheco Date: Tue, 16 Sep 2025 19:35:22 -0300 Subject: [PATCH 2/4] chore: local compose/image tweaks for testing metrics (not part of PR) --- Dockerfile.metrics | 19 +++++++++++++++++++ docker-compose.yaml | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.metrics diff --git a/Dockerfile.metrics b/Dockerfile.metrics new file mode 100644 index 00000000..e8d46d04 --- /dev/null +++ b/Dockerfile.metrics @@ -0,0 +1,19 @@ +FROM evoapicloud/evolution-api:latest AS base +WORKDIR /evolution + +# Copiamos apenas o necessário para recompilar o dist com as mudanças locais +COPY tsconfig.json tsup.config.ts package.json ./ +COPY src ./src + +# Recompila usando os node_modules já presentes na imagem base +RUN npm run build + +# Runtime final: reaproveita a imagem oficial e apenas sobrepõe o dist +FROM evoapicloud/evolution-api:latest AS final +WORKDIR /evolution +COPY --from=base /evolution/dist ./dist + +ENV PROMETHEUS_METRICS=true + +# Entrada original da imagem oficial já sobe o app em /evolution + diff --git a/docker-compose.yaml b/docker-compose.yaml index b049f00f..b4899797 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,7 +3,7 @@ version: "3.8" services: api: container_name: evolution_api - image: evoapicloud/evolution-api:latest + image: evolution/api:metrics restart: always depends_on: - redis From 0e737d48c12355597b9d8a287846bb97cd29f5b3 Mon Sep 17 00:00:00 2001 From: Elizandro Pacheco Date: Tue, 16 Sep 2025 19:40:21 -0300 Subject: [PATCH 3/4] chore(metrics): use 'unknown' as fallback for clientName label --- src/api/routes/index.router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index d28c05f2..1d865269 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -57,7 +57,7 @@ if (process.env.PROMETHEUS_METRICS === 'true') { const lines: string[] = []; - const clientName = process.env.DATABASE_CONNECTION_CLIENT_NAME || ''; + const clientName = process.env.DATABASE_CONNECTION_CLIENT_NAME || 'unknown'; const serverUrl = serverConfig.URL || ''; // environment info From edfcb0c0821937b9b005e36fe5a7c1fed364b586 Mon Sep 17 00:00:00 2001 From: Elizandro Pacheco Date: Wed, 17 Sep 2025 12:05:30 -0300 Subject: [PATCH 4/4] style(metrics): linted index.router.ts after eslint --fix --- src/api/routes/index.router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index 1d865269..70019d3c 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -5,8 +5,8 @@ import { ChannelRouter } from '@api/integrations/channel/channel.router'; import { ChatbotRouter } from '@api/integrations/chatbot/chatbot.router'; import { EventRouter } from '@api/integrations/event/event.router'; import { StorageRouter } from '@api/integrations/storage/storage.router'; -import { configService } from '@config/env.config'; import { waMonitor } from '@api/server.module'; +import { configService } from '@config/env.config'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; import { Router } from 'express'; import fs from 'fs';