2024-09-12 09:07:18 -07:00
|
|
|
import { GlobalConfig } from '@n8n/config';
|
2023-08-04 11:51:07 -07:00
|
|
|
import type express from 'express';
|
|
|
|
import promBundle from 'express-prom-bundle';
|
2024-09-18 02:16:17 -07:00
|
|
|
import { InstanceSettings } from 'n8n-core';
|
2024-09-12 09:07:18 -07:00
|
|
|
import { EventMessageTypeNames } from 'n8n-workflow';
|
2024-08-28 02:36:00 -07:00
|
|
|
import promClient, { type Counter, type Gauge } from 'prom-client';
|
2023-08-04 11:51:07 -07:00
|
|
|
import semverParse from 'semver/functions/parse';
|
|
|
|
import { Service } from 'typedi';
|
|
|
|
|
2024-09-12 09:07:18 -07:00
|
|
|
import config from '@/config';
|
|
|
|
import { N8N_VERSION } from '@/constants';
|
2024-07-18 01:27:35 -07:00
|
|
|
import type { EventMessageTypes } from '@/eventbus';
|
2024-09-12 09:07:18 -07:00
|
|
|
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
2024-08-28 02:36:00 -07:00
|
|
|
import { EventService } from '@/events/event.service';
|
2024-09-12 09:07:18 -07:00
|
|
|
import { CacheService } from '@/services/cache/cache.service';
|
|
|
|
|
|
|
|
import type { Includes, MetricCategory, MetricLabel } from './types';
|
2023-08-04 11:51:07 -07:00
|
|
|
|
|
|
|
@Service()
|
2024-07-18 01:27:35 -07:00
|
|
|
export class PrometheusMetricsService {
|
2023-10-25 07:35:22 -07:00
|
|
|
constructor(
|
|
|
|
private readonly cacheService: CacheService,
|
2024-01-26 03:21:15 -08:00
|
|
|
private readonly eventBus: MessageEventBus,
|
2024-07-31 08:45:11 -07:00
|
|
|
private readonly globalConfig: GlobalConfig,
|
2024-08-28 02:36:00 -07:00
|
|
|
private readonly eventService: EventService,
|
2024-09-18 02:16:17 -07:00
|
|
|
private readonly instanceSettings: InstanceSettings,
|
2024-07-18 01:27:35 -07:00
|
|
|
) {}
|
|
|
|
|
|
|
|
private readonly counters: { [key: string]: Counter<string> | null } = {};
|
|
|
|
|
2024-08-28 02:36:00 -07:00
|
|
|
private readonly gauges: Record<string, Gauge<string>> = {};
|
|
|
|
|
2024-07-31 08:45:11 -07:00
|
|
|
private readonly prefix = this.globalConfig.endpoints.metrics.prefix;
|
2024-07-18 01:27:35 -07:00
|
|
|
|
2024-07-22 03:01:44 -07:00
|
|
|
private readonly includes: Includes = {
|
2024-07-18 01:27:35 -07:00
|
|
|
metrics: {
|
2024-07-31 08:45:11 -07:00
|
|
|
default: this.globalConfig.endpoints.metrics.includeDefaultMetrics,
|
|
|
|
routes: this.globalConfig.endpoints.metrics.includeApiEndpoints,
|
|
|
|
cache: this.globalConfig.endpoints.metrics.includeCacheMetrics,
|
|
|
|
logs: this.globalConfig.endpoints.metrics.includeMessageEventBusMetrics,
|
2024-08-28 02:36:00 -07:00
|
|
|
queue: this.globalConfig.endpoints.metrics.includeQueueMetrics,
|
2024-07-18 01:27:35 -07:00
|
|
|
},
|
|
|
|
labels: {
|
2024-07-31 08:45:11 -07:00
|
|
|
credentialsType: this.globalConfig.endpoints.metrics.includeCredentialTypeLabel,
|
|
|
|
nodeType: this.globalConfig.endpoints.metrics.includeNodeTypeLabel,
|
|
|
|
workflowId: this.globalConfig.endpoints.metrics.includeWorkflowIdLabel,
|
|
|
|
apiPath: this.globalConfig.endpoints.metrics.includeApiPathLabel,
|
|
|
|
apiMethod: this.globalConfig.endpoints.metrics.includeApiMethodLabel,
|
|
|
|
apiStatusCode: this.globalConfig.endpoints.metrics.includeApiStatusCodeLabel,
|
2024-07-18 01:27:35 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
async init(app: express.Application) {
|
2023-08-04 11:51:07 -07:00
|
|
|
promClient.register.clear(); // clear all metrics in case we call this a second time
|
2024-07-18 01:27:35 -07:00
|
|
|
this.initDefaultMetrics();
|
|
|
|
this.initN8nVersionMetric();
|
|
|
|
this.initCacheMetrics();
|
|
|
|
this.initEventBusMetrics();
|
2024-07-19 04:27:08 -07:00
|
|
|
this.initRouteMetrics(app);
|
2024-08-28 02:36:00 -07:00
|
|
|
this.initQueueMetrics();
|
2023-08-04 11:51:07 -07:00
|
|
|
this.mountMetricsEndpoint(app);
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
enableMetric(metric: MetricCategory) {
|
|
|
|
this.includes.metrics[metric] = true;
|
|
|
|
}
|
2023-08-04 11:51:07 -07:00
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
disableMetric(metric: MetricCategory) {
|
|
|
|
this.includes.metrics[metric] = false;
|
|
|
|
}
|
2023-08-04 11:51:07 -07:00
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
disableAllMetrics() {
|
2024-07-22 03:01:44 -07:00
|
|
|
for (const metric in this.includes.metrics) {
|
|
|
|
this.includes.metrics[metric as MetricCategory] = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enableLabels(labels: MetricLabel[]) {
|
|
|
|
for (const label of labels) {
|
|
|
|
this.includes.labels[label] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
disableAllLabels() {
|
|
|
|
for (const label in this.includes.labels) {
|
|
|
|
this.includes.labels[label as MetricLabel] = false;
|
2023-08-04 11:51:07 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
/**
|
|
|
|
* Set up metric for n8n version: `n8n_version_info`
|
|
|
|
*/
|
|
|
|
private initN8nVersionMetric() {
|
|
|
|
const n8nVersion = semverParse(N8N_VERSION ?? '0.0.0');
|
|
|
|
|
|
|
|
if (!n8nVersion) return;
|
|
|
|
|
|
|
|
const versionGauge = new promClient.Gauge({
|
|
|
|
name: this.prefix + 'version_info',
|
|
|
|
help: 'n8n version info.',
|
|
|
|
labelNames: ['version', 'major', 'minor', 'patch'],
|
|
|
|
});
|
|
|
|
|
|
|
|
const { version, major, minor, patch } = n8nVersion;
|
|
|
|
|
|
|
|
versionGauge.set({ version: 'v' + version, major, minor, patch }, 1);
|
2023-08-04 11:51:07 -07:00
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
/**
|
|
|
|
* Set up default metrics collection with `prom-client`, e.g.
|
|
|
|
* `process_cpu_seconds_total`, `process_resident_memory_bytes`, etc.
|
|
|
|
*/
|
|
|
|
private initDefaultMetrics() {
|
|
|
|
if (!this.includes.metrics.default) return;
|
2023-08-04 11:51:07 -07:00
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
promClient.collectDefaultMetrics();
|
2023-08-04 11:51:07 -07:00
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
/**
|
2024-07-19 04:27:08 -07:00
|
|
|
* Set up metrics for server routes with `express-prom-bundle`
|
2024-07-18 01:27:35 -07:00
|
|
|
*/
|
2024-07-19 04:27:08 -07:00
|
|
|
private initRouteMetrics(app: express.Application) {
|
2024-07-22 03:01:44 -07:00
|
|
|
if (!this.includes.metrics.routes) return;
|
2024-07-18 01:27:35 -07:00
|
|
|
|
|
|
|
const metricsMiddleware = promBundle({
|
|
|
|
autoregister: false,
|
|
|
|
includeUp: false,
|
|
|
|
includePath: this.includes.labels.apiPath,
|
|
|
|
includeMethod: this.includes.labels.apiMethod,
|
|
|
|
includeStatusCode: this.includes.labels.apiStatusCode,
|
|
|
|
});
|
|
|
|
|
|
|
|
app.use(
|
2024-07-19 04:27:08 -07:00
|
|
|
[
|
|
|
|
'/rest/',
|
|
|
|
'/api/',
|
|
|
|
'/webhook/',
|
|
|
|
'/webhook-waiting/',
|
|
|
|
'/webhook-test/',
|
|
|
|
'/form/',
|
|
|
|
'/form-waiting/',
|
|
|
|
'/form-test/',
|
|
|
|
],
|
2024-07-18 01:27:35 -07:00
|
|
|
metricsMiddleware,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private mountMetricsEndpoint(app: express.Application) {
|
2024-05-31 05:06:13 -07:00
|
|
|
app.get('/metrics', async (_req: express.Request, res: express.Response) => {
|
2023-08-04 11:51:07 -07:00
|
|
|
const metrics = await promClient.register.metrics();
|
2024-07-22 03:01:44 -07:00
|
|
|
const prefixedMetrics = this.addPrefixToMetrics(metrics);
|
2023-08-04 11:51:07 -07:00
|
|
|
res.setHeader('Content-Type', promClient.register.contentType);
|
2024-07-22 03:01:44 -07:00
|
|
|
res.send(prefixedMetrics).end();
|
2023-08-04 11:51:07 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-07-22 03:01:44 -07:00
|
|
|
private addPrefixToMetrics(metrics: string) {
|
|
|
|
return metrics
|
|
|
|
.split('\n')
|
|
|
|
.map((rawLine) => {
|
|
|
|
const line = rawLine.trim();
|
|
|
|
|
|
|
|
if (!line || line.startsWith('#') || line.startsWith(this.prefix)) return rawLine;
|
|
|
|
|
|
|
|
return this.prefix + line;
|
|
|
|
})
|
|
|
|
.join('\n');
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
/**
|
|
|
|
* Set up cache metrics: `n8n_cache_hits_total`, `n8n_cache_misses_total`, and
|
|
|
|
* `n8n_cache_updates_total`
|
|
|
|
*/
|
|
|
|
private initCacheMetrics() {
|
|
|
|
if (!this.includes.metrics.cache) return;
|
|
|
|
|
|
|
|
const [hitsConfig, missesConfig, updatesConfig] = ['hits', 'misses', 'updates'].map((kind) => ({
|
|
|
|
name: this.prefix + 'cache_' + kind + '_total',
|
|
|
|
help: `Total number of cache ${kind}.`,
|
2023-08-04 11:51:07 -07:00
|
|
|
labelNames: ['cache'],
|
2024-07-18 01:27:35 -07:00
|
|
|
}));
|
|
|
|
|
|
|
|
this.counters.cacheHitsTotal = new promClient.Counter(hitsConfig);
|
2023-08-04 11:51:07 -07:00
|
|
|
this.counters.cacheHitsTotal.inc(0);
|
2024-07-18 01:27:35 -07:00
|
|
|
this.cacheService.on('metrics.cache.hit', () => this.counters.cacheHitsTotal?.inc(1));
|
2023-08-04 11:51:07 -07:00
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
this.counters.cacheMissesTotal = new promClient.Counter(missesConfig);
|
2023-08-04 11:51:07 -07:00
|
|
|
this.counters.cacheMissesTotal.inc(0);
|
2024-07-18 01:27:35 -07:00
|
|
|
this.cacheService.on('metrics.cache.miss', () => this.counters.cacheMissesTotal?.inc(1));
|
2023-08-04 11:51:07 -07:00
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
this.counters.cacheUpdatesTotal = new promClient.Counter(updatesConfig);
|
2023-08-04 11:51:07 -07:00
|
|
|
this.counters.cacheUpdatesTotal.inc(0);
|
2024-07-18 01:27:35 -07:00
|
|
|
this.cacheService.on('metrics.cache.update', () => this.counters.cacheUpdatesTotal?.inc(1));
|
2023-08-04 11:51:07 -07:00
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
private toCounter(event: EventMessageTypes) {
|
|
|
|
const { eventName } = event;
|
|
|
|
|
|
|
|
if (!this.counters[eventName]) {
|
|
|
|
const metricName = this.prefix + eventName.replace('n8n.', '').replace(/\./g, '_') + '_total';
|
2023-08-04 11:51:07 -07:00
|
|
|
|
|
|
|
if (!promClient.validateMetricName(metricName)) {
|
2024-07-18 01:27:35 -07:00
|
|
|
this.counters[eventName] = null;
|
2023-08-04 11:51:07 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
const labels = this.toLabels(event);
|
|
|
|
|
2023-08-04 11:51:07 -07:00
|
|
|
const counter = new promClient.Counter({
|
|
|
|
name: metricName,
|
2024-07-18 01:27:35 -07:00
|
|
|
help: `Total number of ${eventName} events.`,
|
|
|
|
labelNames: Object.keys(labels),
|
2023-08-04 11:51:07 -07:00
|
|
|
});
|
2024-07-18 01:27:35 -07:00
|
|
|
counter.labels(labels).inc(0);
|
|
|
|
this.counters[eventName] = counter;
|
2023-08-04 11:51:07 -07:00
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
return this.counters[eventName];
|
2023-08-04 11:51:07 -07:00
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
private initEventBusMetrics() {
|
|
|
|
if (!this.includes.metrics.logs) return;
|
|
|
|
|
|
|
|
this.eventBus.on('metrics.eventBus.event', (event: EventMessageTypes) => {
|
|
|
|
const counter = this.toCounter(event);
|
2023-08-04 11:51:07 -07:00
|
|
|
if (!counter) return;
|
|
|
|
counter.inc(1);
|
|
|
|
});
|
|
|
|
}
|
2024-06-11 00:11:39 -07:00
|
|
|
|
2024-08-28 02:36:00 -07:00
|
|
|
private initQueueMetrics() {
|
2024-09-18 02:16:17 -07:00
|
|
|
if (
|
|
|
|
!this.includes.metrics.queue ||
|
|
|
|
config.getEnv('executions.mode') !== 'queue' ||
|
|
|
|
this.instanceSettings.instanceType !== 'main'
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
2024-08-28 02:36:00 -07:00
|
|
|
|
|
|
|
this.gauges.waiting = new promClient.Gauge({
|
|
|
|
name: this.prefix + 'scaling_mode_queue_jobs_waiting',
|
|
|
|
help: 'Current number of enqueued jobs waiting for pickup in scaling mode.',
|
|
|
|
});
|
|
|
|
|
|
|
|
this.gauges.active = new promClient.Gauge({
|
|
|
|
name: this.prefix + 'scaling_mode_queue_jobs_active',
|
|
|
|
help: 'Current number of jobs being processed across all workers in scaling mode.',
|
|
|
|
});
|
|
|
|
|
|
|
|
this.counters.completed = new promClient.Counter({
|
|
|
|
name: this.prefix + 'scaling_mode_queue_jobs_completed',
|
|
|
|
help: 'Total number of jobs completed across all workers in scaling mode since instance start.',
|
|
|
|
});
|
|
|
|
|
|
|
|
this.counters.failed = new promClient.Counter({
|
|
|
|
name: this.prefix + 'scaling_mode_queue_jobs_failed',
|
|
|
|
help: 'Total number of jobs failed across all workers in scaling mode since instance start.',
|
|
|
|
});
|
|
|
|
|
|
|
|
this.gauges.waiting.set(0);
|
|
|
|
this.gauges.active.set(0);
|
|
|
|
this.counters.completed.inc(0);
|
|
|
|
this.counters.failed.inc(0);
|
|
|
|
|
|
|
|
this.eventService.on('job-counts-updated', (jobCounts) => {
|
|
|
|
this.gauges.waiting.set(jobCounts.waiting);
|
|
|
|
this.gauges.active.set(jobCounts.active);
|
|
|
|
this.counters.completed?.inc(jobCounts.completed);
|
|
|
|
this.counters.failed?.inc(jobCounts.failed);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
private toLabels(event: EventMessageTypes): Record<string, string> {
|
|
|
|
const { __type, eventName, payload } = event;
|
|
|
|
|
|
|
|
switch (__type) {
|
2024-06-11 00:11:39 -07:00
|
|
|
case EventMessageTypeNames.audit:
|
2024-07-18 01:27:35 -07:00
|
|
|
if (eventName.startsWith('n8n.audit.user.credentials')) {
|
|
|
|
return this.includes.labels.credentialsType
|
|
|
|
? { credential_type: (event.payload.credentialType ?? 'unknown').replace(/\./g, '_') }
|
2024-06-11 00:11:39 -07:00
|
|
|
: {};
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:27:35 -07:00
|
|
|
if (eventName.startsWith('n8n.audit.workflow')) {
|
|
|
|
return this.includes.labels.workflowId
|
|
|
|
? { workflow_id: payload.workflowId ?? 'unknown' }
|
2024-06-11 00:11:39 -07:00
|
|
|
: {};
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case EventMessageTypeNames.node:
|
2024-07-18 01:27:35 -07:00
|
|
|
return this.includes.labels.nodeType
|
|
|
|
? {
|
|
|
|
node_type: (payload.nodeType ?? 'unknown')
|
|
|
|
.replace('n8n-nodes-', '')
|
|
|
|
.replace(/\./g, '_'),
|
|
|
|
}
|
2024-06-11 00:11:39 -07:00
|
|
|
: {};
|
|
|
|
|
|
|
|
case EventMessageTypeNames.workflow:
|
2024-07-18 01:27:35 -07:00
|
|
|
return this.includes.labels.workflowId
|
|
|
|
? { workflow_id: payload.workflowId ?? 'unknown' }
|
2024-06-11 00:11:39 -07:00
|
|
|
: {};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
2023-08-04 11:51:07 -07:00
|
|
|
}
|