静态站点埋点日志本地聚合与自助分析脚本实践
【摘要】 背景痛点静态站点的埋点数据一般是离散 JSON 行日志,直接存储在对象存储或者简单 Serverless 日志库中,原生存在几个问题:原始日志混杂心跳、异常、页面访问、自定义事件,格式不统一,脏数据多(上报超时、浏览器非法参数、爬虫扫描请求);原始数据无法直接看大盘:UV、PV、LCP 分位、页面停留时长、错误分布;引入专业日志系统太重,对于校园导览这类小流量静态站点,维护成本高;日志没有...
背景痛点
静态站点的埋点数据一般是离散 JSON 行日志,直接存储在对象存储或者简单 Serverless 日志库中,原生存在几个问题:
- 原始日志混杂心跳、异常、页面访问、自定义事件,格式不统一,脏数据多(上报超时、浏览器非法参数、爬虫扫描请求);
- 原始数据无法直接看大盘:UV、PV、LCP 分位、页面停留时长、错误分布;
- 引入专业日志系统太重,对于校园导览这类小流量静态站点,维护成本高;
- 日志没有脱敏,UA、IP 原始信息直接存储,存在合规风险。
我们的目标:构建一个本地/CI 可定时执行的日志聚合脚本,输入原始埋点日志,输出结构化指标报表(JSON + Markdown 大盘)。
整体设计思路
整体流水线分为 4 个阶段:
日志拉取 -> 清洗过滤 & 脱敏 -> 指标聚合计算 -> 输出报表 + 持久化快照
- 过滤层:过滤爬虫、无效心跳、非法格式、测试标记的埋点;
- 脱敏层:IP 掩码、UA 只保留设备/浏览器大类,不存储完整识别信息;
- 聚合层:按页面路径、日期、设备维度,统计 PV/UV、性能指标分位数、JS 错误 TOP;
- 输出层:机器可读 JSON 指标,同时生成可读的 Markdown 报表,可直接放进仓库归档。
核心实现:Node 日志分析脚本
// log-analyzer.js
const fs = require('fs');
const path = require('path');
const RAW_LOG_PATH = './logs/raw.log';
const OUTPUT_JSON = './logs/report.json';
const OUTPUT_MD = './logs/report.md';
// 简单脱敏:IP掩码
function maskIp(ip) {
if (!ip || ip === '-') return 'unknown';
const parts = ip.split('.');
if (parts.length === 4) return `${parts[0]}.${parts[1]}.*.*`;
return ip;
}
// 简易UV标识(业务侧使用匿名sessionId,不依赖真实用户标识)
function isValidRecord(item) {
if (!item || typeof item !== 'object') return false;
// 过滤爬虫、心跳ping、非法上报
const isPing = item.event_type === 'ping';
const isBot = item.ua?.toLowerCase().includes('bot') || item.ua?.toLowerCase().includes('spider');
const hasRequired = !!item.sessionId && !!item.page_path && !!item.timestamp;
return !isPing && !isBot && hasRequired;
}
function calcQuantile(arr, q) {
if (arr.length === 0) return null;
const sorted = [...arr].sort((a, b) => a - b);
const idx = Math.floor(sorted.length * q);
return sorted[idx];
}
function main() {
if (!fs.existsSync(RAW_LOG_PATH)) {
console.log('原始日志文件不存在');
return;
}
const rawText = fs.readFileSync(RAW_LOG_PATH, 'utf-8');
const lines = rawText.split('\n').filter(Boolean);
const records = [];
const errorMap = new Map();
const lcpList = [];
const pageStat = new Map();
const sessionSet = new Set();
for (const line of lines) {
try {
const item = JSON.parse(line);
if (!isValidRecord(item)) continue;
item.ip = maskIp(item.ip);
records.push(item);
sessionSet.add(item.sessionId);
// 页面维度统计
if (!pageStat.has(item.page_path)) {
pageStat.set(item.page_path, { pv: 0, uvSet: new Set() });
}
const p = pageStat.get(item.page_path);
p.pv += 1;
p.uvSet.add(item.sessionId);
// LCP性能指标收集
if (item.event_type === 'performance' && item.lcp) {
lcpList.push(Number(item.lcp));
}
// JS错误统计
if (item.event_type === 'error' && item.msg) {
errorMap.set(item.msg, (errorMap.get(item.msg) || 0) + 1);
}
} catch (e) {
// 忽略JSON解析失败脏日志
continue;
}
}
// 汇总指标
const totalPV = records.length;
const totalUV = sessionSet.size;
const lcpP50 = calcQuantile(lcpList, 0.5);
const lcpP90 = calcQuantile(lcpList, 0.9);
const lcpP95 = calcQuantile(lcpList, 0.95);
// 页面维度转为普通对象
const pageResult = {};
for (const [k, v] of pageStat.entries()) {
pageResult[k] = { pv: v.pv, uv: v.uvSet.size };
}
// 错误TOP10
const sortedError = Array.from(errorMap.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
const report = {
meta: { generateAt: new Date().toISOString() },
summary: { totalPV, totalUV, lcpP50, lcpP90, lcpP95 },
page: pageResult,
topError: sortedError
};
// 输出JSON
fs.writeFileSync(OUTPUT_JSON, JSON.stringify(report, null, 2), 'utf-8');
// 输出Markdown报表
let md = `# 站点埋点自助分析报表\n\n生成时间:${report.meta.generateAt}\n\n## 大盘总览\n\n| 指标 | 值 |\n| ---- | ---- |\n| PV | ${totalPV} |\n| UV | ${totalUV} |\n| LCP P50(ms) | ${lcpP50 ?? '-' } |\n| LCP P90(ms) | ${lcpP90 ?? '-' } |\n| LCP P95(ms) | ${lcpP95 ?? '-' } |\n\n## 页面访问分布\n\n| 页面路径 | PV | UV |\n| ---- | ---- | ---- |\n`;
for (const [path, stat] of Object.entries(pageResult)) {
md += `| ${path} | ${stat.pv} | ${stat.uv} |\n`;
}
md += `\n## TOP 前端错误\n\n| 错误信息 | 次数 |\n| ---- | ---- |\n`;
for (const [msg, cnt] of sortedError) {
md += `| ${msg} | ${cnt} |\n`;
}
fs.writeFileSync(OUTPUT_MD, md, 'utf-8');
console.log('报表生成完成', OUTPUT_JSON, OUTPUT_MD);
}
main();
配套 CI 定时执行方案
在之前的 GitCode CI 流水线里新增定时任务,比如每 6 小时拉取一次远端日志、执行分析脚本,产出报表并提交回仓库:
- 拉取日志:从对象存储 / Serverless 日志接口追加下载原始日志,避免重复计算;
- 执行脚本:
node log-analyzer.js; - 产物归档:把 report.md、report.json 提交到仓库 docs/report 目录,形成可追溯的历史大盘快照。
工程权衡与边界说明
- UV 精度取舍:这里依赖前端生成的匿名 sessionId,不是严格真实用户,适合小站点趋势观察,不适合高精度用户统计;
- 数据容量上限:纯本地脚本适合十万行以内日志,数据量暴涨之后需要换成流式处理;
- 合规边界:全程不收集手机号、实名信息,IP 做掩码,符合静态站点轻量埋点的隐私要求;
- 告警能力缺失:脚本只做统计,不自动告警;扩展方向:增加阈值判断(比如 LCP P95 > 3000ms、错误突增),对接 Webhook 推送通知。
后续演进方向
- 增加按地域、设备类型的多维度下钻;
- 增加环比计算,自动对比昨日同时段流量;
- 接入简单图表渲染,直接输出带 mermaid 图表的 Markdown 报表。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)