UNPKG

n8n

Version:

n8n Workflow Automation Tool

536 lines 25.2 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PrometheusMetricsService = void 0; const node_fs_1 = require("node:fs"); const config_1 = require("@n8n/config"); const constants_1 = require("@n8n/constants"); const db_1 = require("@n8n/db"); const decorators_1 = require("@n8n/decorators"); const di_1 = require("@n8n/di"); const express_prom_bundle_1 = __importDefault(require("express-prom-bundle")); const luxon_1 = require("luxon"); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const prom_client_1 = __importDefault(require("prom-client")); const parse_1 = __importDefault(require("semver/functions/parse")); const constants_2 = require("../constants"); const message_event_bus_1 = require("../eventbus/message-event-bus/message-event-bus"); const event_service_1 = require("../events/event.service"); const cache_service_1 = require("../services/cache/cache.service"); let PrometheusMetricsService = class PrometheusMetricsService { constructor(cacheService, eventBus, globalConfig, eventService, instanceSettings, workflowRepository, licenseMetricsRepository) { this.cacheService = cacheService; this.eventBus = eventBus; this.globalConfig = globalConfig; this.eventService = eventService; this.instanceSettings = instanceSettings; this.workflowRepository = workflowRepository; this.licenseMetricsRepository = licenseMetricsRepository; this.counters = {}; this.tokenExchangeListenersRegistered = false; this.gauges = {}; this.histograms = {}; this.prefix = this.globalConfig.endpoints.metrics.prefix; this.includes = { metrics: { default: this.globalConfig.endpoints.metrics.includeDefaultMetrics, routes: this.globalConfig.endpoints.metrics.includeApiEndpoints, cache: this.globalConfig.endpoints.metrics.includeCacheMetrics, logs: this.globalConfig.endpoints.metrics.includeMessageEventBusMetrics, queue: this.globalConfig.endpoints.metrics.includeQueueMetrics, workflowExecutionDuration: this.globalConfig.endpoints.metrics.includeWorkflowExecutionDuration, workflowStatistics: this.globalConfig.endpoints.metrics.includeWorkflowStatistics, }, labels: { 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, workflowName: this.globalConfig.endpoints.metrics.includeWorkflowNameLabel, }, }; this.workflowStatisticsCache = null; } async init(app) { prom_client_1.default.register.clear(); this.initDefaultMetrics(); this.initPssMetric(); this.initN8nVersionMetric(); if (this.instanceSettings.instanceType === 'main') this.initInstanceRoleMetric(); this.initCacheMetrics(); this.initEventBusMetrics(); this.initRouteMetrics(app); this.initQueueMetrics(); this.initWorkflowExecutionDurationMetric(); this.initActiveWorkflowCountMetric(); this.initWorkflowStatisticsMetrics(); this.initTokenExchangeMetrics(); this.mountMetricsEndpoint(app); } enableMetric(metric) { this.includes.metrics[metric] = true; } disableMetric(metric) { this.includes.metrics[metric] = false; } disableAllMetrics() { for (const metric in this.includes.metrics) { this.includes.metrics[metric] = false; } } enableLabels(labels) { for (const label of labels) { this.includes.labels[label] = true; } } disableAllLabels() { for (const label in this.includes.labels) { this.includes.labels[label] = false; } } initN8nVersionMetric() { const n8nVersion = (0, parse_1.default)(constants_2.N8N_VERSION ?? '0.0.0'); if (!n8nVersion) return; const versionGauge = new prom_client_1.default.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); } initInstanceRoleMetric() { this.gauges.instanceRoleLeader = new prom_client_1.default.Gauge({ name: this.prefix + 'instance_role_leader', help: 'Whether this main instance is the leader (1) or not (0).', }); this.gauges.instanceRoleLeader.set(this.instanceSettings.isLeader ? 1 : 0); } updateOnLeaderTakeover() { this.gauges.instanceRoleLeader?.set(1); } updateOnLeaderStepdown() { this.gauges.instanceRoleLeader?.set(0); } initDefaultMetrics() { if (!this.includes.metrics.default) return; prom_client_1.default.collectDefaultMetrics({ prefix: this.globalConfig.endpoints.metrics.prefix }); } initPssMetric() { if (!this.includes.metrics.default) return; let pssAvailable = true; try { (0, node_fs_1.readFileSync)('/proc/self/smaps_rollup', 'utf8'); } catch { pssAvailable = false; } if (!pssAvailable) return; const prefix = this.prefix; new prom_client_1.default.Gauge({ name: prefix + 'process_pss_bytes', help: 'Proportional Set Size of the process in bytes.', collect() { try { const content = (0, node_fs_1.readFileSync)('/proc/self/smaps_rollup', 'utf8'); const match = content.match(/^Pss:\s+(\d+)\s+kB$/m); if (match) { this.set(parseInt(match[1], 10) * 1024); } } catch { } }, }); } initRouteMetrics(app) { if (!this.includes.metrics.routes) return; const metricsMiddleware = (0, express_prom_bundle_1.default)({ autoregister: false, includeUp: false, includePath: this.includes.labels.apiPath, includeMethod: this.includes.labels.apiMethod, includeStatusCode: this.includes.labels.apiStatusCode, httpDurationMetricName: this.prefix + 'http_request_duration_seconds', }); const activityGauge = new prom_client_1.default.Gauge({ name: this.prefix + 'last_activity', help: 'last instance activity (backend request) in Unix time (seconds).', }); activityGauge.set(luxon_1.DateTime.now().toUnixInteger()); app.use([ '/api/', `/${this.globalConfig.endpoints.rest}/`, `/${this.globalConfig.endpoints.webhook}/`, `/${this.globalConfig.endpoints.webhookWaiting}/`, `/${this.globalConfig.endpoints.webhookTest}/`, `/${this.globalConfig.endpoints.form}/`, `/${this.globalConfig.endpoints.formWaiting}/`, `/${this.globalConfig.endpoints.formTest}/`, ], async (req, res, next) => { activityGauge.set(luxon_1.DateTime.now().toUnixInteger()); await metricsMiddleware(req, res, next); }); } mountMetricsEndpoint(app) { app.get('/metrics', async (_req, res) => { const metrics = await prom_client_1.default.register.metrics(); res.setHeader('Content-Type', prom_client_1.default.register.contentType); res.send(metrics).end(); }); } 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}.`, labelNames: ['cache'], })); this.counters.cacheHitsTotal = new prom_client_1.default.Counter(hitsConfig); this.counters.cacheHitsTotal.inc(0); this.cacheService.on('metrics.cache.hit', () => this.counters.cacheHitsTotal?.inc(1)); this.counters.cacheMissesTotal = new prom_client_1.default.Counter(missesConfig); this.counters.cacheMissesTotal.inc(0); this.cacheService.on('metrics.cache.miss', () => this.counters.cacheMissesTotal?.inc(1)); this.counters.cacheUpdatesTotal = new prom_client_1.default.Counter(updatesConfig); this.counters.cacheUpdatesTotal.inc(0); this.cacheService.on('metrics.cache.update', () => this.counters.cacheUpdatesTotal?.inc(1)); } toCounter(event) { const { eventName } = event; if (!this.counters[eventName]) { const metricName = this.prefix + eventName.replace('n8n.', '').replace(/\./g, '_') + '_total'; if (!prom_client_1.default.validateMetricName(metricName)) { this.counters[eventName] = null; return null; } const labels = this.toLabels(event); const counter = new prom_client_1.default.Counter({ name: metricName, help: `Total number of ${eventName} events.`, labelNames: Object.keys(labels), }); this.counters[eventName] = counter; } return this.counters[eventName]; } initEventBusMetrics() { if (!this.includes.metrics.logs) return; this.eventBus.on('metrics.eventBus.event', (event) => { const counter = this.toCounter(event); if (!counter) return; const labels = this.toLabels(event); counter.inc(labels, 1); }); } initQueueMetrics() { if (!this.includes.metrics.queue || this.globalConfig.executions.mode !== 'queue' || this.instanceSettings.instanceType !== 'main') { return; } this.gauges.waiting = new prom_client_1.default.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 prom_client_1.default.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 prom_client_1.default.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 prom_client_1.default.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); }); } initWorkflowExecutionDurationMetric() { if (!this.includes.metrics.workflowExecutionDuration) return; const labelNames = ['status', 'mode']; if (this.includes.labels.workflowId) labelNames.push('workflow_id'); this.histograms.workflowExecutionDuration = new prom_client_1.default.Histogram({ name: this.prefix + 'workflow_execution_duration_seconds', help: 'Workflow execution duration in seconds.', labelNames, buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 600], }); this.eventService.on('workflow-post-execute', ({ runData, workflow }) => { if (!runData?.stoppedAt) return; const durationSeconds = (runData.stoppedAt.getTime() - runData.startedAt.getTime()) / 1000; const labels = { status: runData.status === 'success' ? 'success' : 'failed', mode: runData.mode, }; if (this.includes.labels.workflowId) { labels.workflow_id = String(workflow.id ?? 'unknown'); } this.histograms.workflowExecutionDuration?.observe(labels, durationSeconds); }); } initActiveWorkflowCountMetric() { const workflowRepository = this.workflowRepository; const cacheService = this.cacheService; const cacheKey = 'metrics:active-workflow-count'; const cacheTtl = this.globalConfig.endpoints.metrics.activeWorkflowCountInterval * constants_1.Time.seconds.toMilliseconds; new prom_client_1.default.Gauge({ name: this.prefix + 'active_workflow_count', help: 'Total number of active workflows.', async collect() { const value = await cacheService.get(cacheKey); const numericValue = value !== undefined ? parseInt(value, 10) : undefined; if (numericValue !== undefined && Number.isFinite(numericValue)) { this.set(numericValue); } else { const activeWorkflowCount = await workflowRepository.getActiveCount(); await cacheService.set(cacheKey, activeWorkflowCount.toString(), cacheTtl); this.set(activeWorkflowCount); } }, }); } toLabels(event) { const { __type, eventName, payload } = event; switch (__type) { case "$$EventMessageAudit": if (eventName.startsWith('n8n.audit.user.credentials')) { return this.includes.labels.credentialsType ? { credential_type: String((event.payload.credentialType ?? 'unknown').replace(/\./g, '_')), } : {}; } if (eventName.startsWith('n8n.audit.workflow')) { return this.buildWorkflowLabels(payload); } break; case "$$EventMessageNode": const nodeLabels = this.buildWorkflowLabels(payload); if (this.includes.labels.nodeType) { nodeLabels.node_type = String((payload.nodeType ?? 'unknown').replace('n8n-nodes-', '').replace(/\./g, '_')); } return nodeLabels; case "$$EventMessageWorkflow": return this.buildWorkflowLabels(payload); } return {}; } buildWorkflowLabels(payload) { const labels = {}; if (this.includes.labels.workflowId) { labels.workflow_id = String(payload.workflowId ?? 'unknown'); } if (this.includes.labels.workflowName) { labels.workflow_name = String(payload.workflowName ?? 'unknown'); } return labels; } async getWorkflowStatistics(cacheService, licenseMetricsRepository, cacheTtl) { const now = Date.now(); if (this.workflowStatisticsCache && now - this.workflowStatisticsCache.timestamp < this.workflowStatisticsCache.ttl) { return this.workflowStatisticsCache.data; } const fullCacheKey = 'metrics:workflow-statistics:shared'; const cachedValue = await cacheService.get(fullCacheKey); if (cachedValue !== undefined) { const parsedValue = (0, n8n_workflow_1.jsonParse)(String(cachedValue), { fallbackValue: undefined }); if (parsedValue !== undefined) { this.workflowStatisticsCache = { data: parsedValue, timestamp: now, ttl: 1000, }; return parsedValue; } } const metrics = await licenseMetricsRepository.getLicenseRenewalMetrics(); await cacheService.set(fullCacheKey, JSON.stringify(metrics), cacheTtl); this.workflowStatisticsCache = { data: metrics, timestamp: now, ttl: 1000, }; return metrics; } createWorkflowStatisticsGauge(metricName, help, getMetricValue, cacheService, licenseMetricsRepository, cacheTtl) { const getWorkflowStatistics = this.getWorkflowStatistics.bind(this); return new prom_client_1.default.Gauge({ name: this.prefix + metricName, help, async collect() { const metrics = await getWorkflowStatistics(cacheService, licenseMetricsRepository, cacheTtl); const value = getMetricValue(metrics); this.set(value); }, }); } initWorkflowStatisticsMetrics() { if (!this.includes.metrics.workflowStatistics) return; const licenseMetricsRepository = this.licenseMetricsRepository; const cacheService = this.cacheService; const cacheTtl = this.globalConfig.endpoints.metrics.workflowStatisticsInterval * constants_1.Time.seconds.toMilliseconds; const metricsConfig = [ { name: 'production_executions', help: 'Total number of production workflow executions (success + error).', getValue: (metrics) => Number(metrics.productionExecutions) || 0, }, { name: 'production_root_executions', help: 'Total number of production root workflow executions (excludes sub-workflows).', getValue: (metrics) => Number(metrics.productionRootExecutions) || 0, }, { name: 'manual_executions', help: 'Total number of manual workflow executions (success + error).', getValue: (metrics) => Number(metrics.manualExecutions) || 0, }, { name: 'enabled_users', help: 'Total number of enabled users.', getValue: (metrics) => Number(metrics.enabledUsers) || 0, }, { name: 'users', help: 'Total number of users.', getValue: (metrics) => Number(metrics.totalUsers) || 0, }, { name: 'workflows', help: 'Total number of workflows.', getValue: (metrics) => Number(metrics.totalWorkflows) || 0, }, { name: 'credentials', help: 'Total number of credentials.', getValue: (metrics) => Number(metrics.totalCredentials) || 0, }, ]; metricsConfig.forEach((config) => { this.createWorkflowStatisticsGauge(config.name, config.help, config.getValue, cacheService, licenseMetricsRepository, cacheTtl); }); } initTokenExchangeMetrics() { this.counters.tokenExchangeRequestsTotal = new prom_client_1.default.Counter({ name: this.prefix + 'token_exchange_requests_total', help: 'Total number of token exchange requests.', labelNames: ['result'], }); this.counters.tokenExchangeRequestsTotal.inc({ result: 'success' }, 0); this.counters.tokenExchangeRequestsTotal.inc({ result: 'failure' }, 0); this.counters.tokenExchangeFailuresTotal = new prom_client_1.default.Counter({ name: this.prefix + 'token_exchange_failures_total', help: 'Total number of token exchange failures broken down by reason.', labelNames: ['reason'], }); this.counters.embedLoginRequestsTotal = new prom_client_1.default.Counter({ name: this.prefix + 'embed_login_requests_total', help: 'Total number of embed login requests.', labelNames: ['result'], }); this.counters.embedLoginRequestsTotal.inc({ result: 'success' }, 0); this.counters.embedLoginRequestsTotal.inc({ result: 'failure' }, 0); this.counters.embedLoginFailuresTotal = new prom_client_1.default.Counter({ name: this.prefix + 'embed_login_failures_total', help: 'Total number of embed login failures broken down by reason.', labelNames: ['reason'], }); this.counters.tokenExchangeJitProvisioningTotal = new prom_client_1.default.Counter({ name: this.prefix + 'token_exchange_jit_provisioning_total', help: 'Total number of users JIT-provisioned via token exchange.', }); this.counters.tokenExchangeJitProvisioningTotal.inc(0); this.counters.tokenExchangeIdentityLinkedTotal = new prom_client_1.default.Counter({ name: this.prefix + 'token_exchange_identity_linked_total', help: 'Total number of external identities linked to existing users via token exchange.', }); this.counters.tokenExchangeIdentityLinkedTotal.inc(0); if (this.tokenExchangeListenersRegistered) return; this.tokenExchangeListenersRegistered = true; this.eventService.on('token-exchange-succeeded', () => { this.counters.tokenExchangeRequestsTotal?.inc({ result: 'success' }, 1); }); this.eventService.on('token-exchange-failed', ({ failureReason }) => { this.counters.tokenExchangeRequestsTotal?.inc({ result: 'failure' }, 1); this.counters.tokenExchangeFailuresTotal?.inc({ reason: failureReason }, 1); }); this.eventService.on('embed-login', () => { this.counters.embedLoginRequestsTotal?.inc({ result: 'success' }, 1); }); this.eventService.on('embed-login-failed', ({ failureReason }) => { this.counters.embedLoginRequestsTotal?.inc({ result: 'failure' }, 1); this.counters.embedLoginFailuresTotal?.inc({ reason: failureReason }, 1); }); this.eventService.on('token-exchange-user-provisioned', () => { this.counters.tokenExchangeJitProvisioningTotal?.inc(1); }); this.eventService.on('token-exchange-identity-linked', () => { this.counters.tokenExchangeIdentityLinkedTotal?.inc(1); }); } }; exports.PrometheusMetricsService = PrometheusMetricsService; __decorate([ (0, decorators_1.OnLeaderTakeover)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], PrometheusMetricsService.prototype, "updateOnLeaderTakeover", null); __decorate([ (0, decorators_1.OnLeaderStepdown)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], PrometheusMetricsService.prototype, "updateOnLeaderStepdown", null); exports.PrometheusMetricsService = PrometheusMetricsService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [cache_service_1.CacheService, message_event_bus_1.MessageEventBus, config_1.GlobalConfig, event_service_1.EventService, n8n_core_1.InstanceSettings, db_1.WorkflowRepository, db_1.LicenseMetricsRepository]) ], PrometheusMetricsService); //# sourceMappingURL=prometheus-metrics.service.js.map