2023-08-04 11:51:07 -07:00
|
|
|
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';
|
|
|
|
|
perf(core): Improve caching service (#8213)
Story: https://linear.app/n8n/issue/PAY-1188
- Implement Redis hashes on the caching service, based on Micha's work
in #7747, adapted from `node-cache-manager-ioredis-yet`. Optimize
workflow ownership lookups and manual webhook lookups with Redis hashes.
- Simplify the caching service by removing all currently unused methods
and options: `enable`, `disable`, `getCache`, `keys`, `keyValues`,
`refreshFunctionEach`, `refreshFunctionMany`, `refreshTtl`, etc.
- Remove the flag `N8N_CACHE_ENABLED`. Currently some features on
`master` are broken with caching disabled, and test webhooks now rely
entirely on caching, for multi-main setup support. We originally
introduced this flag to protect against excessive memory usage, but
total cache usage is low enough that we decided to drop this setting.
Apparently this flag was also never documented.
- Overall caching service refactor: use generics, reduce branching, add
discriminants for cache kinds for better type safety, type caching
events, improve readability, remove outdated docs, etc. Also refactor
and expand caching service tests.
Follow-up to: https://github.com/n8n-io/n8n/pull/8176
---------
Co-authored-by: Michael Auerswald <michael.auerswald@gmail.com>
2024-01-05 02:52:44 -08:00
|
|
|
import { CacheService } from '@/services/cache/cache.service';
|
2024-08-26 02:10:06 -07:00
|
|
|
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
2024-06-11 00:11:39 -07:00
|
|
|
import { EventMessageTypeNames } from 'n8n-workflow';
|
2024-07-18 01:27:35 -07:00
|
|
|
import type { EventMessageTypes } from '@/eventbus';
|
2024-07-22 03:01:44 -07:00
|
|
|
import type { Includes, MetricCategory, MetricLabel } from './types';
|
2024-07-31 08:45:11 -07:00
|
|
|
import { GlobalConfig } from '@n8n/config';
|
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-07-18 01:27:35 -07:00
|
|
|
) {}
|
|
|
|
|
|
|
|
private readonly counters: { [key: string]: Counter<string> | null } = {};
|
|
|
|
|
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-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);
|
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-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
|
|
|
}
|