import config from '@/config'; import { N8N_VERSION } from '@/constants'; import type express from 'express'; import promBundle from 'express-prom-bundle'; import promClient, { type Counter } from 'prom-client'; import semverParse from 'semver/functions/parse'; import { Service } from 'typedi'; import EventEmitter from 'events'; import { CacheService } from '@/services/cache.service'; import type { EventMessageTypes } from '@/eventbus/EventMessageClasses'; import { METRICS_EVENT_NAME, getLabelsForEvent, } from '@/eventbus/MessageEventBusDestination/Helpers.ee'; import { eventBus } from '@/eventbus'; import { Logger } from '@/Logger'; @Service() export class MetricsService extends EventEmitter { constructor( private readonly logger: Logger, private readonly cacheService: CacheService, ) { super(); } counters: Record | null> = {}; async configureMetrics(app: express.Application) { promClient.register.clear(); // clear all metrics in case we call this a second time this.setupDefaultMetrics(); this.setupN8nVersionMetric(); this.setupCacheMetrics(); this.setupMessageEventBusMetrics(); this.setupApiMetrics(app); this.mountMetricsEndpoint(app); } private setupN8nVersionMetric() { const n8nVersion = semverParse(N8N_VERSION || '0.0.0'); if (n8nVersion) { const versionGauge = new promClient.Gauge({ name: config.getEnv('endpoints.metrics.prefix') + 'version_info', help: 'n8n version info.', labelNames: ['version', 'major', 'minor', 'patch'], }); versionGauge.set( { version: 'v' + n8nVersion.version, major: n8nVersion.major, minor: n8nVersion.minor, patch: n8nVersion.patch, }, 1, ); } } private setupDefaultMetrics() { if (config.getEnv('endpoints.metrics.includeDefaultMetrics')) { promClient.collectDefaultMetrics(); } } private setupApiMetrics(app: express.Application) { if (config.getEnv('endpoints.metrics.includeApiEndpoints')) { const metricsMiddleware = promBundle({ autoregister: false, includeUp: false, includePath: config.getEnv('endpoints.metrics.includeApiPathLabel'), includeMethod: config.getEnv('endpoints.metrics.includeApiMethodLabel'), includeStatusCode: config.getEnv('endpoints.metrics.includeApiStatusCodeLabel'), }); app.use(['/rest/', '/webhook/', 'webhook-test/', '/api/'], metricsMiddleware); } } mountMetricsEndpoint(app: express.Application) { app.get('/metrics', async (req: express.Request, res: express.Response) => { const metrics = await promClient.register.metrics(); res.setHeader('Content-Type', promClient.register.contentType); res.send(metrics).end(); }); } private setupCacheMetrics() { if (!config.getEnv('endpoints.metrics.includeCacheMetrics')) { return; } this.counters.cacheHitsTotal = new promClient.Counter({ name: config.getEnv('endpoints.metrics.prefix') + 'cache_hits_total', help: 'Total number of cache hits.', labelNames: ['cache'], }); this.counters.cacheHitsTotal.inc(0); this.cacheService.on(this.cacheService.metricsCounterEvents.cacheHit, (amount: number = 1) => { this.counters.cacheHitsTotal?.inc(amount); }); this.counters.cacheMissesTotal = new promClient.Counter({ name: config.getEnv('endpoints.metrics.prefix') + 'cache_misses_total', help: 'Total number of cache misses.', labelNames: ['cache'], }); this.counters.cacheMissesTotal.inc(0); this.cacheService.on(this.cacheService.metricsCounterEvents.cacheMiss, (amount: number = 1) => { this.counters.cacheMissesTotal?.inc(amount); }); this.counters.cacheUpdatesTotal = new promClient.Counter({ name: config.getEnv('endpoints.metrics.prefix') + 'cache_updates_total', help: 'Total number of cache updates.', labelNames: ['cache'], }); this.counters.cacheUpdatesTotal.inc(0); this.cacheService.on( this.cacheService.metricsCounterEvents.cacheUpdate, (amount: number = 1) => { this.counters.cacheUpdatesTotal?.inc(amount); }, ); } private getCounterForEvent(event: EventMessageTypes): Counter | null { if (!promClient) return null; if (!this.counters[event.eventName]) { const prefix = config.getEnv('endpoints.metrics.prefix'); const metricName = prefix + event.eventName.replace('n8n.', '').replace(/\./g, '_') + '_total'; if (!promClient.validateMetricName(metricName)) { this.logger.debug(`Invalid metric name: ${metricName}. Ignoring it!`); this.counters[event.eventName] = null; return null; } const counter = new promClient.Counter({ name: metricName, help: `Total number of ${event.eventName} events.`, labelNames: Object.keys(getLabelsForEvent(event)), }); counter.inc(0); this.counters[event.eventName] = counter; } return this.counters[event.eventName]; } private setupMessageEventBusMetrics() { if (!config.getEnv('endpoints.metrics.includeMessageEventBusMetrics')) { return; } eventBus.on(METRICS_EVENT_NAME, (event: EventMessageTypes) => { const counter = this.getCounterForEvent(event); if (!counter) return; counter.inc(1); }); } }