From b7c4eac93b52d069429c503ea380e6a325fa48de Mon Sep 17 00:00:00 2001 From: devlikepro Date: Mon, 31 Aug 2026 16:17:50 +0700 Subject: [PATCH] feat: Prometheus support - GET /metrics - closes #2245 fix #2245 --- .env.example | 14 ++ package.json | 1 + src/core/SwaggerConfiguratorCore.ts | 2 + src/core/app.module.core.ts | 9 + .../waha-prometheus/prometheus.config.ts | 124 ++++++++++++++ .../waha-prometheus/prometheus.controller.ts | 30 ++++ .../prometheus.events.subscriber.ts | 65 +++++++ .../prometheus.http.middleware.ts | 30 ++++ .../waha-prometheus/prometheus.metrics.ts | 160 ++++++++++++++++++ .../waha-prometheus/prometheus.module.ts | 48 ++++++ .../prometheus.sessions.collector.ts | 46 +++++ yarn.lock | 29 +++- 12 files changed, 557 insertions(+), 1 deletion(-) create mode 100644 src/modules/waha-prometheus/prometheus.config.ts create mode 100644 src/modules/waha-prometheus/prometheus.controller.ts create mode 100644 src/modules/waha-prometheus/prometheus.events.subscriber.ts create mode 100644 src/modules/waha-prometheus/prometheus.http.middleware.ts create mode 100644 src/modules/waha-prometheus/prometheus.metrics.ts create mode 100644 src/modules/waha-prometheus/prometheus.module.ts create mode 100644 src/modules/waha-prometheus/prometheus.sessions.collector.ts diff --git a/.env.example b/.env.example index a229878d..f5869923 100644 --- a/.env.example +++ b/.env.example @@ -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 ===== # ========================= diff --git a/package.json b/package.json index ec147c84..469dbcc2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/core/SwaggerConfiguratorCore.ts b/src/core/SwaggerConfiguratorCore.ts index 1b1fff5a..7d230f01 100644 --- a/src/core/SwaggerConfiguratorCore.ts +++ b/src/core/SwaggerConfiguratorCore.ts @@ -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); diff --git a/src/core/app.module.core.ts b/src/core/app.module.core.ts index 110cc7a3..832265c6 100644 --- a/src/core/app.module.core.ts +++ b/src/core/app.module.core.ts @@ -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 = [ diff --git a/src/modules/waha-prometheus/prometheus.config.ts b/src/modules/waha-prometheus/prometheus.config.ts new file mode 100644 index 00000000..f5c273c4 --- /dev/null +++ b/src/modules/waha-prometheus/prometheus.config.ts @@ -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]; + } +} diff --git a/src/modules/waha-prometheus/prometheus.controller.ts b/src/modules/waha-prometheus/prometheus.controller.ts new file mode 100644 index 00000000..52103646 --- /dev/null +++ b/src/modules/waha-prometheus/prometheus.controller.ts @@ -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 { + await this.collector.collect(); + const body = await this.metrics.render(); + response.set('Content-Type', this.metrics.contentType); + response.send(body); + } +} diff --git a/src/modules/waha-prometheus/prometheus.events.subscriber.ts b/src/modules/waha-prometheus/prometheus.events.subscriber.ts new file mode 100644 index 00000000..d73e9741 --- /dev/null +++ b/src/modules/waha-prometheus/prometheus.events.subscriber.ts @@ -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 = []; + } +} diff --git a/src/modules/waha-prometheus/prometheus.http.middleware.ts b/src/modules/waha-prometheus/prometheus.http.middleware.ts new file mode 100644 index 00000000..2970fb28 --- /dev/null +++ b/src/modules/waha-prometheus/prometheus.http.middleware.ts @@ -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(); + } +} diff --git a/src/modules/waha-prometheus/prometheus.metrics.ts b/src/modules/waha-prometheus/prometheus.metrics.ts new file mode 100644 index 00000000..3d6e428d --- /dev/null +++ b/src/modules/waha-prometheus/prometheus.metrics.ts @@ -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 { + return this.registry.metrics(); + } +} diff --git a/src/modules/waha-prometheus/prometheus.module.ts b/src/modules/waha-prometheus/prometheus.module.ts new file mode 100644 index 00000000..61a34887 --- /dev/null +++ b/src/modules/waha-prometheus/prometheus.module.ts @@ -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'); + } +} diff --git a/src/modules/waha-prometheus/prometheus.sessions.collector.ts b/src/modules/waha-prometheus/prometheus.sessions.collector.ts new file mode 100644 index 00000000..7dedfb68 --- /dev/null +++ b/src/modules/waha-prometheus/prometheus.sessions.collector.ts @@ -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 { + 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); + } +} diff --git a/yarn.lock b/yarn.lock index fd27fc02..1cc0de8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"