1 parent
89543ab371
commit
b7c4eac93b
12 files changed
+557
-1
No files matched your search
@@ -56,6 +56,20 @@ WAHA_LOG_LEVEL=info
|
||||
# Don't print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
|
||||
# ======================
|
||||
# ===== PROMETHEUS =====
|
||||
# ======================
|
||||
# Prometheus metrics endpoint - GET /metrics
|
||||
# WAHA_PROMETHEUS_ENABLED=True
|
||||
# WAHA_PROMETHEUS_PATH=/metrics
|
||||
# WAHA_PROMETHEUS_METRIC_PREFIX=waha_
|
||||
# WAHA_PROMETHEUS_HTTP_DURATION_BUCKETS=0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10,30
|
||||
# Events to count in waha_events_total ('*' for all) - the server actively processes listed events even with no webhooks
|
||||
# WAHA_PROMETHEUS_TRACK_EVENTS=message.any
|
||||
# Optional basic auth for the metrics endpoint (both must be set)
|
||||
# WAHA_PROMETHEUS_USERNAME=admin
|
||||
# WAHA_PROMETHEUS_PASSWORD=secret
|
||||
|
||||
# =========================
|
||||
# ===== MEDIA STORAGE =====
|
||||
# =========================
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"@opentelemetry/instrumentation-pino": "^0.67.0",
|
||||
"@opentelemetry/sdk-node": "^0.221.0",
|
||||
"@opentelemetry/sdk-trace": "^2.10.0",
|
||||
"@prometheus-io/client": "^0.16.1",
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/mustache": "^4.2.5",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DECORATORS } from '@nestjs/swagger/dist/constants';
|
||||
import { GetAppsApiTags } from '@waha/apps/app_sdk/api/tags';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
import { DashboardConfigServiceCore } from '@waha/core/config/DashboardConfigServiceCore';
|
||||
import { getPrometheusExcludePaths } from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { Logger } from 'nestjs-pino';
|
||||
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
@@ -206,6 +207,7 @@ export class SwaggerConfiguratorCore {
|
||||
'/jobs',
|
||||
'/jobs/',
|
||||
...config.getExcludedFullPaths(),
|
||||
...getPrometheusExcludePaths(),
|
||||
]);
|
||||
|
||||
const authFunction = BasicAuthFunction(username, password, exclude);
|
||||
|
||||
@@ -34,6 +34,11 @@ import { isPresenceAutoOnlineEnabled } from '@waha/modules/waha-maintain-online-
|
||||
import { MaintainOnlineStatusModule } from '@waha/modules/waha-maintain-online-status/maintain-online-status.module';
|
||||
import { isJidEngine } from '@waha/modules/waha-wid-jid/wid-jid.plugins';
|
||||
import { MessageSourceModule } from '@waha/modules/waha-message-source/message-source.module';
|
||||
import {
|
||||
getPrometheusPath,
|
||||
isPrometheusEnabled,
|
||||
} from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { PrometheusModule } from '@waha/modules/waha-prometheus/prometheus.module';
|
||||
import { WebhookModule } from '@waha/modules/waha-webhook/webhook.module';
|
||||
import { SessionRuntimeInfoModule } from '@waha/modules/waha-session-runtime-info/session-runtime-info.module';
|
||||
import { WidJIDModule } from '@waha/modules/waha-wid-jid/wid-jid.module';
|
||||
@@ -99,6 +104,7 @@ export const IMPORTS_CORE = [
|
||||
ignore: (req) => {
|
||||
return (
|
||||
req.url.startsWith('/ping') ||
|
||||
req.url.startsWith(getPrometheusPath()) ||
|
||||
req.url.startsWith('/dashboard/') ||
|
||||
req.url.startsWith('/api/files/') ||
|
||||
req.url.startsWith('/api/s3/') ||
|
||||
@@ -161,6 +167,9 @@ export const IMPORTS_CORE = [
|
||||
isPresenceAutoOnlineEnabled,
|
||||
{ debug: isDebugEnabled() },
|
||||
),
|
||||
ConditionalModule.registerWhen(PrometheusModule, isPrometheusEnabled, {
|
||||
debug: isDebugEnabled(),
|
||||
}),
|
||||
];
|
||||
|
||||
const IMPORTS_MEDIA = [
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
|
||||
import { EventWildUnmask } from '@waha/utils/events';
|
||||
import * as Joi from 'joi';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
|
||||
enum Env {
|
||||
WAHA_PROMETHEUS_ENABLED = 'WAHA_PROMETHEUS_ENABLED',
|
||||
WAHA_PROMETHEUS_PATH = 'WAHA_PROMETHEUS_PATH',
|
||||
WAHA_PROMETHEUS_METRIC_PREFIX = 'WAHA_PROMETHEUS_METRIC_PREFIX',
|
||||
WAHA_PROMETHEUS_HTTP_DURATION_BUCKETS = 'WAHA_PROMETHEUS_HTTP_DURATION_BUCKETS',
|
||||
WAHA_PROMETHEUS_USERNAME = 'WAHA_PROMETHEUS_USERNAME',
|
||||
WAHA_PROMETHEUS_PASSWORD = 'WAHA_PROMETHEUS_PASSWORD',
|
||||
WAHA_PROMETHEUS_TRACK_EVENTS = 'WAHA_PROMETHEUS_TRACK_EVENTS',
|
||||
}
|
||||
|
||||
const DEFAULT_PATH = '/metrics';
|
||||
const DEFAULT_METRIC_PREFIX = 'waha_';
|
||||
const DEFAULT_HTTP_DURATION_BUCKETS = [
|
||||
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30,
|
||||
];
|
||||
|
||||
export const PrometheusEnvSchema = Joi.object({
|
||||
[Env.WAHA_PROMETHEUS_ENABLED]: Joi.boolean().truthy('1').falsy('0'),
|
||||
[Env.WAHA_PROMETHEUS_PATH]: Joi.string().pattern(/^\//).allow(''),
|
||||
[Env.WAHA_PROMETHEUS_METRIC_PREFIX]: Joi.string().allow(''),
|
||||
[Env.WAHA_PROMETHEUS_HTTP_DURATION_BUCKETS]: Joi.string()
|
||||
.pattern(/^\d+(\.\d+)?(,\d+(\.\d+)?)*$/)
|
||||
.allow(''),
|
||||
[Env.WAHA_PROMETHEUS_USERNAME]: Joi.string().allow(''),
|
||||
[Env.WAHA_PROMETHEUS_PASSWORD]: Joi.string().allow(''),
|
||||
[Env.WAHA_PROMETHEUS_TRACK_EVENTS]: Joi.string().allow(''),
|
||||
});
|
||||
|
||||
export function isPrometheusEnabled(env: NodeJS.ProcessEnv): boolean {
|
||||
const value = env[Env.WAHA_PROMETHEUS_ENABLED];
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return parseBool(value);
|
||||
}
|
||||
|
||||
export function getPrometheusPath(): string {
|
||||
return process.env[Env.WAHA_PROMETHEUS_PATH] || DEFAULT_PATH;
|
||||
}
|
||||
|
||||
export function getPrometheusExcludePaths(): string[] {
|
||||
if (!isPrometheusEnabled(process.env)) {
|
||||
return [];
|
||||
}
|
||||
return [getPrometheusPath()];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PrometheusConfigService {
|
||||
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@InjectPinoLogger(PrometheusConfigService.name)
|
||||
private logger: PinoLogger,
|
||||
) {}
|
||||
|
||||
get path(): string {
|
||||
return getPrometheusPath();
|
||||
}
|
||||
|
||||
get metricPrefix(): string {
|
||||
return this.configService.get(
|
||||
Env.WAHA_PROMETHEUS_METRIC_PREFIX,
|
||||
DEFAULT_METRIC_PREFIX,
|
||||
);
|
||||
}
|
||||
|
||||
get httpDurationBuckets(): number[] {
|
||||
const value = this.configService.get(
|
||||
Env.WAHA_PROMETHEUS_HTTP_DURATION_BUCKETS,
|
||||
'',
|
||||
);
|
||||
if (!value) {
|
||||
return DEFAULT_HTTP_DURATION_BUCKETS;
|
||||
}
|
||||
return value.split(',').map((bucket) => parseFloat(bucket));
|
||||
}
|
||||
|
||||
get events(): WAHAEvents[] {
|
||||
const value = this.configService.get(Env.WAHA_PROMETHEUS_TRACK_EVENTS, '');
|
||||
if (!value) {
|
||||
return [WAHAEvents.MESSAGE_ANY];
|
||||
}
|
||||
const names = value
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
const result = this.eventUnmask.unmask(names);
|
||||
if (result.unknown.length > 0) {
|
||||
throw new Error(
|
||||
`${
|
||||
Env.WAHA_PROMETHEUS_TRACK_EVENTS
|
||||
} - unknown events '${result.unknown.join(', ')}', ` +
|
||||
`expected '*' or: ${Object.values(WAHAEvents).join(', ')}`,
|
||||
);
|
||||
}
|
||||
return result.events as WAHAEvents[];
|
||||
}
|
||||
|
||||
get credentials(): [string, string] | null {
|
||||
const username = this.configService.get(Env.WAHA_PROMETHEUS_USERNAME, '');
|
||||
const password = this.configService.get(Env.WAHA_PROMETHEUS_PASSWORD, '');
|
||||
if (!username && !password) {
|
||||
return null;
|
||||
}
|
||||
if (!username || !password) {
|
||||
this.logger.warn(
|
||||
'Set up both WAHA_PROMETHEUS_USERNAME and WAHA_PROMETHEUS_PASSWORD ' +
|
||||
'to enable metrics endpoint authentication.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return [username, password];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Controller, Get, Header, Res } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { getPrometheusPath } from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { WahaMetrics } from '@waha/modules/waha-prometheus/prometheus.metrics';
|
||||
import { SessionMetricsCollector } from '@waha/modules/waha-prometheus/prometheus.sessions.collector';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Controller(getPrometheusPath())
|
||||
@ApiTags('🔍 Observability')
|
||||
export class PrometheusController {
|
||||
constructor(
|
||||
private metrics: WahaMetrics,
|
||||
private collector: SessionMetricsCollector,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Header('Cache-Control', 'no-store')
|
||||
@ApiOperation({
|
||||
summary: 'Prometheus metrics',
|
||||
description:
|
||||
'Prometheus text exposition format. ' +
|
||||
'Enable with WAHA_PROMETHEUS_ENABLED=True, configure with WAHA_PROMETHEUS_* environment variables.',
|
||||
})
|
||||
async get(@Res() response: Response): Promise<void> {
|
||||
await this.collector.collect();
|
||||
const body = await this.metrics.render();
|
||||
response.set('Content-Type', this.metrics.contentType);
|
||||
response.send(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Injectable,
|
||||
OnApplicationBootstrap,
|
||||
OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { PrometheusConfigService } from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { WahaMetrics } from '@waha/modules/waha-prometheus/prometheus.metrics';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class EventMetricsSubscriber
|
||||
implements OnApplicationBootstrap, OnModuleDestroy
|
||||
{
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
private metrics: WahaMetrics,
|
||||
private config: PrometheusConfigService,
|
||||
) {}
|
||||
|
||||
onApplicationBootstrap(): void {
|
||||
const manager = this.moduleRef.get(SessionManager, { strict: false });
|
||||
for (const eventName of this.config.events) {
|
||||
this.subscriptions.push(
|
||||
manager.getSessionEvent('*', eventName).subscribe((event) => {
|
||||
const session = event?.session;
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
this.metrics.incrementEvent(session, eventName);
|
||||
const fromMe = event?.payload?.fromMe;
|
||||
if (
|
||||
eventName === WAHAEvents.MESSAGE_ANY &&
|
||||
typeof fromMe === 'boolean'
|
||||
) {
|
||||
this.metrics.incrementMessage(session, fromMe);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.subscriptions.push(
|
||||
manager
|
||||
.getSessionEvent('*', WAHAEvents.SESSION_STATUS)
|
||||
.subscribe((event) => {
|
||||
const session = event?.session;
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
const timestampMs = event.timestamp ?? Date.now();
|
||||
this.metrics.observeSessionStatusChange(session, timestampMs);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onModuleDestroy(): void {
|
||||
for (const subscription of this.subscriptions) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
this.subscriptions = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { PrometheusConfigService } from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { WahaMetrics } from '@waha/modules/waha-prometheus/prometheus.metrics';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class HttpMetricsMiddleware implements NestMiddleware {
|
||||
constructor(
|
||||
private metrics: WahaMetrics,
|
||||
private config: PrometheusConfigService,
|
||||
) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction): void {
|
||||
const pathname = (req.originalUrl || req.url || '').split('?')[0];
|
||||
if (pathname.startsWith(this.config.path)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const start = process.hrtime.bigint();
|
||||
res.on('finish', () => {
|
||||
const durationSeconds = Number(process.hrtime.bigint() - start) / 1e9;
|
||||
this.metrics.observeHttpRequest(
|
||||
req.method,
|
||||
res.statusCode,
|
||||
durationSeconds,
|
||||
);
|
||||
});
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrometheusConfigService } from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { VERSION } from '@waha/version';
|
||||
import {
|
||||
collectDefaultMetrics,
|
||||
Counter,
|
||||
Gauge,
|
||||
Histogram,
|
||||
Registry,
|
||||
} from '@prometheus-io/client';
|
||||
|
||||
export interface SessionCountRow {
|
||||
name: string;
|
||||
status: string;
|
||||
engine: string;
|
||||
activityTimestampMs: number | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WahaMetrics {
|
||||
readonly registry: Registry;
|
||||
private readonly httpRequests: Counter<'method' | 'status'>;
|
||||
private readonly httpDuration: Histogram<'method' | 'status'>;
|
||||
private readonly sessions: Gauge<'status' | 'engine'>;
|
||||
private readonly sessionStatus: Gauge<'session' | 'status' | 'engine'>;
|
||||
private readonly sessionActivity: Gauge<'session'>;
|
||||
private readonly sessionStatusChange: Gauge<'session'>;
|
||||
private readonly messages: Counter<'session' | 'fromMe'>;
|
||||
private readonly events: Counter<'session' | 'event'>;
|
||||
|
||||
constructor(config: PrometheusConfigService) {
|
||||
this.registry = new Registry();
|
||||
// always keep the worker label, even if it's empty (set an empty string, otherwise it renders as worker="null")
|
||||
this.registry.setDefaultLabels({ worker: VERSION.worker.id || '' });
|
||||
const prefix = config.metricPrefix;
|
||||
collectDefaultMetrics({ register: this.registry, prefix: prefix });
|
||||
const up = new Gauge({
|
||||
name: `${prefix}up`,
|
||||
help: '1 if the WAHA process is serving Prometheus metrics',
|
||||
registers: [this.registry],
|
||||
});
|
||||
up.set(1);
|
||||
const info = new Gauge({
|
||||
name: `${prefix}info`,
|
||||
help: 'WAHA build information',
|
||||
labelNames: ['version', 'tier', 'engine', 'platform'],
|
||||
registers: [this.registry],
|
||||
});
|
||||
info.set(
|
||||
{
|
||||
version: VERSION.version,
|
||||
tier: VERSION.tier,
|
||||
engine: VERSION.engine,
|
||||
platform: VERSION.platform,
|
||||
},
|
||||
1,
|
||||
);
|
||||
this.httpRequests = new Counter({
|
||||
name: `${prefix}http_requests_total`,
|
||||
help: 'API HTTP requests handled by WAHA',
|
||||
labelNames: ['method', 'status'],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.httpDuration = new Histogram({
|
||||
name: `${prefix}http_request_duration_seconds`,
|
||||
help: 'API HTTP request duration in seconds',
|
||||
labelNames: ['method', 'status'],
|
||||
buckets: config.httpDurationBuckets,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.sessions = new Gauge({
|
||||
name: `${prefix}sessions`,
|
||||
help: 'WhatsApp sessions by status and engine',
|
||||
labelNames: ['status', 'engine'],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.sessionStatus = new Gauge({
|
||||
name: `${prefix}session_status`,
|
||||
help: 'Current status per session (1 = session is in this status)',
|
||||
labelNames: ['session', 'status', 'engine'],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.sessionActivity = new Gauge({
|
||||
name: `${prefix}session_activity_timestamp_seconds`,
|
||||
help: 'Unix timestamp of the last session activity',
|
||||
labelNames: ['session'],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.sessionStatusChange = new Gauge({
|
||||
name: `${prefix}session_status_change_timestamp_seconds`,
|
||||
help: 'Unix timestamp of the last session status change',
|
||||
labelNames: ['session'],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.messages = new Counter({
|
||||
name: `${prefix}messages_total`,
|
||||
help: 'WhatsApp messages observed by WAHA',
|
||||
labelNames: ['session', 'fromMe'],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.events = new Counter({
|
||||
name: `${prefix}events_total`,
|
||||
help: 'WAHA events observed by WAHA (WAHA_PROMETHEUS_TRACK_EVENTS)',
|
||||
labelNames: ['session', 'event'],
|
||||
registers: [this.registry],
|
||||
});
|
||||
}
|
||||
|
||||
get contentType(): string {
|
||||
return this.registry.contentType;
|
||||
}
|
||||
|
||||
observeHttpRequest(
|
||||
method: string,
|
||||
statusCode: number,
|
||||
durationSeconds: number,
|
||||
): void {
|
||||
const labels = {
|
||||
method: method.toUpperCase(),
|
||||
status: String(statusCode),
|
||||
};
|
||||
this.httpRequests.inc(labels);
|
||||
this.httpDuration.observe(labels, durationSeconds);
|
||||
}
|
||||
|
||||
incrementMessage(session: string, fromMe: boolean): void {
|
||||
this.messages.inc({ session: session, fromMe: String(fromMe) });
|
||||
}
|
||||
|
||||
incrementEvent(session: string, event: string): void {
|
||||
this.events.inc({ session: session, event: event });
|
||||
}
|
||||
|
||||
observeSessionStatusChange(session: string, timestampMs: number): void {
|
||||
this.sessionStatusChange.set({ session: session }, timestampMs / 1000);
|
||||
}
|
||||
|
||||
setSessionCounts(rows: SessionCountRow[]): void {
|
||||
this.sessions.reset();
|
||||
this.sessionStatus.reset();
|
||||
this.sessionActivity.reset();
|
||||
for (const row of rows) {
|
||||
this.sessions.inc({ status: row.status, engine: row.engine });
|
||||
this.sessionStatus.set(
|
||||
{ session: row.name, status: row.status, engine: row.engine },
|
||||
1,
|
||||
);
|
||||
if (row.activityTimestampMs) {
|
||||
this.sessionActivity.set(
|
||||
{ session: row.name },
|
||||
row.activityTimestampMs / 1000,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(): Promise<string> {
|
||||
return this.registry.metrics();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
import {
|
||||
PrometheusConfigService,
|
||||
PrometheusEnvSchema,
|
||||
} from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { PrometheusController } from '@waha/modules/waha-prometheus/prometheus.controller';
|
||||
import { EventMetricsSubscriber } from '@waha/modules/waha-prometheus/prometheus.events.subscriber';
|
||||
import { HttpMetricsMiddleware } from '@waha/modules/waha-prometheus/prometheus.http.middleware';
|
||||
import { WahaMetrics } from '@waha/modules/waha-prometheus/prometheus.metrics';
|
||||
import { SessionMetricsCollector } from '@waha/modules/waha-prometheus/prometheus.sessions.collector';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
validationSchema: PrometheusEnvSchema,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
PrometheusConfigService,
|
||||
WahaMetrics,
|
||||
HttpMetricsMiddleware,
|
||||
SessionMetricsCollector,
|
||||
EventMetricsSubscriber,
|
||||
],
|
||||
controllers: [PrometheusController],
|
||||
})
|
||||
/**
|
||||
* Prometheus metrics endpoint (WAHA_PROMETHEUS_ENABLED=True).
|
||||
* Serves GET /metrics (WAHA_PROMETHEUS_PATH) with process, HTTP, session and message metrics;
|
||||
* optional basic auth via WAHA_PROMETHEUS_USERNAME and WAHA_PROMETHEUS_PASSWORD.
|
||||
*/
|
||||
export class PrometheusModule implements NestModule {
|
||||
constructor(private config: PrometheusConfigService) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
const credentials = this.config.credentials;
|
||||
if (credentials) {
|
||||
const username = credentials[0];
|
||||
const password = credentials[1];
|
||||
consumer
|
||||
.apply(BasicAuthFunction(username, password))
|
||||
.forRoutes(this.config.path);
|
||||
}
|
||||
consumer.apply(HttpMetricsMiddleware).forRoutes('api', 'mcp');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import {
|
||||
SessionCountRow,
|
||||
WahaMetrics,
|
||||
} from '@waha/modules/waha-prometheus/prometheus.metrics';
|
||||
import { getEngineName } from '@waha/version';
|
||||
|
||||
@Injectable()
|
||||
export class SessionMetricsCollector {
|
||||
private manager: SessionManager | null = null;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
private metrics: WahaMetrics,
|
||||
) {}
|
||||
|
||||
private getManager(): SessionManager {
|
||||
if (!this.manager) {
|
||||
this.manager = this.moduleRef.get(SessionManager, { strict: false });
|
||||
}
|
||||
return this.manager;
|
||||
}
|
||||
|
||||
async collect(): Promise<void> {
|
||||
const manager = this.getManager();
|
||||
const sessions = await manager.getSessions(true);
|
||||
const rows: SessionCountRow[] = [];
|
||||
for (const info of sessions) {
|
||||
let engine: string;
|
||||
try {
|
||||
engine = String(manager.getSession(info.name).engine);
|
||||
} catch {
|
||||
engine = String(getEngineName());
|
||||
}
|
||||
rows.push({
|
||||
name: info.name,
|
||||
status: String(info.status),
|
||||
engine: engine,
|
||||
activityTimestampMs: info.timestamps?.activity ?? null,
|
||||
});
|
||||
}
|
||||
this.metrics.setSessionCounts(rows);
|
||||
}
|
||||
}
|
||||
@@ -3205,7 +3205,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.1":
|
||||
"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0, @opentelemetry/api@npm:^1.9.1":
|
||||
version: 1.9.1
|
||||
resolution: "@opentelemetry/api@npm:1.9.1"
|
||||
checksum: 10/b26032739d3c54ca99b5a2920844a1fbd4c3ee383cacbb0915e8c706a2626fe91e96feaa6e893397abe0545dc8d0a765b220aa18a31b1773176eeaf3a225e10e
|
||||
@@ -3710,6 +3710,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@prometheus-io/client@npm:^0.16.1":
|
||||
version: 0.16.1
|
||||
resolution: "@prometheus-io/client@npm:0.16.1"
|
||||
dependencies:
|
||||
"@opentelemetry/api": "npm:^1.4.0"
|
||||
tdigest: "npm:^0.1.1"
|
||||
checksum: 10/6e8cdf0fbafc8506e3b5fb32815504fdfcf702e06b0d5d3d345a41ecf7e726e9a8e23b536eded80fdfb2073e14baee1622819129802104984c9fc179d1787048
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "@protobufjs/aspromise@npm:1.1.2"
|
||||
@@ -6093,6 +6103,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bintrees@npm:1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "bintrees@npm:1.0.2"
|
||||
checksum: 10/071896cea5ea5413316c8436e95799444c208630d5c539edd8a7089fc272fc5d3634aa4a2e4847b28350dda1796162e14a34a0eda53108cc5b3c2ff6a036c1fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bl@npm:^4.0.3, bl@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "bl@npm:4.1.0"
|
||||
@@ -13980,6 +13997,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tdigest@npm:^0.1.1":
|
||||
version: 0.1.3
|
||||
resolution: "tdigest@npm:0.1.3"
|
||||
dependencies:
|
||||
bintrees: "npm:1.0.2"
|
||||
checksum: 10/c4a0fe3469ddc1eea56938c26215b5bd690efb93ef583872c09baf81646c15acb077d9253e72e39e2655ed140410ca901bb75b77a7d29fcecf77a2efde91e52b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"terser-webpack-plugin@npm:^5.3.11":
|
||||
version: 5.3.14
|
||||
resolution: "terser-webpack-plugin@npm:5.3.14"
|
||||
@@ -14679,6 +14705,7 @@ __metadata:
|
||||
"@opentelemetry/instrumentation-pino": "npm:^0.67.0"
|
||||
"@opentelemetry/sdk-node": "npm:^0.221.0"
|
||||
"@opentelemetry/sdk-trace": "npm:^2.10.0"
|
||||
"@prometheus-io/client": "npm:^0.16.1"
|
||||
"@types/better-sqlite3": "npm:^7.6.10"
|
||||
"@types/express": "npm:^4.17.3"
|
||||
"@types/jest": "npm:26.0.10"
|
||||
|
||||
Reference in new issue
Block a user