fix(core)!: Change last activity to use unix time (#15951)

This commit is contained in:
Juuso Tapaninen
2025-06-03 16:55:55 +03:00
committed by GitHub
parent 2c9c3dab33
commit 4e2229752b
4 changed files with 39 additions and 15 deletions

View File

@@ -2,6 +2,20 @@
This list shows all the versions which include breaking changes and how to upgrade.
## 1.98.0
### What changed?
The `last_activity` metric included as a part of route metrics has been changed to output a Unix time in seconds from
the previous timestamp label approach. The labeling approach could result in high cardinality within Prometheus and
thus result in poorer performance.
### When is action necessary?
If you've been ingesting route metrics from your n8n instance (version 1.81.0 and newer), you should analyze
how the `last_activity` metric has affected your Prometheus instance and potentially clean up the old data. Future
metrics will also be served in a different format, which needs to be taken into account.
## 1.83.0
### What changed?

View File

@@ -161,8 +161,7 @@ describe('PrometheusMetricsService', () => {
expect(promClient.Gauge).toHaveBeenNthCalledWith(2, {
name: 'n8n_last_activity',
help: 'last instance activity (backend request).',
labelNames: ['timestamp'],
help: 'last instance activity (backend request) in Unix time (seconds).',
});
expect(app.use).toHaveBeenCalledWith(

View File

@@ -3,6 +3,7 @@ import { WorkflowRepository } from '@n8n/db';
import { Service } from '@n8n/di';
import type express from 'express';
import promBundle from 'express-prom-bundle';
import { DateTime } from 'luxon';
import { InstanceSettings } from 'n8n-core';
import { EventMessageTypeNames } from 'n8n-workflow';
import promClient, { type Counter, type Gauge } from 'prom-client';
@@ -137,11 +138,10 @@ export class PrometheusMetricsService {
const activityGauge = new promClient.Gauge({
name: this.prefix + 'last_activity',
help: 'last instance activity (backend request).',
labelNames: ['timestamp'],
help: 'last instance activity (backend request) in Unix time (seconds).',
});
activityGauge.set({ timestamp: new Date().toISOString() }, 1);
activityGauge.set(DateTime.now().toUnixInteger());
app.use(
[
@@ -155,8 +155,7 @@ export class PrometheusMetricsService {
`/${this.globalConfig.endpoints.formTest}/`,
],
async (req, res, next) => {
activityGauge.reset();
activityGauge.set({ timestamp: new Date().toISOString() }, 1);
activityGauge.set(DateTime.now().toUnixInteger());
await metricsMiddleware(req, res, next);
},

View File

@@ -1,6 +1,7 @@
import { GlobalConfig } from '@n8n/config';
import { WorkflowRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { DateTime } from 'luxon';
import { parse as semverParse } from 'semver';
import request, { type Response } from 'supertest';
@@ -53,6 +54,11 @@ describe('PrometheusMetricsService', () => {
prometheusService.disableAllLabels();
});
afterEach(() => {
// Make sure fake timers aren't in effect after a test
jest.useRealTimers();
});
it('should return n8n version', async () => {
/**
* Arrange
@@ -211,9 +217,11 @@ describe('PrometheusMetricsService', () => {
/**
* Arrange
*/
const startTime = DateTime.now().toUnixInteger();
jest.useFakeTimers().setSystemTime(startTime * 1000);
prometheusService.enableMetric('routes');
await prometheusService.init(server.app);
await agent.get('/api/v1/workflows');
/**
* Act
@@ -230,26 +238,30 @@ describe('PrometheusMetricsService', () => {
expect(lines).toContainEqual(expect.stringContaining('n8n_test_last_activity'));
const lastActivityLine = lines.find((line) =>
line.startsWith('n8n_test_last_activity{timestamp='),
);
const lastActivityLine = lines.find((line) => line.startsWith('n8n_test_last_activity'));
expect(lastActivityLine).toBeDefined();
expect(lastActivityLine?.endsWith('1')).toBe(true);
const value = lastActivityLine!.split(' ')[1];
expect(parseInt(value, 10)).toBe(startTime);
// Update last activity
jest.advanceTimersByTime(1000);
await agent.get('/api/v1/workflows');
response = await agent.get('/metrics');
const updatedLines = toLines(response);
const newLastActivityLine = updatedLines.find((line) =>
line.startsWith('n8n_test_last_activity{timestamp='),
line.startsWith('n8n_test_last_activity'),
);
expect(newLastActivityLine).toBeDefined();
// Timestamp label should be different
expect(newLastActivityLine).not.toBe(lastActivityLine);
const newValue = newLastActivityLine!.split(' ')[1];
expect(parseInt(newValue, 10)).toBe(startTime + 1);
});
it('should return labels in route metrics if enabled', async () => {