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('未知错误') });
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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