diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 0fd9d399ac..88d62b6aa0 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -168,8 +168,8 @@ export class Server extends AbstractServer { async configure(): Promise { if (config.getEnv('endpoints.metrics.enable')) { - const { MetricsService } = await import('@/services/metrics.service'); - await Container.get(MetricsService).configureMetrics(this.app); + const { PrometheusMetricsService } = await import('@/metrics/prometheus-metrics.service'); + await Container.get(PrometheusMetricsService).configureMetrics(this.app); } const { frontendService } = this; diff --git a/packages/cli/src/metrics/__tests__/prometheus-metrics.service.test.ts b/packages/cli/src/metrics/__tests__/prometheus-metrics.service.test.ts new file mode 100644 index 0000000000..eee260add8 --- /dev/null +++ b/packages/cli/src/metrics/__tests__/prometheus-metrics.service.test.ts @@ -0,0 +1,123 @@ +import config from '@/config'; +import promClient from 'prom-client'; +import promBundle from 'express-prom-bundle'; +import { mock } from 'jest-mock-extended'; +import { PrometheusMetricsService } from '../prometheus-metrics.service'; +import type express from 'express'; +import type { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; + +const mockMiddleware = ( + _req: express.Request, + _res: express.Response, + next: express.NextFunction, +) => next(); + +jest.mock('prom-client'); +jest.mock('express-prom-bundle', () => jest.fn(() => mockMiddleware)); + +describe('PrometheusMetricsService', () => { + beforeEach(() => { + config.load(config.default); + }); + + describe('configureMetrics', () => { + it('should set up `n8n_version_info`', async () => { + const service = new PrometheusMetricsService(mock(), mock(), mock()); + + await service.configureMetrics(mock()); + + expect(promClient.Gauge).toHaveBeenCalledWith({ + name: 'n8n_version_info', + help: 'n8n version info.', + labelNames: ['version', 'major', 'minor', 'patch'], + }); + }); + + it('should set up default metrics collection with `prom-client`', async () => { + const service = new PrometheusMetricsService(mock(), mock(), mock()); + + await service.configureMetrics(mock()); + + expect(promClient.collectDefaultMetrics).toHaveBeenCalled(); + }); + + it('should set up `n8n_cache_hits_total`', async () => { + config.set('endpoints.metrics.includeCacheMetrics', true); + const service = new PrometheusMetricsService(mock(), mock(), mock()); + + await service.configureMetrics(mock()); + + expect(promClient.Counter).toHaveBeenCalledWith({ + name: 'n8n_cache_hits_total', + help: 'Total number of cache hits.', + labelNames: ['cache'], + }); + expect(service.counters.cacheHitsTotal?.inc).toHaveBeenCalledWith(0); + }); + + it('should set up `n8n_cache_misses_total`', async () => { + config.set('endpoints.metrics.includeCacheMetrics', true); + const service = new PrometheusMetricsService(mock(), mock(), mock()); + + await service.configureMetrics(mock()); + + expect(promClient.Counter).toHaveBeenCalledWith({ + name: 'n8n_cache_misses_total', + help: 'Total number of cache misses.', + labelNames: ['cache'], + }); + expect(service.counters.cacheMissesTotal?.inc).toHaveBeenCalledWith(0); + }); + + it('should set up `n8n_cache_updates_total`', async () => { + config.set('endpoints.metrics.includeCacheMetrics', true); + const service = new PrometheusMetricsService(mock(), mock(), mock()); + + await service.configureMetrics(mock()); + + expect(promClient.Counter).toHaveBeenCalledWith({ + name: 'n8n_cache_updates_total', + help: 'Total number of cache updates.', + labelNames: ['cache'], + }); + expect(service.counters.cacheUpdatesTotal?.inc).toHaveBeenCalledWith(0); + }); + + it('should set up API metrics with `express-prom-bundle`', async () => { + config.set('endpoints.metrics.includeApiEndpoints', true); + config.set('endpoints.metrics.includeApiPathLabel', true); + config.set('endpoints.metrics.includeApiMethodLabel', true); + config.set('endpoints.metrics.includeApiStatusCodeLabel', true); + const service = new PrometheusMetricsService(mock(), mock(), mock()); + + const app = mock(); + + await service.configureMetrics(app); + + expect(promBundle).toHaveBeenCalledWith({ + autoregister: false, + includeUp: false, + includePath: true, + includeMethod: true, + includeStatusCode: true, + }); + + expect(app.use).toHaveBeenCalledWith( + ['/rest/', '/webhook/', 'webhook-test/', '/api/'], + expect.any(Function), + ); + }); + + it('should set up event bus metrics', async () => { + const eventBus = mock(); + const service = new PrometheusMetricsService(mock(), mock(), eventBus); + + await service.configureMetrics(mock()); + + expect(eventBus.on).toHaveBeenCalledWith( + 'metrics.messageEventBus.Event', + expect.any(Function), + ); + }); + }); +}); diff --git a/packages/cli/src/services/metrics.service.ts b/packages/cli/src/metrics/prometheus-metrics.service.ts similarity index 99% rename from packages/cli/src/services/metrics.service.ts rename to packages/cli/src/metrics/prometheus-metrics.service.ts index edee289de1..f925bd870b 100644 --- a/packages/cli/src/services/metrics.service.ts +++ b/packages/cli/src/metrics/prometheus-metrics.service.ts @@ -14,7 +14,7 @@ import { Logger } from '@/Logger'; import { EventMessageTypeNames } from 'n8n-workflow'; @Service() -export class MetricsService extends EventEmitter { +export class PrometheusMetricsService extends EventEmitter { constructor( private readonly logger: Logger, private readonly cacheService: CacheService, diff --git a/packages/cli/test/integration/metrics.test.ts b/packages/cli/test/integration/metrics.test.ts index 0e62ddbae0..0b2dac801f 100644 --- a/packages/cli/test/integration/metrics.test.ts +++ b/packages/cli/test/integration/metrics.test.ts @@ -4,7 +4,7 @@ import request from 'supertest'; import config from '@/config'; import { N8N_VERSION } from '@/constants'; -import { MetricsService } from '@/services/metrics.service'; +import { PrometheusMetricsService } from '@/metrics/prometheus-metrics.service'; import { ExecutionRecoveryService } from '@/executions/execution-recovery.service'; import { setupTestServer } from './shared/utils'; @@ -42,7 +42,7 @@ describe('Metrics', () => { it('should return cache metrics when enabled', async () => { config.set('endpoints.metrics.includeCacheMetrics', true); - await Container.get(MetricsService).configureMetrics(testServer.app); + await Container.get(PrometheusMetricsService).configureMetrics(testServer.app); const lines = await getMetricsResponseAsLines(); expect(lines).toContain('n8n_test_cache_hits_total 0'); expect(lines).toContain('n8n_test_cache_misses_total 0'); @@ -67,7 +67,7 @@ describe('Metrics', () => { it('should return default metrics', async () => { config.set('endpoints.metrics.includeDefaultMetrics', true); - await Container.get(MetricsService).configureMetrics(testServer.app); + await Container.get(PrometheusMetricsService).configureMetrics(testServer.app); const lines = await getMetricsResponseAsLines(); expect(lines).toContain('nodejs_heap_space_size_total_bytes{space="read_only"} 0'); config.set('endpoints.metrics.includeDefaultMetrics', false); @@ -75,7 +75,7 @@ describe('Metrics', () => { it('should not return default metrics only when disabled', async () => { config.set('endpoints.metrics.includeDefaultMetrics', false); - await Container.get(MetricsService).configureMetrics(testServer.app); + await Container.get(PrometheusMetricsService).configureMetrics(testServer.app); const lines = await getMetricsResponseAsLines(); expect(lines).not.toContain('nodejs_heap_space_size_total_bytes{space="read_only"} 0'); config.set('endpoints.metrics.includeDefaultMetrics', true); diff --git a/packages/cli/test/integration/shared/utils/testServer.ts b/packages/cli/test/integration/shared/utils/testServer.ts index a9921a6354..4e1f165f91 100644 --- a/packages/cli/test/integration/shared/utils/testServer.ts +++ b/packages/cli/test/integration/shared/utils/testServer.ts @@ -145,8 +145,10 @@ export const setupTestServer = ({ break; case 'metrics': - const { MetricsService } = await import('@/services/metrics.service'); - await Container.get(MetricsService).configureMetrics(app); + const { PrometheusMetricsService } = await import( + '@/metrics/prometheus-metrics.service' + ); + await Container.get(PrometheusMetricsService).configureMetrics(app); break; case 'eventBus':