mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-18 19:32:21 -06:00
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:
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -26,7 +26,7 @@ const minioClient = (() => {
|
||||
}
|
||||
})();
|
||||
|
||||
const bucketName = process.env.S3_BUCKET;
|
||||
const bucketName = BUCKET.BUCKET_NAME;
|
||||
|
||||
const bucketExists = async () => {
|
||||
if (minioClient) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user