Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

350 lines
10 KiB
TypeScript

import { Router } from '@rocket.chat/http-router';
import Ajv from 'ajv';
import express from 'express';
import request from 'supertest';
import { metricsMiddleware } from './metrics';
import { CachedSettings } from '../../../settings/CachedSettings';
describe('Metrics middleware', () => {
it('should handle metrics', async () => {
const ajv = new Ajv();
const app = express();
const api = new Router('/api');
const settings = new CachedSettings();
settings.set({
_id: 'Prometheus_API_User_Agent',
value: true,
} as any);
const summary = {
startTimer: jest.fn().mockImplementation(() => jest.fn()),
};
// Get the mock startTimer function
const mockEndTimer = jest.fn();
summary.startTimer.mockReturnValue(mockEndTimer);
const histogram = { startTimer: jest.fn().mockReturnValue(jest.fn()) };
const responseSizeHistogram = { observe: jest.fn() };
const activeRequestsGauge = { inc: jest.fn(), dec: jest.fn() };
api
.use(
metricsMiddleware({
api: { version: 1 } as any,
settings,
endpointTimeSummary: summary as any,
endpointTimeHistogram: histogram as any,
responseSizeHistogram: responseSizeHistogram as any,
activeRequestsGauge: activeRequestsGauge as any,
}),
)
.get(
'/test',
{
response: {
200: ajv.compile({
type: 'object',
properties: {
message: { type: 'string' },
},
}),
},
},
async () => {
return {
statusCode: 200,
body: {
message: 'Metrics test successful',
},
};
},
);
app.use(api.router);
const response = await request(app).get('/api/test').set('user-agent', 'test');
expect(response.statusCode).toBe(200);
expect(response.body).toHaveProperty('message', 'Metrics test successful');
expect(summary.startTimer).toHaveBeenCalledTimes(1);
expect(mockEndTimer).toHaveBeenCalledWith({ status: 200, method: 'get', version: 1, user_agent: 'test', entrypoint: '/api/test' });
});
it('should strip path from metrics', async () => {
const ajv = new Ajv();
const app = express();
const api = new Router('/api');
const settings = new CachedSettings();
settings.set({
_id: 'Prometheus_API_User_Agent',
value: true,
} as any);
const summary = {
startTimer: jest.fn().mockImplementation(() => jest.fn()),
};
// Get the mock startTimer function
const mockEndTimer = jest.fn();
summary.startTimer.mockReturnValue(mockEndTimer);
const histogram = { startTimer: jest.fn().mockReturnValue(jest.fn()) };
const responseSizeHistogram = { observe: jest.fn() };
const activeRequestsGauge = { inc: jest.fn(), dec: jest.fn() };
api
.use(
metricsMiddleware({
basePathRegex: new RegExp(/^\/api\//),
api: { version: 1 } as any,
settings,
endpointTimeSummary: summary as any,
endpointTimeHistogram: histogram as any,
responseSizeHistogram: responseSizeHistogram as any,
activeRequestsGauge: activeRequestsGauge as any,
}),
)
.get(
'/test',
{
response: {
200: ajv.compile({
type: 'object',
properties: {
message: { type: 'string' },
},
}),
},
},
async () => {
return {
statusCode: 200,
body: {
message: 'Metrics test successful',
},
};
},
);
app.use(api.router);
const response = await request(app).get('/api/test').set('user-agent', 'test');
expect(response.statusCode).toBe(200);
expect(response.body).toHaveProperty('message', 'Metrics test successful');
expect(summary.startTimer).toHaveBeenCalledTimes(1);
expect(mockEndTimer).toHaveBeenCalledWith({ status: 200, method: 'get', version: 1, user_agent: 'test', entrypoint: 'test' });
});
it('should decode path for method.call endpoints', async () => {
const ajv = new Ajv();
const app = express();
const settings = new CachedSettings();
const api = new Router('/api');
const summary = {
startTimer: jest.fn().mockImplementation(() => jest.fn()),
};
// Get the mock startTimer function
const mockEndTimer = jest.fn();
summary.startTimer.mockReturnValue(mockEndTimer);
const histogram = { startTimer: jest.fn().mockReturnValue(jest.fn()) };
const responseSizeHistogram = { observe: jest.fn() };
const activeRequestsGauge = { inc: jest.fn(), dec: jest.fn() };
api
.use(
metricsMiddleware({
basePathRegex: new RegExp(/^\/api\//),
api: { version: 1 } as any,
settings,
endpointTimeSummary: summary as any,
endpointTimeHistogram: histogram as any,
responseSizeHistogram: responseSizeHistogram as any,
activeRequestsGauge: activeRequestsGauge as any,
}),
)
.get(
'/method.call/:id',
{
response: {
200: ajv.compile({
type: 'object',
properties: {
message: { type: 'string' },
},
}),
},
},
async () => {
return {
statusCode: 200,
body: {
message: `Metrics test successful`,
},
};
},
);
app.use(api.router);
const response = await request(app).get('/api/method.call/get%3Aparam').set('user-agent', 'test');
expect(response.statusCode).toBe(200);
expect(response.body).toHaveProperty('message', 'Metrics test successful');
expect(summary.startTimer).toHaveBeenCalledTimes(1);
expect(mockEndTimer).toHaveBeenCalledWith({
status: 200,
method: 'get',
version: 1,
entrypoint: 'method.call/get:param',
});
});
it('should only record requests matching its own base path', async () => {
const ajv = new Ajv();
const app = express();
const settings = new CachedSettings();
const makeMetrics = () => {
const endTimer = jest.fn();
return {
endTimer,
summary: { startTimer: jest.fn().mockReturnValue(endTimer) },
histogram: { startTimer: jest.fn().mockReturnValue(jest.fn()) },
responseSizeHistogram: { observe: jest.fn() },
activeRequestsGauge: { inc: jest.fn(), dec: jest.fn() },
};
};
const v1Metrics = makeMetrics();
const experimentalMetrics = makeMetrics();
const route = (router: Router<any, any, any>) =>
router.get(
'/test',
{
response: {
200: ajv.compile({
type: 'object',
properties: {
message: { type: 'string' },
},
}),
},
},
async () => ({
statusCode: 200,
body: { message: 'Metrics test successful' },
}),
);
const api = new Router('/api');
api
.use(
metricsMiddleware({
basePathRegex: new RegExp(/^\/api\/v1\//),
api: { version: 'v1' } as any,
settings,
endpointTimeSummary: v1Metrics.summary as any,
endpointTimeHistogram: v1Metrics.histogram as any,
responseSizeHistogram: v1Metrics.responseSizeHistogram as any,
activeRequestsGauge: v1Metrics.activeRequestsGauge as any,
}),
)
.use(
metricsMiddleware({
basePathRegex: new RegExp(/^\/api\/experimental\//),
api: { version: 'experimental' } as any,
settings,
endpointTimeSummary: experimentalMetrics.summary as any,
endpointTimeHistogram: experimentalMetrics.histogram as any,
responseSizeHistogram: experimentalMetrics.responseSizeHistogram as any,
activeRequestsGauge: experimentalMetrics.activeRequestsGauge as any,
}),
)
.use(route(new Router('/v1')))
.use(route(new Router('/experimental')));
app.use(api.router);
expect((await request(app).get('/api/v1/test')).statusCode).toBe(200);
expect(v1Metrics.summary.startTimer).toHaveBeenCalledTimes(1);
expect(v1Metrics.endTimer).toHaveBeenCalledWith({ status: 200, method: 'get', version: 'v1', entrypoint: 'test' });
expect(experimentalMetrics.summary.startTimer).not.toHaveBeenCalled();
expect(experimentalMetrics.activeRequestsGauge.inc).not.toHaveBeenCalled();
expect((await request(app).get('/api/experimental/test')).statusCode).toBe(200);
expect(experimentalMetrics.summary.startTimer).toHaveBeenCalledTimes(1);
expect(experimentalMetrics.endTimer).toHaveBeenCalledWith({
status: 200,
method: 'get',
version: 'experimental',
entrypoint: 'test',
});
expect(v1Metrics.summary.startTimer).toHaveBeenCalledTimes(1);
});
it('should sample the default router and unmatched paths exactly once', async () => {
const ajv = new Ajv();
const app = express();
const settings = new CachedSettings();
const makeMetrics = () => {
const endTimer = jest.fn();
return {
endTimer,
summary: { startTimer: jest.fn().mockReturnValue(endTimer) },
histogram: { startTimer: jest.fn().mockReturnValue(jest.fn()) },
responseSizeHistogram: { observe: jest.fn() },
activeRequestsGauge: { inc: jest.fn(), dec: jest.fn() },
};
};
const v1Metrics = makeMetrics();
const defaultMetrics = makeMetrics();
const wire = (metrics: ReturnType<typeof makeMetrics>, extra: { basePathRegex?: RegExp; excludePathRegex?: RegExp }, version: string) =>
metricsMiddleware({
...extra,
api: { version },
settings,
endpointTimeSummary: metrics.summary as any,
endpointTimeHistogram: metrics.histogram as any,
responseSizeHistogram: metrics.responseSizeHistogram as any,
activeRequestsGauge: metrics.activeRequestsGauge as any,
});
const api = new Router('/api')
.use(wire(v1Metrics, { basePathRegex: new RegExp(/^\/api\/v1\//) }, 'v1'))
.use(wire(defaultMetrics, { excludePathRegex: new RegExp(/^\/api\/(v1|experimental|apps)\//) }, 'default'));
const route = (router: Router<any, any, any>, subpath: string) =>
router.get(subpath, { response: { 200: ajv.compile({ type: 'object' }) } }, async () => ({ statusCode: 200 as const, body: {} }));
// the catch-all router is mounted first on purpose: the exclusion guard has to hold
// regardless of the order the versioned routers happen to be registered in
api.use(route(new Router(''), 'info'));
api.use(route(new Router('/v1'), '/test'));
expect((await request(app.use(api.router)).get('/api/info')).statusCode).toBe(200);
expect(v1Metrics.summary.startTimer).not.toHaveBeenCalled();
expect(defaultMetrics.endTimer).toHaveBeenCalledWith({ status: 200, method: 'get', version: 'default', entrypoint: '/api/info' });
defaultMetrics.endTimer.mockClear();
expect((await request(app).get('/api/v1/test')).statusCode).toBe(200);
expect(v1Metrics.summary.startTimer).toHaveBeenCalledTimes(1);
expect(defaultMetrics.summary.startTimer).toHaveBeenCalledTimes(1); // still just the /api/info call
expect(defaultMetrics.endTimer).not.toHaveBeenCalled();
expect((await request(app).get('/api/bogus')).statusCode).toBe(404);
expect(defaultMetrics.endTimer).toHaveBeenCalledWith({ status: 404, method: 'get', version: 'default', entrypoint: '/api/*' });
});
});