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
This commit is contained in:
Davidson Gomes
2025-09-17 16:50:36 -03:00
parent 81a991a62e
commit 3ddbd6a7fb
16 changed files with 538 additions and 44 deletions

View File

@@ -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<AudioConverter>('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,
},
});

View File

@@ -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<AudioConverter>('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,
},
});

View File

@@ -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<Buffer> {
if (process.env.API_AUDIO_CONVERTER) {
const audioConverterConfig = this.configService.get<AudioConverter>('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) {

View File

@@ -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>('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);

View File

@@ -26,7 +26,7 @@ const minioClient = (() => {
}
})();
const bucketName = process.env.S3_BUCKET;
const bucketName = BUCKET.BUCKET_NAME;
const bucketExists = async () => {
if (minioClient) {

View File

@@ -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>('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>('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)

View File

@@ -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>('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({