mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-17 15:46:59 +08:00
fix: propagate initialization errors and reduce request metrics
This commit is contained in:
+4
-4
@@ -80,8 +80,8 @@ export default (app: Router) => {
|
||||
return res.send({ code: 450, message: t('未知错误') });
|
||||
}
|
||||
const userService = Container.get(UserService);
|
||||
await userService.updateUsernameAndPassword(req.body);
|
||||
res.send({ code: 200, message: t('更新成功') });
|
||||
const result = await userService.updateUsernameAndPassword(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
@@ -282,8 +282,8 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
await userService.updateUsernameAndPassword(req.body);
|
||||
res.send({ code: 200, message: t('更新成功') });
|
||||
const result = await userService.updateUsernameAndPassword(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
|
||||
@@ -3,46 +3,34 @@ import Logger from '../loaders/logger';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { metricsService } from '../services/metrics';
|
||||
|
||||
interface RequestMetrics {
|
||||
method: string;
|
||||
path: string;
|
||||
duration: number;
|
||||
statusCode: number;
|
||||
timestamp: number;
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
const requestMetrics: RequestMetrics[] = [];
|
||||
const UNMONITORED_PATH_SUFFIXES = ['/api/health', '/open/health'];
|
||||
const HTTP_METRIC_SAMPLE_INTERVAL = 10;
|
||||
let requestSampleOffset = 0;
|
||||
|
||||
export const monitoringMiddleware = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
if (UNMONITORED_PATH_SUFFIXES.some((path) => req.path.endsWith(path))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
const originalEnd = res.end;
|
||||
|
||||
res.end = function (chunk?: any, encoding?: any, cb?: any) {
|
||||
const duration = performance.now() - start;
|
||||
const metric: RequestMetrics = {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
duration,
|
||||
statusCode: res.statusCode,
|
||||
timestamp: Date.now(),
|
||||
platform: req.platform,
|
||||
};
|
||||
|
||||
requestMetrics.push(metric);
|
||||
const shouldSample = requestSampleOffset === 0;
|
||||
requestSampleOffset =
|
||||
(requestSampleOffset + 1) % HTTP_METRIC_SAMPLE_INTERVAL;
|
||||
if (shouldSample) {
|
||||
metricsService.record('http_request', duration, {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
statusCode: res.statusCode.toString(),
|
||||
...(req.platform && { platform: req.platform }),
|
||||
});
|
||||
|
||||
if (requestMetrics.length > 1000) {
|
||||
requestMetrics.shift();
|
||||
}
|
||||
|
||||
if (duration > 1000) {
|
||||
@@ -58,23 +46,3 @@ export const monitoringMiddleware = (
|
||||
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user