diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 52c190dc61..1ebe4df6a2 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -446,6 +446,20 @@ const config = convict({ }, endpoints: { + metrics: { + enable: { + format: 'Boolean', + default: false, + env: 'N8N_ENABLE_METRICS', + doc: 'Enable metrics endpoint', + }, + prefix: { + format: String, + default: 'n8n_', + env: 'N8N_METRICS_PREFIX', + doc: 'An optional prefix for metric names. Default: n8n_', + }, + }, rest: { format: String, default: 'rest', @@ -471,7 +485,7 @@ const config = convict({ doc: 'Disable production webhooks from main process. This helps ensures no http traffic load to main process when using webhook-specific processes.', }, skipWebhoooksDeregistrationOnShutdown: { - /** + /** * Longer explanation: n8n deregisters webhooks on shutdown / deactivation * and registers on startup / activation. If we skip * deactivation on shutdown, webhooks will remain active on 3rd party services. diff --git a/packages/cli/package.json b/packages/cli/package.json index 47f415318a..18e32adba0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -111,6 +111,7 @@ "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^8.3.0", + "prom-client": "^13.1.0", "request-promise-native": "^1.0.7", "sqlite3": "^5.0.1", "sse-channel": "^3.1.1", diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f435e58eab..187fccb340 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -23,6 +23,7 @@ import * as csrf from 'csrf'; import * as requestPromise from 'request-promise-native'; import { createHmac } from 'crypto'; import { compare } from 'bcryptjs'; +import * as promClient from 'prom-client'; import { ActiveExecutions, @@ -108,6 +109,7 @@ import * as parseUrl from 'parseurl'; import * as querystring from 'querystring'; import * as Queue from '../src/Queue'; import { OptionsWithUrl } from 'request-promise-native'; +import { Registry } from 'prom-client'; class App { @@ -197,6 +199,16 @@ class App { async config(): Promise { + const enableMetrics = config.get('endpoints.metrics.enable') as boolean; + let register: Registry; + + if (enableMetrics === true) { + const prefix = config.get('endpoints.metrics.prefix') as string; + register = new promClient.Registry(); + register.setDefaultLabels({ prefix }); + promClient.collectDefaultMetrics({ register }); + } + this.versions = await GenericHelpers.getVersions(); this.frontendSettings.versionCli = this.versions.cli; @@ -204,7 +216,7 @@ class App { const excludeEndpoints = config.get('security.excludeEndpoints') as string; - const ignoredEndpoints = ['healthz', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials]; + const ignoredEndpoints = ['healthz', 'metrics', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials]; ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':')); const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`); @@ -386,7 +398,7 @@ class App { this.app.use(history({ rewrites: [ { - from: new RegExp(`^\/(${this.restEndpoint}|healthz|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), + from: new RegExp(`^\/(${this.restEndpoint}|healthz|metrics|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), to: (context) => { return context.parsedUrl!.pathname!.toString(); }, @@ -454,7 +466,16 @@ class App { ResponseHelper.sendSuccessResponse(res, responseData, true, 200); }); - + // ---------------------------------------- + // Metrics + // ---------------------------------------- + if (enableMetrics === true) { + this.app.get('/metrics', async (req: express.Request, res: express.Response) => { + const response = await register.metrics(); + res.setHeader('Content-Type', register.contentType); + ResponseHelper.sendSuccessResponse(res, response, true, 200); + }); + } // ---------------------------------------- // Workflow @@ -1728,7 +1749,7 @@ class App { stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, finished: result.finished, }; - + return returnData; }