// ==UserScript==
// @name 有咖互动接口上线统计数据
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 是否接口上线后查询日志麻烦,统计繁琐,使用本插件可以直接一次性查询
// @author DaiXukai([email protected])
// @match https://next.api.aliyun.com/**
// @icon https://next.api.aliyun.com/favicon.ico
// @license GNU GPLv3
// @grant GM_registerMenuCommand
// @grant GM_notification
// ==/UserScript==
// 使用方式:
// 1. 打开网站 https://next.api.aliyun.com/api/Sls/2020-12-30/GetHistograms
// 2. 登录(不可用) RAM 账号
// 3. 点击脚本“执行”,输入参数等待结果
// 插件配置
const config = {
// 日志库配置
logstore: {
// 访问日志
access: "hd_manbo_portal_access",
// 错误日志
error: "hd_manbo_portal_error",
}
}
// 运行状态
const state = {
running: false,
inputQuery: "",
inputDate: "1,2",
eleBox: undefined
}
/**
* prompt 用户输入,忽略空字符串
* @param {string} promptMsg 输入提示信息
* @param {string} def 默认输入内容(占位)
* @param {RegExp} regex 校验内容正则表达式
* @throws 1 点击取消抛出
* @returns string 用户输入内容
*/
function input(promptMsg, def, regex) {
let tmpInput = "";
while (!regex.test(tmpInput)) {
tmpInput = prompt(promptMsg, def);
if (tmpInput === null || tmpInput === undefined) {
// 点击取消
throw 1;
}
tmpInput = tmpInput.trim();
}
return tmpInput;
}
/**
* 请求接口查询日志统计数
* @param {string} logstore 日志库名
* @param {number} from 开始时间戳(秒)
* @param {number} to 结束时间戳(秒)
* @param {string} query 查询语句
* @returns Promise 结果为统计到的日志数量
*/
async function request(logstore, from, to, query) {
let resp = await fetch("https://next.api.aliyun.com/api/auth/product/openAPIRequest", {
"headers": {
"accept": "application/json",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"bx-v": "2.5.6",
"cache-control": "no-cache",
"content-type": "application/json",
"pragma": "no-cache",
"sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Microsoft Edge\";v=\"120\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin"
},
"referrer": "https://next.api.aliyun.com/api/Sls/2020-12-30/GetHistograms?sdkStyle=dara¶ms=%7B%22to%22:1703073392,%22from%22:1703072486,%22logstore%22:%22hd_service_live_info%22,%22project%22:%22hd-live%22,%22query%22:%22444%22%7D&tab=DEBUG&lang=PYTHON",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": "{\"action\":\"GetHistograms\",\"product\":\"Sls\",\"bodyStyle\":null,\"apiStyle\":\"ROA\",\"apiVersion\":\"2020-12-30\",\"accessKeyId\":null,\"proxyEndpoint\":\"http://popunify-inner-pre.aliyuncs.com\",\"endpoint\":\"cn-beijing.log.aliyuncs.com\",\"regionId\":\"cn-beijing\",\"paramObject\":{\"method\":\"GET\",\"path\":\"/logstores/{logstore}/index?type=histogram\",\"params\":\"[{\\\"name\\\":\\\"project\\\",\\\"position\\\":\\\"Host\\\",\\\"required\\\":true,\\\"checkBlank\\\":false,\\\"visibility\\\":\\\"Public\\\",\\\"deprecated\\\":false,\\\"type\\\":\\\"String\\\",\\\"title\\\":\\\"project 名称。\\\",\\\"example\\\":\\\"ali-test-project\\\",\\\"description\\\":\\\"project 名称。\\\"},{\\\"name\\\":\\\"logstore\\\",\\\"position\\\":\\\"Path\\\",\\\"required\\\":true,\\\"checkBlank\\\":false,\\\"visibility\\\":\\\"Public\\\",\\\"deprecated\\\":false,\\\"type\\\":\\\"String\\\",\\\"title\\\":\\\"logstore 名称。\\\",\\\"example\\\":\\\"test-logstore\\\",\\\"description\\\":\\\"Logstore名称。\\\"},{\\\"name\\\":\\\"from\\\",\\\"position\\\":\\\"Query\\\",\\\"required\\\":true,\\\"checkBlank\\\":false,\\\"visibility\\\":\\\"Public\\\",\\\"deprecated\\\":false,\\\"type\\\":\\\"Long\\\",\\\"title\\\":\\\"查询开始时间点。UNIX时间戳格式,表示从1970-1-1 00:00:00 UTC计算起的秒数。\\\\n\\\\n时间区间遵循“左闭右开”原则,即该时间区间包括区间开始时间点,但不包括区间结束时间点。如果from和to的值相同,则为无效区间,函数直接返回错误。\\\",\\\"example\\\":\\\"1409529600\\\",\\\"description\\\":\\\"子时间区间的开始时间点。UNIX时间戳格式,表示从1970-1-1 00:00:00 UTC计算起的秒数。\\\"},{\\\"name\\\":\\\"to\\\",\\\"position\\\":\\\"Query\\\",\\\"required\\\":true,\\\"checkBlank\\\":false,\\\"visibility\\\":\\\"Public\\\",\\\"deprecated\\\":false,\\\"type\\\":\\\"Long\\\",\\\"title\\\":\\\"查询结束时间点。UNIX时间戳格式,表示从1970-1-1 00:00:00 UTC计算起的秒数。\\\\n\\\\n时间区间遵循“左闭右开”原则,即该时间区间包括区间开始时间点,但不包括区间结束时间点。如果from和to的值相同,则为无效区间,函数直接返回错误。\\\",\\\"example\\\":\\\"1409569200\\\",\\\"description\\\":\\\"子时间区间的结束时间点。UNIX时间戳格式,表示从1970-1-1 00:00:00 UTC计算起的秒数。\\\"},{\\\"name\\\":\\\"topic\\\",\\\"position\\\":\\\"Query\\\",\\\"required\\\":false,\\\"checkBlank\\\":false,\\\"visibility\\\":\\\"Public\\\",\\\"deprecated\\\":false,\\\"type\\\":\\\"String\\\",\\\"title\\\":\\\"日志主题。\\\",\\\"example\\\":\\\"topic\\\",\\\"description\\\":\\\"日志主题。\\\"},{\\\"name\\\":\\\"query\\\",\\\"position\\\":\\\"Query\\\",\\\"required\\\":false,\\\"checkBlank\\\":false,\\\"visibility\\\":\\\"Public\\\",\\\"deprecated\\\":false,\\\"type\\\":\\\"String\\\",\\\"title\\\":\\\"查询语句。仅支持查询语句,不支持分析语句。关于查询语句的详细语法,请参见查询语法。\\\",\\\"example\\\":\\\"with_pack_meta\\\",\\\"description\\\":\\\"查询语句。仅支持查询语句,不支持分析语句。关于查询语句的详细语法,请参见[查询语法](~~43772~~)。\\\"}]\",\"protocol\":\"HTTP|HTTPS\"},\"params\":{\"to\":" + to + ",\"from\":" + from + ",\"logstore\":\"" + logstore + "\",\"project\":\"hd-live\",\"query\":\"" + query + "\"},\"credential\":{\"type\":\"ak\"}}",
"method": "POST",
"mode": "cors",
"credentials": "include"
});
if (resp.status !== 200) {
alert("请求失败,请重试");
throw Error(`请求失败 logstore=${logstore}, from=${from}, to=${to}, query=${query}\nresponse=>${JSON.stringify(resp)}`);
}
let data = await resp.json();
if (data.code === "E_NEED_LOGIN") {
// 未登录(不可用)
alert("请先登录(不可用)后重试\nhttps://next.api.aliyun.com/api/Sls/2020-12-30/GetHistograms");
throw Error(`未登录(不可用)`);
}
if (data.code !== 0) {
alert("请求失败,未知异常,请重试");
throw Error(`请求失败 logstore=${logstore}, from=${from}, to=${to}, query=${query}\nresponse=>${JSON.stringify(resp)}`);
}
data = data.data
// 计数
let count = 0;
for (let item of data.result) {
count += item.count;
}
return count;
}
/**
* html 字符串转换为 Node 对象
* @param {string} html html 代码段
* @returns Node 转换出的 Node 元素
*/
function parseDom(html) {
let template = document.createElement('template');
template.innerHTML = html;
return template.content.childNodes[0];
}
function closeResult() {
if (state.eleBox) {
state.eleBox.remove();
state.eleBox = undefined;
}
}
unsafeWindow.closeResult = closeResult;
function showResult(data) {
console.log("result", data);
let eleBox = parseDom(`<div id="_sls_result_box_" style="padding: 12px; background: white; border-radius: 16px; position: fixed; z-index: 999; top: 0; bottom:0; left: 0; right: 0; margin: auto auto; width: fit-content; height: fit-content; box-shadow: rgba(0, 0, 0, 0.3) 0 19px 38px, rgba(0, 0, 0, 0.22) 0 15px 12px; display: flex; flex-direction: column; align-items: center;"></div>`)
let eleTable = parseDom(`<table style="background: white; border: 1px solid #818181; border-collapse: collapse;"></table>`);
let eleThead = document.createElement('thead');
let eleTbody = document.createElement('tbody');
eleTable.appendChild(eleThead);
eleTable.appendChild(eleTbody);
eleBox.appendChild(eleTable);
let eleButton = parseDom(`<button onclick="window.closeResult()" style="background: #00adff; margin-top: 16px; border: 0; border-radius: 8px; padding: 4px 24px; color: white;">关闭</button>`)
eleBox.appendChild(eleButton);
// 表头
let eleTr = document.createElement('tr');
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>查询接口(已排除登录(不可用)校验和用户禁用)</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>日期</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>全天请求次数</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>失败总数</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>成功率</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>响应时间</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>晚高峰请求次数</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>晚高峰失败次数</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>晚高峰成功率</th>"));
eleTr.append(parseDom("<th style='padding: 8px 16px; border: 1px solid #818181'>晚高峰QPS</th>"));
eleThead.appendChild(eleTr);
// 表内容
for (let item of data) {
eleTr = document.createElement('tr');
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${item.api}</td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${item.date}</td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${item.dayAccessCount}</td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${item.dayErrorCount}</td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${parseFloat(Math.floor(item.daySuccessRatio * 100) / 100)}%</td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'></td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${item.peakAccessCount}</td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${item.peakErrorCount}</td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${parseFloat(Math.floor(item.peakSuccessRatio * 100) / 100)}%</td>`));
eleTr.append(parseDom(`<td style='padding: 8px 16px; border: 1px solid #818181'>${parseFloat(Math.floor(item.peakQps * 10000) / 10000)}</td`));
eleTbody.appendChild(eleTr);
}
document.body.appendChild(eleBox);
state.eleBox = eleBox;
GM_notification({
title: "有咖互动接口数据统计",
text: "统计完成",
timeout: 3000,
});
}
// 脚本主体
GM_registerMenuCommand("执行", async () => {
if (state.running) {
return;
}
try {
state.running = true;
closeResult();
// 输入查询参数
state.inputQuery = input(`请输入查询接口(英文逗号分隔,如 "/dynamic/lottery/basic/info, /dynamic/lottery/detail")`, state.inputQuery, /^.+(,.+)*$/);
state.inputDate = input(`请输入查询日期(前一天为 1,前第二天为 2,以此类推,英文逗号分隔,如 "1,2" 查询前两天)`, state.inputDate, /^[1-9]\d{0,6}(,[1-9]\d{0,6})*$/);
// 解析参数
let apiList = state.inputQuery.split(",");
for (let i in apiList) {
apiList[i] = apiList[i].trim();
}
let dateList = state.inputDate.split(",");
for (let i in dateList) {
dateList[i] = parseInt(dateList[i].trim());
}
GM_notification({
title: "有咖互动接口数据统计",
text: "执行统计中,请勿关闭窗口,请稍等...",
timeout: 3000,
});
// 获取今天的开始时间戳(秒)
const now = new Date();
const todayStartTimestamp = new Date(`${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} 00:00:00`).getTime() / 1000;
let data = [];
for (let api of apiList) {
for (let date of dateList) {
let startTimestamp = todayStartTimestamp - date * 24 * 3600;
let startTime = new Date(startTimestamp * 1000);
let endTimestamp = todayStartTimestamp - (date - 1) * 24 * 3600;
// 全天请求次数
let dayAccessCount = await request(config.logstore.access, startTimestamp, endTimestamp, `#\\"${api} remote\\"`);
// 失败总数
let dayErrorCount = await request(config.logstore.error, startTimestamp, endTimestamp, `#\\"| requestURI:${api} |\\" not 用户鉴权失败 not 用户已被禁用`);
// 成功率
let daySuccessRatio = dayAccessCount === 0 ? 100 : ((1 - dayErrorCount / dayAccessCount) * 100);
// 晚高峰请求次数 20:00-24:00
let peakAccessCount = await request(config.logstore.access, startTimestamp - 4 * 3600, endTimestamp, `#\\"${api} remote\\"`);
// 晚高峰失败次数 20:00-24:00
let peakErrorCount = await request(config.logstore.error, startTimestamp - 4 * 3600, endTimestamp, `#\\"| requestURI:${api} |\\" not 用户鉴权失败 not 用户已被禁用`);
// 晚高峰成功率
let peakSuccessRatio = peakAccessCount === 0 ? 100 : ((1 - (peakErrorCount / peakAccessCount)) * 100);
// 晚高峰QPS
let peakQps = peakAccessCount / (4 * 3600);
data.push({
date: `${startTime.getFullYear()}-${startTime.getMonth() + 1}-${startTime.getDate()}`,
api,
dayAccessCount,
dayErrorCount,
daySuccessRatio,
peakAccessCount,
peakErrorCount,
peakSuccessRatio,
peakQps
});
}
}
showResult(data);
} finally {
state.running = false;
}
}, "Random");