befly 3.24.17 → 3.24.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apis/admin/cacheRefresh.js +2 -2
- package/apis/dashboard/environmentInfo.js +6 -1
- package/apis/dashboard/performanceMetrics.js +11 -8
- package/apis/dashboard/serviceStatus.js +78 -60
- package/apis/tongJi/cacheHealth.js +214 -0
- package/apis/tongJi/errorReport.js +72 -1
- package/apis/tongJi/errorStats.js +160 -5
- package/apis/tongJi/fallbackReset.js +69 -0
- package/apis/tongJi/infoReport.js +116 -16
- package/apis/tongJi/infoStats.js +160 -68
- package/apis/tongJi/onlineReport.js +14 -23
- package/apis/tongJi/onlineStats.js +94 -91
- package/hooks/permission.js +6 -2
- package/hooks/rateLimit.js +245 -0
- package/lib/cacheHelper.js +105 -60
- package/lib/redisHelper.js +68 -0
- package/lib/requestMetrics.js +203 -0
- package/package.json +2 -2
- package/plugins/email.js +6 -2
- package/router/api.js +7 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const WINDOW_SECONDS = 60;
|
|
2
|
+
const SLOWEST_WINDOW_MS = 60 * 60 * 1000;
|
|
3
|
+
const SLOWEST_API_LIMIT = 5;
|
|
4
|
+
|
|
5
|
+
const requestBuckets = new Map();
|
|
6
|
+
const activeRequestState = {
|
|
7
|
+
count: 0
|
|
8
|
+
};
|
|
9
|
+
const slowestState = {
|
|
10
|
+
apiPath: "",
|
|
11
|
+
duration: 0,
|
|
12
|
+
timestamp: 0
|
|
13
|
+
};
|
|
14
|
+
const slowestListState = [];
|
|
15
|
+
|
|
16
|
+
function toSecondTimestamp(timestamp) {
|
|
17
|
+
return Math.floor(timestamp / 1000);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getOrCreateBucket(secondTimestamp) {
|
|
21
|
+
let bucket = requestBuckets.get(secondTimestamp);
|
|
22
|
+
|
|
23
|
+
if (bucket) {
|
|
24
|
+
return bucket;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
bucket = {
|
|
28
|
+
requestCount: 0,
|
|
29
|
+
errorCount: 0,
|
|
30
|
+
durationTotal: 0
|
|
31
|
+
};
|
|
32
|
+
requestBuckets.set(secondTimestamp, bucket);
|
|
33
|
+
|
|
34
|
+
return bucket;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cleanupBuckets(nowSecondTimestamp) {
|
|
38
|
+
const minSecondTimestamp = nowSecondTimestamp - WINDOW_SECONDS + 1;
|
|
39
|
+
|
|
40
|
+
for (const key of requestBuckets.keys()) {
|
|
41
|
+
if (key >= minSecondTimestamp) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
requestBuckets.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cleanupSlowest(nowTimestamp) {
|
|
50
|
+
if (slowestState.timestamp <= 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (nowTimestamp - slowestState.timestamp <= SLOWEST_WINDOW_MS) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
slowestState.apiPath = "";
|
|
59
|
+
slowestState.duration = 0;
|
|
60
|
+
slowestState.timestamp = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cleanupSlowestList(nowTimestamp) {
|
|
64
|
+
for (let index = slowestListState.length - 1; index >= 0; index -= 1) {
|
|
65
|
+
if (nowTimestamp - slowestListState[index].timestamp <= SLOWEST_WINDOW_MS) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
slowestListState.splice(index, 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function updateSlowestList(apiPath, duration, nowTimestamp) {
|
|
74
|
+
const safePath = String(apiPath || "");
|
|
75
|
+
|
|
76
|
+
if (!safePath) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let existing = null;
|
|
81
|
+
|
|
82
|
+
for (const item of slowestListState) {
|
|
83
|
+
if (item.path !== safePath) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
existing = item;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (existing) {
|
|
92
|
+
if (duration > existing.time) {
|
|
93
|
+
existing.time = duration;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
existing.timestamp = nowTimestamp;
|
|
97
|
+
} else {
|
|
98
|
+
slowestListState.push({
|
|
99
|
+
path: safePath,
|
|
100
|
+
time: duration,
|
|
101
|
+
timestamp: nowTimestamp
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
slowestListState.sort((left, right) => {
|
|
106
|
+
if (right.time !== left.time) {
|
|
107
|
+
return right.time - left.time;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return right.timestamp - left.timestamp;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (slowestListState.length > SLOWEST_API_LIMIT) {
|
|
114
|
+
slowestListState.length = SLOWEST_API_LIMIT;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const requestMetrics = {
|
|
119
|
+
reset: function () {
|
|
120
|
+
requestBuckets.clear();
|
|
121
|
+
activeRequestState.count = 0;
|
|
122
|
+
slowestState.apiPath = "";
|
|
123
|
+
slowestState.duration = 0;
|
|
124
|
+
slowestState.timestamp = 0;
|
|
125
|
+
slowestListState.length = 0;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
onRequestStart: function () {
|
|
129
|
+
activeRequestState.count += 1;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
onRequestEnd: function (apiPath, duration, isError) {
|
|
133
|
+
const nowTimestamp = Date.now();
|
|
134
|
+
const secondTimestamp = toSecondTimestamp(nowTimestamp);
|
|
135
|
+
const safeDuration = Number.isFinite(Number(duration)) ? Number(duration) : 0;
|
|
136
|
+
const bucket = getOrCreateBucket(secondTimestamp);
|
|
137
|
+
|
|
138
|
+
bucket.requestCount += 1;
|
|
139
|
+
bucket.durationTotal += safeDuration;
|
|
140
|
+
|
|
141
|
+
if (isError === true) {
|
|
142
|
+
bucket.errorCount += 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (safeDuration > slowestState.duration || nowTimestamp - slowestState.timestamp > SLOWEST_WINDOW_MS) {
|
|
146
|
+
slowestState.apiPath = String(apiPath || "");
|
|
147
|
+
slowestState.duration = safeDuration;
|
|
148
|
+
slowestState.timestamp = nowTimestamp;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
updateSlowestList(apiPath, safeDuration, nowTimestamp);
|
|
152
|
+
|
|
153
|
+
if (activeRequestState.count > 0) {
|
|
154
|
+
activeRequestState.count -= 1;
|
|
155
|
+
} else {
|
|
156
|
+
activeRequestState.count = 0;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
getSnapshot: function () {
|
|
161
|
+
const nowTimestamp = Date.now();
|
|
162
|
+
const nowSecondTimestamp = toSecondTimestamp(nowTimestamp);
|
|
163
|
+
|
|
164
|
+
cleanupBuckets(nowSecondTimestamp);
|
|
165
|
+
cleanupSlowest(nowTimestamp);
|
|
166
|
+
cleanupSlowestList(nowTimestamp);
|
|
167
|
+
|
|
168
|
+
let requestCount = 0;
|
|
169
|
+
let errorCount = 0;
|
|
170
|
+
let durationTotal = 0;
|
|
171
|
+
|
|
172
|
+
for (const bucket of requestBuckets.values()) {
|
|
173
|
+
requestCount += bucket.requestCount;
|
|
174
|
+
errorCount += bucket.errorCount;
|
|
175
|
+
durationTotal += bucket.durationTotal;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const qps = Number((requestCount / WINDOW_SECONDS).toFixed(2));
|
|
179
|
+
const errorRate = requestCount > 0 ? Number(((errorCount / requestCount) * 100).toFixed(2)) : 0;
|
|
180
|
+
const avgResponseTime = requestCount > 0 ? Math.round(durationTotal / requestCount) : 0;
|
|
181
|
+
const slowestApi = slowestState.apiPath
|
|
182
|
+
? {
|
|
183
|
+
path: slowestState.apiPath,
|
|
184
|
+
time: slowestState.duration
|
|
185
|
+
}
|
|
186
|
+
: null;
|
|
187
|
+
const slowestApis = slowestListState.map((item) => {
|
|
188
|
+
return {
|
|
189
|
+
path: item.path,
|
|
190
|
+
time: item.time
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
qps: qps,
|
|
196
|
+
errorRate: errorRate,
|
|
197
|
+
avgResponseTime: avgResponseTime,
|
|
198
|
+
activeRequests: activeRequestState.count,
|
|
199
|
+
slowestApi: slowestApi,
|
|
200
|
+
slowestApis: slowestApis
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.24.
|
|
3
|
+
"version": "3.24.19",
|
|
4
4
|
"gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"fast-xml-parser": "^5.8.0",
|
|
59
|
-
"nodemailer": "^8.0.
|
|
59
|
+
"nodemailer": "^8.0.9",
|
|
60
60
|
"pathe": "^2.0.3",
|
|
61
61
|
"picomatch": "^4.0.4",
|
|
62
62
|
"ua-parser-js": "^2.0.10",
|
package/plugins/email.js
CHANGED
|
@@ -7,18 +7,22 @@ import nodemailer from "nodemailer";
|
|
|
7
7
|
|
|
8
8
|
import { Logger } from "../lib/logger.js";
|
|
9
9
|
|
|
10
|
+
function canCreateTransport(config) {
|
|
11
|
+
return Boolean(String(config?.host || "").trim() && String(config?.user || "").trim() && String(config?.pass || "").trim());
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
export default {
|
|
11
15
|
order: 7,
|
|
12
16
|
handler: async function (befly) {
|
|
13
17
|
const config = befly?.config?.email || {};
|
|
14
18
|
let transporter = null;
|
|
15
19
|
|
|
16
|
-
if (config
|
|
20
|
+
if (canCreateTransport(config)) {
|
|
17
21
|
try {
|
|
18
22
|
transporter = nodemailer.createTransport({
|
|
19
23
|
host: config.host,
|
|
20
24
|
port: config.port || 25,
|
|
21
|
-
secure: config.ssl,
|
|
25
|
+
secure: config.secure ?? config.ssl,
|
|
22
26
|
auth: {
|
|
23
27
|
user: config.user,
|
|
24
28
|
pass: config.pass
|
package/router/api.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import picomatch from "picomatch";
|
|
7
7
|
|
|
8
8
|
import { Logger } from "../lib/logger.js";
|
|
9
|
+
import { requestMetrics } from "../lib/requestMetrics.js";
|
|
9
10
|
// 相对导入
|
|
10
11
|
import { setCorsOptions } from "../utils/cors.js";
|
|
11
12
|
import { getClientIp } from "../utils/getClientIp.js";
|
|
@@ -94,6 +95,9 @@ export function apiHandler(apis, hooks, context) {
|
|
|
94
95
|
apiRequired: apiData.required,
|
|
95
96
|
apiFile: apiData.filePath
|
|
96
97
|
};
|
|
98
|
+
const requestStartTime = Date.now();
|
|
99
|
+
let requestHasError = false;
|
|
100
|
+
requestMetrics.onRequestStart();
|
|
97
101
|
|
|
98
102
|
try {
|
|
99
103
|
// 4. 串联执行所有钩子
|
|
@@ -143,6 +147,7 @@ export function apiHandler(apis, hooks, context) {
|
|
|
143
147
|
// 7. 返回响应(自动处理 response/result/日志)
|
|
144
148
|
return FinalResponse(ctx);
|
|
145
149
|
} catch (err) {
|
|
150
|
+
requestHasError = true;
|
|
146
151
|
// 全局错误处理
|
|
147
152
|
Logger.error("请求错误", err, {
|
|
148
153
|
path: ctx.apiPath,
|
|
@@ -162,6 +167,8 @@ export function apiHandler(apis, hooks, context) {
|
|
|
162
167
|
msg: "内部服务错误"
|
|
163
168
|
};
|
|
164
169
|
return FinalResponse(ctx);
|
|
170
|
+
} finally {
|
|
171
|
+
requestMetrics.onRequestEnd(ctx.apiPath, Date.now() - requestStartTime, requestHasError);
|
|
165
172
|
}
|
|
166
173
|
};
|
|
167
174
|
}
|