diff --git a/README.md b/README.md index a4a3448..f8d99c7 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ 4. 在 **环境变量 (Environment Variables)** 中添加以下配置: - `SECRET_ID`: 您的腾讯云 SecretId - `SECRET_KEY`: 您的腾讯云 SecretKey - - `SITE_NAME`: (可选) 自定义大屏标题,默认为 "AcoFork 的 EdgeOne 监控大屏" - - `SITE_ICON`: (可选) 自定义网页图标,默认为 "https://q2.qlogo.cn/headimg_dl?dst_uin=2726730791&spec=0" + - `SITE_NAME`: 大屏标题,固定为 "茶茶吖 的 EdgeOne 监控大屏" + - `SITE_ICON`: 网页图标,固定为 "https://blog.ccya.top/logo.svg" 5. 部署项目。 ### 方式二:本地运行 / Node.js 环境 diff --git a/index.html b/index.html index 4b7bd10..e8eaad9 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + - AcoFork 的 EdgeOne 监控大屏 + 茶茶吖 的 EdgeOne 监控大屏 @@ -116,7 +116,7 @@
-

AcoFork 的 EdgeOne 监控大屏

+

茶茶吖 的 EdgeOne 监控大屏

EdgeOne 站点流量与请求量分析

@@ -171,10 +171,16 @@

Ac
-
+
+ + +
@@ -1203,6 +1209,8 @@

User Agent 请求数排行 const interval = document.getElementById('interval').value; const zoneId = document.getElementById('zoneId').value.trim(); + const domainSelect = document.getElementById('domainSelect'); + const domain = domainSelect ? domainSelect.value.trim() : '*'; let url = `/api/traffic?metric=${metric}&startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`; if (interval && interval !== 'auto') { @@ -1211,6 +1219,9 @@

User Agent 请求数排行 if (zoneId) { url += `&zoneId=${encodeURIComponent(zoneId)}`; } + if (zoneId && zoneId !== '*' && domain && domain !== '*') { + url += `&domain=${encodeURIComponent(domain)}`; + } const response = await fetch(url); const result = await response.json(); @@ -1399,6 +1410,7 @@

User Agent 请求数排行 select.value = "*"; } + await fetchDomains(select.value); } catch (err) { console.error("Error fetching zones:", err); const select = document.getElementById('zoneId'); @@ -1406,9 +1418,49 @@

User Agent 请求数排行 if (select.options.length === 0 || select.options[0].value !== '*') { select.innerHTML = ''; } + await fetchDomains("*"); } } + async function fetchDomains(zoneId) { + const select = document.getElementById('domainSelect'); + if (!select) return; + + if (!zoneId || zoneId === '*') { + select.innerHTML = ''; + select.disabled = true; + return; + } + + try { + select.disabled = false; + select.innerHTML = ''; + + const response = await fetch(`/api/domains?zoneId=${encodeURIComponent(zoneId)}`); + const result = await response.json(); + + select.innerHTML = ''; + if (result.AccelerationDomains && result.AccelerationDomains.length > 0) { + result.AccelerationDomains.forEach(domain => { + const option = document.createElement('option'); + option.value = domain.DomainName; + option.text = domain.DomainName; + select.appendChild(option); + }); + } + } catch (err) { + console.error("Error fetching domains:", err); + select.innerHTML = ''; + select.disabled = true; + } + } + + async function handleZoneChange() { + const zoneId = document.getElementById('zoneId').value.trim(); + await fetchDomains(zoneId); + refreshData(); + } + ////ENTRANCE//// async function initDashboard() { // Initialize charts @@ -2348,35 +2400,29 @@

User Agent 请求数排行 } //通用处理 - const renderTopChart = (results, chartInstance, metricName, mapObject = null) => { + const renderTopChart = (results, chartInstance, metricName, mapObject = null, config = {}) => { + const { keyFormatter, tooltipValueFormatter, axisLabelFormatter } = config; const data = results[metricName]; if (!data || data.type !== 'top' || !data.data) return; const sortedData = [...data.data].sort((a, b) => b.Value - a.Value).slice(0, 10); - - let unit = ''; - let divisor = 1; - let label = metricLabels[metricName] || ''; - - if (metricName.includes('outFlux')) { - const maxVal = sortedData.length > 0 ? sortedData[0].Value : 0; - const best = getBestUnit(maxVal, 'bytes'); - unit = best.unit; - divisor = best.divisor; - } else { - const maxVal = sortedData.length > 0 ? sortedData[0].Value : 0; - const best = getBestCountUnit(maxVal); - unit = best.unit; - divisor = best.divisor; - } - const yAxisData = sortedData.map(item => mapObject?.[item.Key] || item.Key).reverse(); //问题点:没对“字段不存在”的短横杠做判断 - const seriesData = sortedData.map(item => { - const val = item.Value / divisor; - return (divisor === 1) ? val : val.toFixed(2); + const unitInfo = metricName.includes('outFlux') + ? getBestUnit(sortedData[0]?.Value || 0, 'bytes') + : getBestCountUnit(sortedData[0]?.Value || 0); + + const yAxisData = sortedData.map(item => { + const mapped = mapObject?.[item.Key] || item.Key; + return keyFormatter ? keyFormatter(mapped) : mapped; }).reverse(); + const rawSeriesData = sortedData.map(item => item.Value).reverse(); + const seriesData = rawSeriesData.map(val => { + const converted = val / unitInfo.divisor; + return unitInfo.divisor === 1 ? converted : converted.toFixed(2); + }); + let color = '#3b82f6'; const lowerName = metricName.toLowerCase(); if (lowerName.includes('country')) color = '#3b82f6'; @@ -2391,41 +2437,32 @@

User Agent 请求数排行 trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: (params) => { - const param = params[0]; - let name = param.name; - if (name.length > 100) name = name.substring(0, 100) + '...'; - - const rawVal = sortedData[sortedData.length - 1 - param.dataIndex].Value; - - let formattedVal = ''; - if (metricName.includes('outFlux')) { - formattedVal = formatBytes(rawVal); - } else { - const fVal = formatCount(rawVal); - // Append '次' if not present in unit (formatCount returns unit, but we might want explicit '次') - // formatCount returns "1.23 万". "1.23 万次" sounds good. - // "500" -> "500 次". - formattedVal = fVal + (fVal.includes('千') || fVal.includes('万') || fVal.includes('亿') ? '次' : ' 次'); - } - - return `${name}
${param.marker}${label}: ${formattedVal}`; + const param = params[0]; + const rawVal = rawSeriesData[param.dataIndex]; + let formattedVal; + if (tooltipValueFormatter) { + formattedVal = tooltipValueFormatter(rawVal, metricName); + } else if (metricName.includes('outFlux')) { + formattedVal = formatBytes(rawVal); + } else { + const fVal = formatCount(rawVal); + formattedVal = fVal + (fVal.includes('千') || fVal.includes('万') || fVal.includes('亿') ? '次' : ' 次'); + } + return `${param.name}
${param.marker}${metricLabels[metricName] || ''}: ${formattedVal}`; } }, grid: { left: '3%', right: '10%', bottom: '3%', containLabel: true }, - xAxis: { type: 'value', name: unit }, - yAxis: { - type: 'category', + xAxis: { type: 'value', name: unitInfo.unit }, + yAxis: { + type: 'category', data: yAxisData, axisLabel: { - formatter: function (value) { - if (value.length > 20) return value.substring(0, 20) + '...'; - return value; - } + formatter: axisLabelFormatter || ((value) => value.length > 20 ? value.substring(0, 20) + '...' : value) } }, series: [ { - name: label, + name: metricLabels[metricName] || '', type: 'bar', data: seriesData, itemStyle: { color: color }, @@ -2497,35 +2534,54 @@

User Agent 请求数排行 function updateTopAnalysisSection(results) { updateTopMapChart(results); + // Flux Charts + const truncate20 = (val) => val.length > 20 ? val.substring(0, 20) + '...' : val; + const truncate50 = (val) => val.length > 50 ? val.substring(0, 50) + '...' : val; + const refererFormatter = (key) => { + const cleaned = (key || '').replace(/`/g, '').trim(); + return cleaned && cleaned !== '-' ? cleaned : '字段不存在'; + }; + const refererAxisFormatter = (val) => truncate50(refererFormatter(val)); + const refererTooltipFormatter = (raw) => { + const formatted = formatCount(raw); + return formatted + (formatted.includes('千') || formatted.includes('万') || formatted.includes('亿') ? '次' : ' 次'); + }; + // Flux Charts renderTopChart(results, chartTopCountry, 'l7Flow_outFlux_country', countryMap); renderTopChart(results, chartTopProvince, 'l7Flow_outFlux_province', provinceMap); - renderTopChart(results, chartTopStatusCode, 'l7Flow_outFlux_statusCode'); - renderTopChart(results, chartTopDomain, 'l7Flow_outFlux_domain'); - renderTopChart(results, chartTopUrl, 'l7Flow_outFlux_url'); + renderTopChart(results, chartTopStatusCode, 'l7Flow_outFlux_statusCode', null, { axisLabelFormatter: truncate20 }); + renderTopChart(results, chartTopDomain, 'l7Flow_outFlux_domain', null, { axisLabelFormatter: truncate50 }); + renderTopChart(results, chartTopUrl, 'l7Flow_outFlux_url', null, { axisLabelFormatter: truncate50 }); renderTopChart(results, chartTopResourceType, 'l7Flow_outFlux_resourceType'); renderTopChart(results, chartTopSip, 'l7Flow_outFlux_sip'); - //renderTopChart(results, chartTopReferer, 'l7Flow_outFlux_referers'); //以前方案 - updateTopRefererChart(results); //降级处理,包含“字段不存在”处理 + renderTopChart(results, chartTopReferer, 'l7Flow_outFlux_referers', null, { + keyFormatter: refererFormatter, + axisLabelFormatter: refererAxisFormatter, + tooltipValueFormatter: (raw) => formatBytes(raw) + }); renderTopChart(results, chartTopUaDevice, 'l7Flow_outFlux_ua_device'); renderTopChart(results, chartTopUaBrowser, 'l7Flow_outFlux_ua_browser'); renderTopChart(results, chartTopUaOs, 'l7Flow_outFlux_ua_os'); - renderTopChart(results, chartTopUa, 'l7Flow_outFlux_ua'); + renderTopChart(results, chartTopUa, 'l7Flow_outFlux_ua', null, { axisLabelFormatter: truncate50 }); // Request Charts renderTopChart(results, chartTopRequestCountry, 'l7Flow_request_country', countryMap); renderTopChart(results, chartTopRequestProvince, 'l7Flow_request_province', provinceMap); - renderTopChart(results, chartTopRequestStatusCode, 'l7Flow_request_statusCode'); - renderTopChart(results, chartTopRequestDomain, 'l7Flow_request_domain'); - renderTopChart(results, chartTopRequestUrl, 'l7Flow_request_url'); + renderTopChart(results, chartTopRequestStatusCode, 'l7Flow_request_statusCode', null, { axisLabelFormatter: truncate20 }); + renderTopChart(results, chartTopRequestDomain, 'l7Flow_request_domain', null, { axisLabelFormatter: truncate50 }); + renderTopChart(results, chartTopRequestUrl, 'l7Flow_request_url', null, { axisLabelFormatter: truncate50 }); renderTopChart(results, chartTopRequestResourceType, 'l7Flow_request_resourceType'); renderTopChart(results, chartTopRequestSip, 'l7Flow_request_sip'); - //renderTopChart(results, chartTopRequestReferer, 'l7Flow_request_referers'); - updateTopRequestRefererChart(results); //降级方案 + renderTopChart(results, chartTopRequestReferer, 'l7Flow_request_referers', null, { + keyFormatter: refererFormatter, + axisLabelFormatter: refererAxisFormatter, + tooltipValueFormatter: refererTooltipFormatter + }); renderTopChart(results, chartTopRequestUaDevice, 'l7Flow_request_ua_device'); renderTopChart(results, chartTopRequestUaBrowser, 'l7Flow_request_ua_browser'); renderTopChart(results, chartTopRequestUaOs, 'l7Flow_request_ua_os'); - renderTopChart(results, chartTopRequestUa, 'l7Flow_request_ua'); + renderTopChart(results, chartTopRequestUa, 'l7Flow_request_ua', null, { axisLabelFormatter: truncate50 }); } diff --git a/node-functions/api/[[default]].js b/node-functions/api/[[default]].js index aeb4c9e..9ca7c60 100644 --- a/node-functions/api/[[default]].js +++ b/node-functions/api/[[default]].js @@ -96,10 +96,17 @@ const FUNCTION_METRICS = [ 'function_cpuCostTime' ]; +function buildDomainFilters(domain) { + if (!domain || domain === '*') { + return undefined; + } + return [{ Key: "domain", Operator: "equals", Value: [domain] }]; +} + app.get('/config', (req, res) => { res.json({ - siteName: process.env.SITE_NAME || 'AcoFork 的 EdgeOne 监控大屏', - siteIcon: process.env.SITE_ICON || 'https://q2.qlogo.cn/headimg_dl?dst_uin=2726730791&spec=0' + siteName: process.env.SITE_NAME || '茶茶吖 的 EdgeOne 监控大屏', + siteIcon: process.env.SITE_ICON || 'https://blog.ccya.top/logo.svg' }); }); @@ -137,6 +144,32 @@ app.get('/zones', async (req, res) => { } }); +// 新增:获取站点下的所有加速域名 +app.get('/domains', async (req, res) => { + try { + const { secretId, secretKey } = getKeys(); + const { zoneId } = req.query; + + if (!secretId || !secretKey) { + return res.status(500).json({ error: "Missing credentials" }); + } + + const TeoClient = teo.v20220901.Client; + const client = new TeoClient({ + credential: { secretId, secretKey }, + region: "ap-guangzhou", + profile: { httpProfile: { endpoint: "teo.tencentcloudapi.com" } } + }); + + const params = zoneId && zoneId !== '*' ? { ZoneId: zoneId } : {}; + const data = await client.DescribeAccelerationDomains(params); + res.json(data); + } catch (err) { + console.error("Error fetching domains:", err); + res.status(500).json({ error: err.message }); + } +}); + app.get('/pages/build-count', async (req, res) => { try { const { secretId, secretKey } = getKeys(); @@ -434,6 +467,7 @@ app.get('/traffic', async (req, res) => { const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); const metric = req.query.metric || "l7Flow_flux"; + const domain = req.query.domain; const startTime = req.query.startTime || formatDate(yesterday); const endTime = req.query.endTime || formatDate(now); const interval = req.query.interval; @@ -445,6 +479,8 @@ app.get('/traffic', async (req, res) => { console.log(`Requesting metric: ${metric}, StartTime: ${startTime}, EndTime: ${endTime}, Interval: ${interval}`); + const domainFilters = buildDomainFilters(domain); + if (TOP_ANALYSIS_METRICS.includes(metric)) { // API: DescribeTopL7AnalysisData params = { @@ -453,6 +489,9 @@ app.get('/traffic', async (req, res) => { "MetricName": metric, "ZoneIds": zoneIds }; + if (domainFilters) { + params["Filters"] = domainFilters; + } console.log("Calling DescribeTopL7AnalysisData with params:", JSON.stringify(params, null, 2)); data = await client.DescribeTopL7AnalysisData(params); } else if (SECURITY_METRICS.includes(metric)) { @@ -545,6 +584,9 @@ app.get('/traffic', async (req, res) => { if (interval && interval !== 'auto') { params["Interval"] = interval; } + if (domainFilters) { + params["Filters"] = domainFilters; + } console.log("Calling Timing API with params:", JSON.stringify(params, null, 2));