From 3ddbd6a7fb4c4ca9b951cfeae0570a17a137e356 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 17 Sep 2025 16:50:36 -0300 Subject: [PATCH] feat(config): add telemetry and metrics configuration options - Introduce new environment variables for telemetry and Prometheus metrics in .env.example - Create example configuration files for Prometheus and Grafana dashboards - Update main application to utilize new configuration settings for Sentry, audio converter, and proxy - Enhance channel services to support audio conversion API integration - Implement middleware for metrics IP whitelisting and basic authentication in routes --- .env.example | 22 ++ grafana-dashboard.json.example | 238 ++++++++++++++++++ prometheus.yml.example | 76 ++++++ .../evolution/evolution.channel.service.ts | 9 +- .../channel/meta/whatsapp.business.service.ts | 9 +- .../whatsapp/whatsapp.baileys.service.ts | 8 +- .../event/websocket/websocket.controller.ts | 4 +- .../storage/s3/libs/minio.server.ts | 2 +- src/api/routes/index.router.ts | 72 +++++- src/api/services/channel.service.ts | 15 +- src/config/env.config.ts | 82 ++++++ src/config/event.config.ts | 5 +- src/main.ts | 13 +- src/utils/instrumentSentry.ts | 7 +- src/utils/sendTelemetry.ts | 9 +- src/utils/use-multi-file-auth-state-prisma.ts | 11 +- 16 files changed, 538 insertions(+), 44 deletions(-) create mode 100644 grafana-dashboard.json.example create mode 100644 prometheus.yml.example diff --git a/.env.example b/.env.example index 07dbac77..52644bea 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,28 @@ SSL_CONF_FULLCHAIN=/path/to/cert.crt SENTRY_DSN= +# Telemetry - Set to false to disable telemetry +TELEMETRY_ENABLED=true +TELEMETRY_URL= + +# Prometheus metrics - Set to true to enable Prometheus metrics +PROMETHEUS_METRICS=false +METRICS_AUTH_REQUIRED=true +METRICS_USER=prometheus +METRICS_PASSWORD=secure_random_password_here +METRICS_ALLOWED_IPS=127.0.0.1,10.0.0.100,192.168.1.50 + +# Proxy configuration (optional) +PROXY_HOST= +PROXY_PORT= +PROXY_PROTOCOL= +PROXY_USERNAME= +PROXY_PASSWORD= + +# Audio converter API (optional) +API_AUDIO_CONVERTER= +API_AUDIO_CONVERTER_KEY= + # Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com' CORS_ORIGIN=* CORS_METHODS=GET,POST,PUT,DELETE diff --git a/grafana-dashboard.json.example b/grafana-dashboard.json.example new file mode 100644 index 00000000..f85eb9d6 --- /dev/null +++ b/grafana-dashboard.json.example @@ -0,0 +1,238 @@ +{ + "dashboard": { + "id": null, + "title": "Evolution API Monitoring", + "tags": ["evolution-api", "whatsapp", "monitoring"], + "style": "dark", + "timezone": "browser", + "panels": [ + { + "id": 1, + "title": "API Status", + "type": "stat", + "targets": [ + { + "expr": "up{job=\"evolution-api\"}", + "legendFormat": "API Status" + } + ], + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "text": "DOWN", + "color": "red" + }, + "1": { + "text": "UP", + "color": "green" + } + }, + "type": "value" + } + ] + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + } + }, + { + "id": 2, + "title": "Total Instances", + "type": "stat", + "targets": [ + { + "expr": "evolution_instances_total", + "legendFormat": "Total Instances" + } + ], + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + } + }, + { + "id": 3, + "title": "Instance Status Overview", + "type": "piechart", + "targets": [ + { + "expr": "sum by (state) (evolution_instance_state)", + "legendFormat": "{{ state }}" + } + ], + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 8 + } + }, + { + "id": 4, + "title": "Instances by Integration Type", + "type": "piechart", + "targets": [ + { + "expr": "sum by (integration) (evolution_instance_up)", + "legendFormat": "{{ integration }}" + } + ], + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 8 + } + }, + { + "id": 5, + "title": "Instance Uptime", + "type": "table", + "targets": [ + { + "expr": "evolution_instance_up", + "format": "table", + "instant": true + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true + }, + "renameByName": { + "instance": "Instance Name", + "integration": "Integration Type", + "Value": "Status" + } + } + } + ], + "fieldConfig": { + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Status" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "text": "DOWN", + "color": "red" + }, + "1": { + "text": "UP", + "color": "green" + } + }, + "type": "value" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 17 + } + }, + { + "id": 6, + "title": "Instance Status Timeline", + "type": "timeseries", + "targets": [ + { + "expr": "evolution_instance_up", + "legendFormat": "{{ instance }} ({{ integration }})" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "stepAfter", + "lineWidth": 2, + "fillOpacity": 10, + "gradientMode": "none", + "spanNulls": false, + "insertNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "min": 0, + "max": 1 + } + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 26 + } + } + ], + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "templating": { + "list": [] + }, + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "refresh": "30s", + "schemaVersion": 27, + "version": 0, + "links": [] + } +} diff --git a/prometheus.yml.example b/prometheus.yml.example new file mode 100644 index 00000000..bcb3b332 --- /dev/null +++ b/prometheus.yml.example @@ -0,0 +1,76 @@ +# Prometheus configuration example for Evolution API +# Copy this file to prometheus.yml and adjust the settings + +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + # - "first_rules.yml" + # - "second_rules.yml" + +scrape_configs: + # Evolution API metrics + - job_name: 'evolution-api' + static_configs: + - targets: ['localhost:8080'] # Adjust to your Evolution API URL + + # Metrics endpoint path + metrics_path: '/metrics' + + # Scrape interval for this job + scrape_interval: 30s + + # Basic authentication (if METRICS_AUTH_REQUIRED=true) + basic_auth: + username: 'prometheus' # Should match METRICS_USER + password: 'secure_random_password_here' # Should match METRICS_PASSWORD + + # Optional: Add custom labels + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__param_target] + target_label: instance + - target_label: __address__ + replacement: localhost:8080 # Evolution API address + +# Alerting configuration (optional) +alerting: + alertmanagers: + - static_configs: + - targets: + # - alertmanager:9093 + +# Example alert rules for Evolution API +# Create a file called evolution_alerts.yml with these rules: +# +# groups: +# - name: evolution-api +# rules: +# - alert: EvolutionAPIDown +# expr: up{job="evolution-api"} == 0 +# for: 1m +# labels: +# severity: critical +# annotations: +# summary: "Evolution API is down" +# description: "Evolution API has been down for more than 1 minute." +# +# - alert: EvolutionInstanceDown +# expr: evolution_instance_up == 0 +# for: 2m +# labels: +# severity: warning +# annotations: +# summary: "Evolution instance {{ $labels.instance }} is down" +# description: "Instance {{ $labels.instance }} has been down for more than 2 minutes." +# +# - alert: HighInstanceCount +# expr: evolution_instances_total > 100 +# for: 5m +# labels: +# severity: warning +# annotations: +# summary: "High number of Evolution instances" +# description: "Evolution API is managing {{ $value }} instances." diff --git a/src/api/integrations/channel/evolution/evolution.channel.service.ts b/src/api/integrations/channel/evolution/evolution.channel.service.ts index 1b4773ce..820218b1 100644 --- a/src/api/integrations/channel/evolution/evolution.channel.service.ts +++ b/src/api/integrations/channel/evolution/evolution.channel.service.ts @@ -13,7 +13,7 @@ import { chatbotController } from '@api/server.module'; import { CacheService } from '@api/services/cache.service'; import { ChannelStartupService } from '@api/services/channel.service'; import { Events, wa } from '@api/types/wa.types'; -import { Chatwoot, ConfigService, Openai, S3 } from '@config/env.config'; +import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config'; import { BadRequestException, InternalServerErrorException } from '@exceptions'; import { createJid } from '@utils/createJid'; import axios from 'axios'; @@ -622,7 +622,8 @@ export class EvolutionStartupService extends ChannelStartupService { number = number.replace(/\D/g, ''); const hash = `${number}-${new Date().getTime()}`; - if (process.env.API_AUDIO_CONVERTER) { + const audioConverterConfig = this.configService.get('AUDIO_CONVERTER'); + if (audioConverterConfig.API_URL) { try { this.logger.verbose('Using audio converter API'); const formData = new FormData(); @@ -640,10 +641,10 @@ export class EvolutionStartupService extends ChannelStartupService { formData.append('format', 'mp4'); - const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, { + const response = await axios.post(audioConverterConfig.API_URL, formData, { headers: { ...formData.getHeaders(), - apikey: process.env.API_AUDIO_CONVERTER_KEY, + apikey: audioConverterConfig.API_KEY, }, }); diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 66847f82..a88f85e9 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -20,7 +20,7 @@ import { chatbotController } from '@api/server.module'; import { CacheService } from '@api/services/cache.service'; import { ChannelStartupService } from '@api/services/channel.service'; import { Events, wa } from '@api/types/wa.types'; -import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config'; +import { AudioConverter, Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config'; import { BadRequestException, InternalServerErrorException } from '@exceptions'; import { createJid } from '@utils/createJid'; import { status } from '@utils/renderStatus'; @@ -1300,7 +1300,8 @@ export class BusinessStartupService extends ChannelStartupService { number = number.replace(/\D/g, ''); const hash = `${number}-${new Date().getTime()}`; - if (process.env.API_AUDIO_CONVERTER) { + const audioConverterConfig = this.configService.get('AUDIO_CONVERTER'); + if (audioConverterConfig.API_URL) { this.logger.verbose('Using audio converter API'); const formData = new FormData(); @@ -1317,10 +1318,10 @@ export class BusinessStartupService extends ChannelStartupService { formData.append('format', 'mp3'); - const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, { + const response = await axios.post(audioConverterConfig.API_URL, formData, { headers: { ...formData.getHeaders(), - apikey: process.env.API_AUDIO_CONVERTER_KEY, + apikey: audioConverterConfig.API_KEY, }, }); diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 44c66434..acacb9b7 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -62,6 +62,7 @@ import { ChannelStartupService } from '@api/services/channel.service'; import { Events, MessageSubtype, TypeMediaMessage, wa } from '@api/types/wa.types'; import { CacheEngine } from '@cache/cacheengine'; import { + AudioConverter, CacheConf, Chatwoot, ConfigService, @@ -2837,7 +2838,8 @@ export class BaileysStartupService extends ChannelStartupService { } public async processAudio(audio: string): Promise { - if (process.env.API_AUDIO_CONVERTER) { + const audioConverterConfig = this.configService.get('AUDIO_CONVERTER'); + if (audioConverterConfig.API_URL) { this.logger.verbose('Using audio converter API'); const formData = new FormData(); @@ -2847,8 +2849,8 @@ export class BaileysStartupService extends ChannelStartupService { formData.append('base64', audio); } - const { data } = await axios.post(process.env.API_AUDIO_CONVERTER, formData, { - headers: { ...formData.getHeaders(), apikey: process.env.API_AUDIO_CONVERTER_KEY }, + const { data } = await axios.post(audioConverterConfig.API_URL, formData, { + headers: { ...formData.getHeaders(), apikey: audioConverterConfig.API_KEY }, }); if (!data.audio) { diff --git a/src/api/integrations/event/websocket/websocket.controller.ts b/src/api/integrations/event/websocket/websocket.controller.ts index 728c7644..72435234 100644 --- a/src/api/integrations/event/websocket/websocket.controller.ts +++ b/src/api/integrations/event/websocket/websocket.controller.ts @@ -31,7 +31,9 @@ export class WebsocketController extends EventController implements EventControl const params = new URLSearchParams(url.search); const { remoteAddress } = req.socket; - const isAllowedHost = (process.env.WEBSOCKET_ALLOWED_HOSTS || '127.0.0.1,::1,::ffff:127.0.0.1') + const websocketConfig = configService.get('WEBSOCKET'); + const allowedHosts = websocketConfig.ALLOWED_HOSTS || '127.0.0.1,::1,::ffff:127.0.0.1'; + const isAllowedHost = allowedHosts .split(',') .map((h) => h.trim()) .includes(remoteAddress); diff --git a/src/api/integrations/storage/s3/libs/minio.server.ts b/src/api/integrations/storage/s3/libs/minio.server.ts index 30c81876..8d296fdc 100644 --- a/src/api/integrations/storage/s3/libs/minio.server.ts +++ b/src/api/integrations/storage/s3/libs/minio.server.ts @@ -26,7 +26,7 @@ const minioClient = (() => { } })(); -const bucketName = process.env.S3_BUCKET; +const bucketName = BUCKET.BUCKET_NAME; const bucketExists = async () => { if (minioClient) { diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index f1a86393..7ef197de 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -6,9 +6,9 @@ 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 { waMonitor } from '@api/server.module'; -import { configService } from '@config/env.config'; +import { configService, Database, Facebook } from '@config/env.config'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; -import { Router } from 'express'; +import { NextFunction, Request, Response, Router } from 'express'; import fs from 'fs'; import mimeTypes from 'mime-types'; import path from 'path'; @@ -37,15 +37,68 @@ enum HttpStatus { const router: Router = Router(); const serverConfig = configService.get('SERVER'); +const databaseConfig = configService.get('DATABASE'); const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard['apikey']]; const telemetry = new Telemetry(); const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +// Middleware for metrics IP whitelist +const metricsIPWhitelist = (req: Request, res: Response, next: NextFunction) => { + const metricsConfig = configService.get('METRICS'); + const allowedIPs = metricsConfig.ALLOWED_IPS?.split(',').map((ip) => ip.trim()) || ['127.0.0.1']; + const clientIP = req.ip || req.connection.remoteAddress || req.socket.remoteAddress; + + if (!allowedIPs.includes(clientIP)) { + return res.status(403).send('Forbidden: IP not allowed'); + } + + next(); +}; + +// Middleware for metrics Basic Authentication +const metricsBasicAuth = (req: Request, res: Response, next: NextFunction) => { + const metricsConfig = configService.get('METRICS'); + const metricsUser = metricsConfig.USER; + const metricsPass = metricsConfig.PASSWORD; + + if (!metricsUser || !metricsPass) { + return res.status(500).send('Metrics authentication not configured'); + } + + const auth = req.get('Authorization'); + if (!auth || !auth.startsWith('Basic ')) { + res.set('WWW-Authenticate', 'Basic realm="Evolution API Metrics"'); + return res.status(401).send('Authentication required'); + } + + const credentials = Buffer.from(auth.slice(6), 'base64').toString(); + const [user, pass] = credentials.split(':'); + + if (user !== metricsUser || pass !== metricsPass) { + return res.status(401).send('Invalid credentials'); + } + + next(); +}; + // Expose Prometheus metrics when enabled by env flag -if (process.env.PROMETHEUS_METRICS === 'true') { - router.get('/metrics', async (req, res) => { +const metricsConfig = configService.get('METRICS'); +if (metricsConfig.ENABLED) { + const metricsMiddleware = []; + + // Add IP whitelist if configured + if (metricsConfig.ALLOWED_IPS) { + metricsMiddleware.push(metricsIPWhitelist); + } + + // Add Basic Auth if required + if (metricsConfig.AUTH_REQUIRED) { + metricsMiddleware.push(metricsBasicAuth); + } + + router.get('/metrics', ...metricsMiddleware, 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'); @@ -57,7 +110,7 @@ if (process.env.PROMETHEUS_METRICS === 'true') { const lines: string[] = []; - const clientName = process.env.DATABASE_CONNECTION_CLIENT_NAME || 'unknown'; + const clientName = databaseConfig.CONNECTION.CLIENT_NAME || 'unknown'; const serverUrl = serverConfig.URL || ''; // environment info @@ -140,19 +193,20 @@ router status: HttpStatus.OK, message: 'Welcome to the Evolution API, it is working!', version: packageJson.version, - clientName: process.env.DATABASE_CONNECTION_CLIENT_NAME, + clientName: databaseConfig.CONNECTION.CLIENT_NAME, manager: !serverConfig.DISABLE_MANAGER ? `${req.protocol}://${req.get('host')}/manager` : undefined, documentation: `https://doc.evolution-api.com`, whatsappWebVersion: (await fetchLatestWaWebVersion({})).version.join('.'), }); }) .post('/verify-creds', authGuard['apikey'], async (req, res) => { + const facebookConfig = configService.get('FACEBOOK'); return res.status(HttpStatus.OK).json({ status: HttpStatus.OK, message: 'Credentials are valid', - facebookAppId: process.env.FACEBOOK_APP_ID, - facebookConfigId: process.env.FACEBOOK_CONFIG_ID, - facebookUserToken: process.env.FACEBOOK_USER_TOKEN, + facebookAppId: facebookConfig.APP_ID, + facebookConfigId: facebookConfig.CONFIG_ID, + facebookUserToken: facebookConfig.USER_TOKEN, }); }) .use('/instance', new InstanceRouter(configService, ...guards).router) diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index dc754ab6..4b39520e 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -9,7 +9,7 @@ import { TypebotService } from '@api/integrations/chatbot/typebot/services/typeb import { PrismaRepository, Query } from '@api/repository/repository.service'; import { eventManager, waMonitor } from '@api/server.module'; import { Events, wa } from '@api/types/wa.types'; -import { Auth, Chatwoot, ConfigService, HttpServer } from '@config/env.config'; +import { Auth, Chatwoot, ConfigService, HttpServer, Proxy } from '@config/env.config'; import { Logger } from '@config/logger.config'; import { NotFoundException } from '@exceptions'; import { Contact, Message, Prisma } from '@prisma/client'; @@ -364,13 +364,14 @@ export class ChannelStartupService { public async loadProxy() { this.localProxy.enabled = false; - if (process.env.PROXY_HOST) { + const proxyConfig = this.configService.get('PROXY'); + if (proxyConfig.HOST) { this.localProxy.enabled = true; - this.localProxy.host = process.env.PROXY_HOST; - this.localProxy.port = process.env.PROXY_PORT || '80'; - this.localProxy.protocol = process.env.PROXY_PROTOCOL || 'http'; - this.localProxy.username = process.env.PROXY_USERNAME; - this.localProxy.password = process.env.PROXY_PASSWORD; + this.localProxy.host = proxyConfig.HOST; + this.localProxy.port = proxyConfig.PORT || '80'; + this.localProxy.protocol = proxyConfig.PROTOCOL || 'http'; + this.localProxy.username = proxyConfig.USERNAME; + this.localProxy.password = proxyConfig.PASSWORD; } const data = await this.prismaRepository.proxy.findUnique({ diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 98ffc1de..66e0b000 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -156,6 +156,7 @@ export type Sqs = { export type Websocket = { ENABLED: boolean; GLOBAL_EVENTS: boolean; + ALLOWED_HOSTS?: string; }; export type WaBusiness = { @@ -320,6 +321,46 @@ export type S3 = { }; export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal }; +export type Metrics = { + ENABLED: boolean; + AUTH_REQUIRED: boolean; + USER?: string; + PASSWORD?: string; + ALLOWED_IPS?: string; +}; + +export type Telemetry = { + ENABLED: boolean; + URL?: string; +}; + +export type Proxy = { + HOST?: string; + PORT?: string; + PROTOCOL?: string; + USERNAME?: string; + PASSWORD?: string; +}; + +export type AudioConverter = { + API_URL?: string; + API_KEY?: string; +}; + +export type Facebook = { + APP_ID?: string; + CONFIG_ID?: string; + USER_TOKEN?: string; +}; + +export type Sentry = { + DSN?: string; +}; + +export type EventEmitter = { + MAX_LISTENERS: number; +}; + export type Production = boolean; export interface Env { @@ -351,6 +392,13 @@ export interface Env { CACHE: CacheConf; S3?: S3; AUTHENTICATION: Auth; + METRICS: Metrics; + TELEMETRY: Telemetry; + PROXY: Proxy; + AUDIO_CONVERTER: AudioConverter; + FACEBOOK: Facebook; + SENTRY: Sentry; + EVENT_EMITTER: EventEmitter; PRODUCTION?: Production; } @@ -542,6 +590,7 @@ export class ConfigService { WEBSOCKET: { ENABLED: process.env?.WEBSOCKET_ENABLED === 'true', GLOBAL_EVENTS: process.env?.WEBSOCKET_GLOBAL_EVENTS === 'true', + ALLOWED_HOSTS: process.env?.WEBSOCKET_ALLOWED_HOSTS, }, PUSHER: { ENABLED: process.env?.PUSHER_ENABLED === 'true', @@ -730,6 +779,39 @@ export class ConfigService { }, EXPOSE_IN_FETCH_INSTANCES: process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', }, + METRICS: { + ENABLED: process.env?.PROMETHEUS_METRICS === 'true', + AUTH_REQUIRED: process.env?.METRICS_AUTH_REQUIRED === 'true', + USER: process.env?.METRICS_USER, + PASSWORD: process.env?.METRICS_PASSWORD, + ALLOWED_IPS: process.env?.METRICS_ALLOWED_IPS, + }, + TELEMETRY: { + ENABLED: process.env?.TELEMETRY_ENABLED === undefined || process.env?.TELEMETRY_ENABLED === 'true', + URL: process.env?.TELEMETRY_URL, + }, + PROXY: { + HOST: process.env?.PROXY_HOST, + PORT: process.env?.PROXY_PORT, + PROTOCOL: process.env?.PROXY_PROTOCOL, + USERNAME: process.env?.PROXY_USERNAME, + PASSWORD: process.env?.PROXY_PASSWORD, + }, + AUDIO_CONVERTER: { + API_URL: process.env?.API_AUDIO_CONVERTER, + API_KEY: process.env?.API_AUDIO_CONVERTER_KEY, + }, + FACEBOOK: { + APP_ID: process.env?.FACEBOOK_APP_ID, + CONFIG_ID: process.env?.FACEBOOK_CONFIG_ID, + USER_TOKEN: process.env?.FACEBOOK_USER_TOKEN, + }, + SENTRY: { + DSN: process.env?.SENTRY_DSN, + }, + EVENT_EMITTER: { + MAX_LISTENERS: Number.parseInt(process.env?.EVENT_EMITTER_MAX_LISTENERS) || 50, + }, }; } } diff --git a/src/config/event.config.ts b/src/config/event.config.ts index 20cd1e40..ab9d05a8 100644 --- a/src/config/event.config.ts +++ b/src/config/event.config.ts @@ -1,10 +1,11 @@ +import { configService, EventEmitter as EventEmitterConfig } from '@config/env.config'; import EventEmitter2 from 'eventemitter2'; -const maxListeners = parseInt(process.env.EVENT_EMITTER_MAX_LISTENERS, 10) || 50; +const eventEmitterConfig = configService.get('EVENT_EMITTER'); export const eventEmitter = new EventEmitter2({ delimiter: '.', newListener: false, ignoreErrors: false, - maxListeners: maxListeners, + maxListeners: eventEmitterConfig.MAX_LISTENERS, }); diff --git a/src/main.ts b/src/main.ts index cf787f32..44a6187b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,15 @@ import { ProviderFiles } from '@api/provider/sessions'; import { PrismaRepository } from '@api/repository/repository.service'; import { HttpStatus, router } from '@api/routes/index.router'; import { eventManager, waMonitor } from '@api/server.module'; -import { Auth, configService, Cors, HttpServer, ProviderSession, Webhook } from '@config/env.config'; +import { + Auth, + configService, + Cors, + HttpServer, + ProviderSession, + Sentry as SentryConfig, + Webhook, +} from '@config/env.config'; import { onUnexpectedError } from '@config/error.config'; import { Logger } from '@config/logger.config'; import { ROOT_DIR } from '@config/path.config'; @@ -140,7 +148,8 @@ async function bootstrap() { eventManager.init(server); - if (process.env.SENTRY_DSN) { + const sentryConfig = configService.get('SENTRY'); + if (sentryConfig.DSN) { logger.info('Sentry - ON'); // Add this after all routes, diff --git a/src/utils/instrumentSentry.ts b/src/utils/instrumentSentry.ts index 1a87086f..a91eb772 100644 --- a/src/utils/instrumentSentry.ts +++ b/src/utils/instrumentSentry.ts @@ -1,10 +1,11 @@ +import { configService, Sentry as SentryConfig } from '@config/env.config'; import * as Sentry from '@sentry/node'; -const dsn = process.env.SENTRY_DSN; +const sentryConfig = configService.get('SENTRY'); -if (dsn) { +if (sentryConfig.DSN) { Sentry.init({ - dsn: dsn, + dsn: sentryConfig.DSN, environment: process.env.NODE_ENV || 'development', tracesSampleRate: 1.0, profilesSampleRate: 1.0, diff --git a/src/utils/sendTelemetry.ts b/src/utils/sendTelemetry.ts index 037ca55d..b2ebe05a 100644 --- a/src/utils/sendTelemetry.ts +++ b/src/utils/sendTelemetry.ts @@ -1,3 +1,4 @@ +import { configService, Telemetry } from '@config/env.config'; import axios from 'axios'; import fs from 'fs'; @@ -10,9 +11,9 @@ export interface TelemetryData { } export const sendTelemetry = async (route: string): Promise => { - const enabled = process.env.TELEMETRY_ENABLED === undefined || process.env.TELEMETRY_ENABLED === 'true'; + const telemetryConfig = configService.get('TELEMETRY'); - if (!enabled) { + if (!telemetryConfig.ENABLED) { return; } @@ -27,9 +28,7 @@ export const sendTelemetry = async (route: string): Promise => { }; const url = - process.env.TELEMETRY_URL && process.env.TELEMETRY_URL !== '' - ? process.env.TELEMETRY_URL - : 'https://log.evolution-api.com/telemetry'; + telemetryConfig.URL && telemetryConfig.URL !== '' ? telemetryConfig.URL : 'https://log.evolution-api.com/telemetry'; axios .post(url, telemetry) diff --git a/src/utils/use-multi-file-auth-state-prisma.ts b/src/utils/use-multi-file-auth-state-prisma.ts index 84d38fe4..7278c056 100644 --- a/src/utils/use-multi-file-auth-state-prisma.ts +++ b/src/utils/use-multi-file-auth-state-prisma.ts @@ -1,5 +1,6 @@ import { prismaRepository } from '@api/server.module'; import { CacheService } from '@api/services/cache.service'; +import { CacheConf, configService } from '@config/env.config'; import { INSTANCE_DIR } from '@config/path.config'; import { AuthenticationState, BufferJSON, initAuthCreds, WAProto as proto } from 'baileys'; import fs from 'fs/promises'; @@ -85,9 +86,10 @@ export default async function useMultiFileAuthStatePrisma( async function writeData(data: any, key: string): Promise { const dataString = JSON.stringify(data, BufferJSON.replacer); + const cacheConfig = configService.get('CACHE'); if (key != 'creds') { - if (process.env.CACHE_REDIS_ENABLED === 'true') { + if (cacheConfig.REDIS.ENABLED) { return await cache.hSet(sessionId, key, data); } else { await fs.writeFile(localFile(key), dataString); @@ -101,9 +103,10 @@ export default async function useMultiFileAuthStatePrisma( async function readData(key: string): Promise { try { let rawData; + const cacheConfig = configService.get('CACHE'); if (key != 'creds') { - if (process.env.CACHE_REDIS_ENABLED === 'true') { + if (cacheConfig.REDIS.ENABLED) { return await cache.hGet(sessionId, key); } else { if (!(await fileExists(localFile(key)))) return null; @@ -123,8 +126,10 @@ export default async function useMultiFileAuthStatePrisma( async function removeData(key: string): Promise { try { + const cacheConfig = configService.get('CACHE'); + if (key != 'creds') { - if (process.env.CACHE_REDIS_ENABLED === 'true') { + if (cacheConfig.REDIS.ENABLED) { return await cache.hDelete(sessionId, key); } else { await fs.unlink(localFile(key));