befly 3.24.19 → 3.25.0
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/_apis.js +20 -0
- package/apis/admin/delete.js +3 -1
- package/apis/admin/detail.js +1 -3
- package/apis/admin/select.js +9 -7
- package/apis/admin/update.js +2 -2
- package/apis/api/all.js +6 -11
- package/apis/api/select.js +18 -19
- package/apis/dict/_dict.js +24 -0
- package/apis/dict/all.js +6 -4
- package/apis/dict/detail.js +9 -5
- package/apis/dict/insert.js +4 -11
- package/apis/dict/items.js +5 -6
- package/apis/dict/select.js +13 -9
- package/apis/dict/update.js +9 -13
- package/apis/dictType/select.js +4 -4
- package/apis/email/config.js +9 -11
- package/apis/email/logList.js +14 -4
- package/apis/email/send.js +10 -3
- package/apis/email/verify.js +9 -7
- package/apis/loginLog/select.js +11 -4
- package/apis/menu/all.js +13 -19
- package/apis/menu/select.js +19 -14
- package/apis/operateLog/select.js +13 -4
- package/apis/role/_role.js +21 -0
- package/apis/role/all.js +1 -3
- package/apis/role/apiSave.js +8 -15
- package/apis/role/apis.js +4 -10
- package/apis/role/delete.js +28 -36
- package/apis/role/detail.js +4 -10
- package/apis/role/insert.js +12 -10
- package/apis/role/menuSave.js +9 -15
- package/apis/role/menus.js +4 -10
- package/apis/role/save.js +19 -23
- package/apis/role/select.js +12 -9
- package/apis/role/update.js +14 -15
- package/apis/source/imageList.js +3 -3
- package/apis/sysConfig/get.js +11 -23
- package/apis/sysConfig/insert.js +22 -27
- package/apis/sysConfig/select.js +11 -5
- package/apis/sysConfig/update.js +33 -38
- package/apis/tongJi/_tongJi.js +41 -0
- package/apis/tongJi/cacheHealth.js +8 -30
- package/apis/tongJi/errorList.js +26 -27
- package/apis/tongJi/errorReport.js +26 -43
- package/apis/tongJi/errorStats.js +17 -48
- package/apis/tongJi/fallbackReset.js +7 -15
- package/apis/tongJi/infoReport.js +20 -32
- package/apis/tongJi/infoStats.js +5 -17
- package/apis/tongJi/onlineReport.js +50 -56
- package/apis/tongJi/onlineStats.js +97 -111
- package/checks/config.js +44 -1
- package/configs/beflyConfig.json +10 -1
- package/index.js +25 -0
- package/lib/dbHelper.js +1 -1
- package/lib/dbParse.js +61 -99
- package/lib/dbUtil.js +101 -21
- package/lib/redisHelper.js +25 -0
- package/lib/sqlBuilder.js +6 -0
- package/package.json +1 -1
- package/plugins/email.js +3 -6
- package/router/api.js +0 -7
- package/utils/email.js +3 -0
- package/apis/admin/cacheRefresh.js +0 -122
- package/apis/dashboard/configStatus.js +0 -39
- package/apis/dashboard/performanceMetrics.js +0 -23
- package/apis/dashboard/permissionStats.js +0 -27
- package/apis/dashboard/systemInfo.js +0 -19
- package/lib/requestMetrics.js +0 -203
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 刷新全部缓存接口
|
|
3
|
-
*
|
|
4
|
-
* 功能:
|
|
5
|
-
* 1. 刷新接口缓存(apis:all)
|
|
6
|
-
* 2. 刷新菜单缓存(menus:all)
|
|
7
|
-
* 3. 刷新角色缓存(role:info:{code})
|
|
8
|
-
* 4. 重建角色接口权限缓存(版本化 Set + activeVersion 原子切换)
|
|
9
|
-
* 5. 重建角色菜单权限缓存(版本化 Set + activeVersion 原子切换)
|
|
10
|
-
*
|
|
11
|
-
* 使用场景:
|
|
12
|
-
* - 执行数据库同步后
|
|
13
|
-
* - 手动修改配置需要立即生效
|
|
14
|
-
* - 缓存出现异常需要重建
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { isNonEmptyString } from "#root/utils/is.js";
|
|
18
|
-
|
|
19
|
-
export default {
|
|
20
|
-
name: "刷新全部缓存",
|
|
21
|
-
method: "POST",
|
|
22
|
-
body: "none",
|
|
23
|
-
auth: true,
|
|
24
|
-
fields: {},
|
|
25
|
-
required: [],
|
|
26
|
-
handler: async (befly) => {
|
|
27
|
-
try {
|
|
28
|
-
const results = {
|
|
29
|
-
apis: { success: false, count: 0 },
|
|
30
|
-
menus: { success: false, count: 0 },
|
|
31
|
-
roles: { success: false, count: 0 },
|
|
32
|
-
roleApiPermissions: { success: false },
|
|
33
|
-
roleMenuPermissions: { success: false }
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// 1. 刷新接口缓存
|
|
37
|
-
try {
|
|
38
|
-
const apis = await befly.mysql.getAll({
|
|
39
|
-
table: "beflyApi"
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
await befly.redis.setObject("apis:all", apis.data.lists);
|
|
43
|
-
results["apis"] = { success: true, count: apis.data.lists.length };
|
|
44
|
-
} catch (error) {
|
|
45
|
-
befly.logger.error("刷新接口缓存失败", error);
|
|
46
|
-
results["apis"] = { success: false, error: error.message };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 2. 刷新菜单缓存
|
|
50
|
-
try {
|
|
51
|
-
const menus = await befly.mysql.getAll({
|
|
52
|
-
table: "beflyMenu"
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
await befly.redis.setObject("menus:all", menus.data.lists);
|
|
56
|
-
|
|
57
|
-
const parentCount = menus.data.lists.filter((m) => !isNonEmptyString(m.parentPath)).length;
|
|
58
|
-
const childCount = menus.data.lists.filter((m) => isNonEmptyString(m.parentPath)).length;
|
|
59
|
-
|
|
60
|
-
results["menus"] = {
|
|
61
|
-
success: true,
|
|
62
|
-
count: menus.data.lists.length,
|
|
63
|
-
parentCount: parentCount,
|
|
64
|
-
childCount: childCount
|
|
65
|
-
};
|
|
66
|
-
} catch (error) {
|
|
67
|
-
befly.logger.error("刷新菜单缓存失败", error);
|
|
68
|
-
results["menus"] = { success: false, error: error.message };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 3. 刷新角色权限缓存
|
|
72
|
-
try {
|
|
73
|
-
const roles = await befly.mysql.getAll({
|
|
74
|
-
table: "beflyRole"
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// 使用 setBatch 批量缓存所有角色
|
|
78
|
-
const count = await befly.redis.setBatch(
|
|
79
|
-
roles.data.lists.map((role) => ({
|
|
80
|
-
key: `role:info:${role.code}`,
|
|
81
|
-
value: role
|
|
82
|
-
}))
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
results["roles"] = { success: true, count: count };
|
|
86
|
-
} catch (error) {
|
|
87
|
-
befly.logger.error("刷新角色缓存失败", error);
|
|
88
|
-
results["roles"] = { success: false, error: error.message };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 4. 重建角色接口权限缓存(版本化 + 原子切换)
|
|
92
|
-
try {
|
|
93
|
-
await befly.cache.cacheRoleApis();
|
|
94
|
-
results["roleApiPermissions"] = { success: true };
|
|
95
|
-
} catch (error) {
|
|
96
|
-
befly.logger.error("重建角色接口权限缓存失败", error);
|
|
97
|
-
results["roleApiPermissions"] = { success: false, error: error.message };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 5. 重建角色菜单权限缓存
|
|
101
|
-
try {
|
|
102
|
-
await befly.cache.cacheRoleMenus();
|
|
103
|
-
results["roleMenuPermissions"] = { success: true };
|
|
104
|
-
} catch (error) {
|
|
105
|
-
befly.logger.error("重建角色菜单权限缓存失败", error);
|
|
106
|
-
results["roleMenuPermissions"] = { success: false, error: error.message };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 检查是否全部成功
|
|
110
|
-
const allSuccess = results["apis"].success && results["menus"].success && results["roles"].success && results["roleApiPermissions"].success && results["roleMenuPermissions"].success;
|
|
111
|
-
|
|
112
|
-
if (allSuccess) {
|
|
113
|
-
return befly.tool.Yes("全部缓存刷新成功", results);
|
|
114
|
-
} else {
|
|
115
|
-
return befly.tool.No("部分缓存刷新失败", results);
|
|
116
|
-
}
|
|
117
|
-
} catch (error) {
|
|
118
|
-
befly.logger.error("刷新全部缓存失败", error);
|
|
119
|
-
return befly.tool.No("刷新全部缓存失败", { error: error.message });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
name: "获取配置状态",
|
|
3
|
-
method: "POST",
|
|
4
|
-
body: "none",
|
|
5
|
-
auth: true,
|
|
6
|
-
fields: {},
|
|
7
|
-
required: [],
|
|
8
|
-
handler: async (befly) => {
|
|
9
|
-
const status = {
|
|
10
|
-
database: { status: "ok", latency: 0, message: "" },
|
|
11
|
-
redis: { status: "ok", latency: 0, message: "" },
|
|
12
|
-
fileSystem: { status: "ok" },
|
|
13
|
-
email: { status: "warning", message: "未配置" },
|
|
14
|
-
oss: { status: "warning", message: "未配置" }
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const startTime = Date.now();
|
|
19
|
-
await befly.mysql.execute("SELECT 1");
|
|
20
|
-
status.database.latency = Date.now() - startTime;
|
|
21
|
-
status.database.status = "ok";
|
|
22
|
-
} catch {
|
|
23
|
-
status.database.status = "error";
|
|
24
|
-
status.database.message = "连接失败";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const startTime = Date.now();
|
|
29
|
-
await befly.redis.ping();
|
|
30
|
-
status.redis.latency = Date.now() - startTime;
|
|
31
|
-
status.redis.status = "ok";
|
|
32
|
-
} catch {
|
|
33
|
-
status.redis.status = "error";
|
|
34
|
-
status.redis.message = "连接失败";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return befly.tool.Yes("获取成功", status);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { requestMetrics } from "#root/lib/requestMetrics.js";
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
name: "获取性能指标",
|
|
5
|
-
method: "POST",
|
|
6
|
-
body: "none",
|
|
7
|
-
auth: true,
|
|
8
|
-
fields: {},
|
|
9
|
-
required: [],
|
|
10
|
-
handler: async (befly) => {
|
|
11
|
-
const metrics = requestMetrics.getSnapshot();
|
|
12
|
-
|
|
13
|
-
return befly.tool.Yes("获取成功", {
|
|
14
|
-
avgResponseTime: Number(metrics.avgResponseTime || 0),
|
|
15
|
-
qps: Number(metrics.qps || 0),
|
|
16
|
-
errorRate: Number(metrics.errorRate || 0),
|
|
17
|
-
activeRequests: Number(metrics.activeRequests || 0),
|
|
18
|
-
activeConnections: Number(metrics.activeRequests || 0),
|
|
19
|
-
slowestApi: metrics.slowestApi || null,
|
|
20
|
-
slowestApis: Array.isArray(metrics.slowestApis) ? metrics.slowestApis : []
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
name: "获取权限统计",
|
|
3
|
-
method: "POST",
|
|
4
|
-
body: "none",
|
|
5
|
-
auth: true,
|
|
6
|
-
fields: {},
|
|
7
|
-
required: [],
|
|
8
|
-
handler: async (befly) => {
|
|
9
|
-
const menuCount = await befly.mysql.getCount({
|
|
10
|
-
table: "beflyMenu"
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
const apiCount = await befly.mysql.getCount({
|
|
14
|
-
table: "beflyApi"
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const roleCount = await befly.mysql.getCount({
|
|
18
|
-
table: "beflyRole"
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
return befly.tool.Yes("获取成功", {
|
|
22
|
-
menuCount: menuCount.data,
|
|
23
|
-
apiCount: apiCount.data,
|
|
24
|
-
roleCount: roleCount.data
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
name: "获取系统信息",
|
|
3
|
-
method: "POST",
|
|
4
|
-
body: "none",
|
|
5
|
-
auth: true,
|
|
6
|
-
fields: {},
|
|
7
|
-
required: [],
|
|
8
|
-
handler: async (befly) => {
|
|
9
|
-
const startTime = Date.now() - Math.floor(process.uptime() * 1000);
|
|
10
|
-
const uptime = Math.floor(process.uptime() * 1000);
|
|
11
|
-
const environment = process.env.RUN_MODE || "development";
|
|
12
|
-
|
|
13
|
-
return befly.tool.Yes("获取成功", {
|
|
14
|
-
environment: environment,
|
|
15
|
-
startTime: startTime,
|
|
16
|
-
uptime: uptime
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
};
|
package/lib/requestMetrics.js
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
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
|
-
};
|