fix: propagate initialization errors and reduce request metrics

This commit is contained in:
whyour
2026-08-17 08:55:53 +08:00
parent afe9b5702b
commit f71fd5ebd6
4 changed files with 193 additions and 53 deletions
+4 -4
View File
@@ -80,8 +80,8 @@ export default (app: Router) => {
return res.send({ code: 450, message: t('未知错误') }); return res.send({ code: 450, message: t('未知错误') });
} }
const userService = Container.get(UserService); const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body); const result = await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: t('更新成功') }); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -282,8 +282,8 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body); const result = await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: t('更新成功') }); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
+11 -43
View File
@@ -3,46 +3,34 @@ import Logger from '../loaders/logger';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { metricsService } from '../services/metrics'; import { metricsService } from '../services/metrics';
interface RequestMetrics { const UNMONITORED_PATH_SUFFIXES = ['/api/health', '/open/health'];
method: string; const HTTP_METRIC_SAMPLE_INTERVAL = 10;
path: string; let requestSampleOffset = 0;
duration: number;
statusCode: number;
timestamp: number;
platform?: string;
}
const requestMetrics: RequestMetrics[] = [];
export const monitoringMiddleware = ( export const monitoringMiddleware = (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction,
) => { ) => {
if (UNMONITORED_PATH_SUFFIXES.some((path) => req.path.endsWith(path))) {
return next();
}
const start = performance.now(); const start = performance.now();
const originalEnd = res.end; const originalEnd = res.end;
res.end = function (chunk?: any, encoding?: any, cb?: any) { res.end = function (chunk?: any, encoding?: any, cb?: any) {
const duration = performance.now() - start; const duration = performance.now() - start;
const metric: RequestMetrics = { const shouldSample = requestSampleOffset === 0;
method: req.method, requestSampleOffset =
path: req.path, (requestSampleOffset + 1) % HTTP_METRIC_SAMPLE_INTERVAL;
duration, if (shouldSample) {
statusCode: res.statusCode,
timestamp: Date.now(),
platform: req.platform,
};
requestMetrics.push(metric);
metricsService.record('http_request', duration, { metricsService.record('http_request', duration, {
method: req.method, method: req.method,
path: req.path, path: req.path,
statusCode: res.statusCode.toString(), statusCode: res.statusCode.toString(),
...(req.platform && { platform: req.platform }), ...(req.platform && { platform: req.platform }),
}); });
if (requestMetrics.length > 1000) {
requestMetrics.shift();
} }
if (duration > 1000) { if (duration > 1000) {
@@ -58,23 +46,3 @@ export const monitoringMiddleware = (
next(); next();
}; };
export const getMetrics = () => {
return {
totalRequests: requestMetrics.length,
averageDuration:
requestMetrics.reduce((acc, curr) => acc + curr.duration, 0) /
requestMetrics.length,
requestsByMethod: requestMetrics.reduce((acc, curr) => {
acc[curr.method] = (acc[curr.method] || 0) + 1;
return acc;
}, {} as Record<string, number>),
requestsByPlatform: requestMetrics.reduce((acc, curr) => {
if (curr.platform) {
acc[curr.platform] = (acc[curr.platform] || 0) + 1;
}
return acc;
}, {} as Record<string, number>),
recentRequests: requestMetrics.slice(-10),
};
};
+73
View File
@@ -0,0 +1,73 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const { monitoringMiddleware } = require('../../back/middlewares/monitoring');
const { metricsService } = require('../../back/services/metrics');
function createResponse() {
return {
statusCode: 200,
ended: false,
end() {
this.ended = true;
},
};
}
test('health probes bypass request metric retention with or without base URL', () => {
const before = metricsService.getMetrics('http_request', {
path: '/api/health',
}).count;
let nextCalled = false;
for (const path of ['/api/health', '/ql/api/health']) {
const response = createResponse();
monitoringMiddleware({ method: 'GET', path }, response, () => {
nextCalled = true;
});
response.end();
assert.equal(response.ended, true);
}
const after = metricsService.getMetrics('http_request', {
path: '/api/health',
}).count;
assert.equal(nextCalled, true);
assert.equal(after, before);
});
test('non-health requests keep bounded service metrics', () => {
const path = '/api/monitoring-test';
const before = metricsService.getMetrics('http_request', { path }).count;
const response = createResponse();
monitoringMiddleware(
{ method: 'GET', path, platform: 'desktop' },
response,
() => {},
);
response.end();
const after = metricsService.getMetrics('http_request', { path }).count;
assert.equal(response.ended, true);
assert.equal(after, before + 1);
});
test('ordinary request metrics are sampled instead of retained per request', () => {
const path = '/api/monitoring-sampling-test';
const before = metricsService.getMetrics('http_request', { path }).count;
for (let index = 0; index < 30; index += 1) {
const response = createResponse();
monitoringMiddleware(
{ method: 'GET', path, platform: 'desktop' },
response,
() => {},
);
response.end();
}
const recorded =
metricsService.getMetrics('http_request', { path }).count - before;
assert.ok(recorded >= 2 && recorded <= 3);
});
+99
View File
@@ -0,0 +1,99 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const express = require('express');
const { Container } = require('typedi');
test('initialization returns the username and password validation result', async (t) => {
const originalGet = Container.get;
const userServicePath = require.resolve('../../back/services/user');
const originalUserService = require.cache[userServicePath];
const i18nPath = require.resolve('../../back/shared/i18n');
const originalI18n = require.cache[i18nPath];
const utilPath = require.resolve('../../back/config/util');
const originalUtil = require.cache[utilPath];
const authPath = require.resolve('../../back/shared/auth');
const originalAuth = require.cache[authPath];
require.cache[userServicePath] = {
id: userServicePath,
filename: userServicePath,
loaded: true,
exports: { __esModule: true, default: class UserService {} },
children: [],
paths: [],
};
require.cache[utilPath] = {
id: utilPath,
filename: utilPath,
loaded: true,
exports: { getToken: () => '', isDemoEnv: () => false },
children: [],
paths: [],
};
require.cache[authPath] = {
id: authPath,
filename: authPath,
loaded: true,
exports: { isDefaultAuthInfo: () => false },
children: [],
paths: [],
};
require.cache[i18nPath] = {
id: i18nPath,
filename: i18nPath,
loaded: true,
exports: { t: (message) => message },
children: [],
paths: [],
};
Container.get = () => ({
updateUsernameAndPassword: async () => ({
code: 400,
message: 'password rejected',
}),
});
t.after(() => {
Container.get = originalGet;
if (originalUserService) {
require.cache[userServicePath] = originalUserService;
} else {
delete require.cache[userServicePath];
}
if (originalI18n) {
require.cache[i18nPath] = originalI18n;
} else {
delete require.cache[i18nPath];
}
if (originalUtil) {
require.cache[utilPath] = originalUtil;
} else {
delete require.cache[utilPath];
}
if (originalAuth) {
require.cache[authPath] = originalAuth;
} else {
delete require.cache[authPath];
}
});
const app = express.Router();
require('../../back/api/user').default(app);
const userRouter = app.stack.find((layer) => layer.name === 'router').handle;
const initRoute = userRouter.stack.find(
(layer) => layer.route?.path === '/init',
);
const handler = initRoute.route.stack.at(-1).handle;
let responseBody;
await handler(
{ body: { username: 'admin', password: 'admin' } },
{ send: (body) => (responseBody = body) },
(error) => {
throw error;
},
);
assert.deepEqual(responseBody, {
code: 400,
message: 'password rejected',
});
});