运行在 nginx 容器内的站点访问统计(OpenResty Lua),同时提供浏览器 SDK,用于统计部署在 CDN 的前端界面。数据分两类:
- 边缘请求:由 nginx
log_by_lua_file记录通过 OpenLiteWaf 的请求; - 前端事件:由 TypeScript SDK 上报页面浏览、Web Vitals 与未捕获错误。
控制台把两者合并为“总活动量”,并保留来源拆分。许可证:MIT。
OpenLiteStats/
├── lua/
│ ├── openlitestats.lua # 核心:记录、聚合、快照、页面输出、采集处理
│ ├── log.lua # log_by_lua_file 入口(边缘记录)
│ ├── view.lua # /stats 与 /stats/data 入口
│ ├── collect.lua # /stats/collect 入口(前端事件)
│ ├── view.html # 控制台模板(CSS/JS 占位符由 Lua 注入)
│ ├── view.css
│ └── view.js
├── sdk/ # TypeScript SDK(Vue 3 CSR/SSG、Web Vitals、错误)
└── tests/ # Lua 回归测试(stub ngx,lua5.1 运行)
记录发生在 log 阶段(响应已发送,bytes_sent / request_time 可用)。以下请求不计入:
- 被 OpenLiteWaf 拦截的请求(WAF deny 时设置
ngx.ctx.olw_blocked,拦截数据由/security页面统计); - CORS 预检(
OPTIONS); /security、/stats前缀(两套统计页与采集端点自身)。
排除按路径段边界判定(/security/stats 排除,/securityXYZ 不排除);空前缀会被 starts_with 拒绝,避免误把全站统计归零。
边缘指标包括请求数、出站字节数(bytes_sent,压缩后)、2xx/3xx/4xx/5xx、请求方法、延迟均值与 P50/P95/P99、机器人/人类分类、独立 IP 近似值。状态码、方法、延迟、机器人按日存储;热门端点按日聚合并设置容量上限,超出后进入“其他”。
SDK 上报三类事件:
pageview:路径、来源 host、设备、语言、UTM source;vital:LCP / CLS / INP / TTFB;error:错误名、消息、截断堆栈、路由。
服务端按日聚合 PV、独立访客、路由、来源、设备、语言、UTM、Vitals 直方图与错误分组。同一 (site, vid, path) 在 2 秒内只计一次,用于吸收 SSG hydration、SPA 重复与重试。
- 总活动量 = 边缘请求 + 前端 PV;控制台同时展示两边占比。
- UV 不相加:边缘 UV 是按 IP 的位图估算,前端 UV 是按匿名 visitor ID 的位图估算,同一人可能同时出现在两边。控制台分别展示“前端访客”和“边缘独立 IP”,不伪造一个去重后的综合 UV。
- 前端访客 ID 为随机值(localStorage,30 天轮换),IP 只用于边缘统计且始终脱敏;服务端不保存原始访客 ID。
- 独立 IP / 访客均为近似值,高基数下存在碰撞误差。
- IP 一律脱敏(IPv4 保留前两段,IPv6 保留前三组);
- Referer 只保留 host,并剥离
user:pass@凭据; - URI / 页面路径不保留 query 与 hash;
- 不采集表单内容,不做浏览器指纹,不写第三方 Cookie;
- SDK 默认尊重 DNT/GPC,提供
optOut()/optIn()与beforeSend钩子。
GET /stats:控制台 HTML,内联脚本每 30 秒轮询;支持今日 / 7 天 / 30 天、深色模式、活动来源筛选。GET /stats/data:JSON 汇总。POST /stats/collect:前端事件采集,支持单事件、事件数组或{ "events": [...] },单请求上限 16KB / 20 条。GET /stats/sdk/<version>/openlitestats.min.js:版本化 SDK 文件(由 nginx 静态目录提供)。
/stats/data 的核心字段:
| 字段 | 说明 |
|---|---|
ranges.today / ranges["7d"] / ranges["30d"] |
每个范围的总量、边缘、前端、状态码、方法、延迟、Top 列表、Vitals、错误 |
today / alltime / yesterday |
兼容字段与累计值 |
rates |
上一分钟、本分钟、近 5 分钟均值 |
hours / today_hours / days / minutes |
趋势序列 |
activity |
边缘请求与前端浏览合并后的最近活动 |
recent / recent_web |
两类原始环形缓冲(调试用) |
snapshot |
快照时间、年龄、schema、最近一次写盘结果 |
web |
前端数据总览(是否启用、累计 PV、今日 UV) |
ranges 内每个 Top 条目为 { k, n, share };other 以单独字段返回(如 endpoint_other、route_other)。
POST /stats/collect
Content-Type: text/plain;charset=UTF-8
{"v":1,"events":[
{"type":"pageview","site":"docs","vid":"...","sid":"...","path":"/guide/install","dev":"desktop","lang":"zh-CN","utm":{"source":"search"}},
{"type":"vital","site":"docs","vid":"...","sid":"...","metric":"LCP","value":1800,"path":"/guide/install"},
{"type":"error","site":"docs","vid":"...","sid":"...","name":"TypeError","message":"cannot read x","stack":"...","path":"/guide/install"}
]}返回 204;非法 JSON 返回 400,超过体积上限返回 413。OPTIONS 预检返回 204。CORS 允许任意 Origin(不带凭据),生产环境仍受 WAF CC 与 nginx limit_req 约束。
@openlitestats/sdk 位于 sdk/,除 Vue 本身外无额外运行时依赖(Web Vitals 使用官方 web-vitals 并打包进产物)。包名当前为仓库内包;发布 npm 前从 sdk/ 构建,或直接使用源站 / CDN 文件。
cd sdk
pnpm install
pnpm typecheck
pnpm test
pnpm build构建产物:
dist/index.js/dist/index.cjs/dist/index.d.tsdist/openlitestats.min.js(IIFE,暴露window.OpenLiteStats)dist/<version>/openlitestats.min.js与integrity.json(供源站 / CDN 使用)
<script
src="https://api.example.com/stats/sdk/1.3.0/openlitestats.min.js"
data-site="docs"
data-endpoint="https://api.example.com/stats/collect"></script>脚本默认上报首屏并监听 History API / popstate。data-auto="false" 可关闭自动上报。
import { createApp } from "vue";
import { OpenLiteStatsPlugin, useOpenLiteStats } from "@openlitestats/sdk";
const app = createApp(App);
app.use(OpenLiteStatsPlugin, {
endpoint: "https://api.example.com/stats/collect",
site: "docs",
router,
});插件在浏览器端运行;router.isReady() 后上报首屏,afterEach 跟踪后续路由,首屏与 hydration 由 SDK 去重。SSR 导入时返回 no-op 实例,不会访问 window。
也可以手动创建:
import { createOpenLiteStats } from "@openlitestats/sdk";
const stats = createOpenLiteStats({
endpoint: "/stats/collect",
site: "docs",
autoPageview: true,
autoHistory: true,
});trackVital() / trackError() 可用于手动补充;beforeSend 可返回 null 丢弃事件。SDK 维护批量队列(默认 5 秒、20 条/批、12KB 载荷上限、队列上限 60),页面隐藏与 pagehide 时优先 sendBeacon。
| 配置 | 默认值 | 说明 |
|---|---|---|
dict_name |
openlitestats |
须与 lua_shared_dict 名一致 |
view_prefix / data_prefix |
/stats、/stats/data |
统计页与 JSON URI |
collect_prefix |
/stats/collect |
前端采集端点 |
exclude_prefixes |
/security、/stats |
不计入边缘统计的前缀(按路径段边界) |
data_dir / snapshot_interval |
/data/openlitestats / 60 |
快照目录 / 写盘间隔 |
ring_capacity / web_ring_capacity |
1000 / 300 | 边缘 / 前端环形缓冲 |
top_n / recent_n |
8 / 20 | 控制台 Top 与最近活动数 |
bm_bits / web_bm_bits |
1048576 / 65536 | 边缘今日 UV / 前端每日 UV 位图 |
hours / days / minutes |
24 / 30 / 60 | 趋势窗口 |
endpoint_capacity |
128 | 每日端点聚合上限,超出进 other |
web_route_capacity / web_ref_capacity |
128 / 64 | 前端路由 / 来源上限 |
web_utm_capacity / web_error_capacity |
32 / 100 | UTM / 错误分组上限 |
collect_max_events / collect_max_body |
20 / 16384 | 采集批量与体积上限 |
worker 0 每 60 秒把 shared dict 写为 data/snapshot.json(临时文件 + rename 原子替换),init_by_lua 恢复。快照 schema 2 使用 kv(普通键)与 bin(base64 位图分片)保存全部受管键,并保留 a_req / a_bytes / seq / ring / hours / bm 等 v1 兼容字段。
- 日键(
d:/e:/w:)TTL 40 天;小时桶 48 小时;分钟桶 2 小时; - 边缘今日 UV 位图 TTL 2 天,前端每日 UV 位图 TTL 40 天;
- 目录不可写时退化为纯内存模式,只影响持久化,不影响统计;
- 旧 v1 快照可恢复累计值与当日计数;旧 64K 位图与新 1M 位不兼容,升级后当日 UV 从零开始。
与 OpenLiteWaf 同容器运行。OpenLiteWaf 的主配置负责 package path 与 shared dict;LogShare 主仓的 compose 挂载 lua/、sdk/dist/ 与 data/,并在 nginx 中提供 /stats、/stats/data、/stats/collect 与 /stats/sdk/。
/stats/collect 的请求体在 OpenLiteWaf 中按精确 URI 豁免特征扫描(仍检查 URI/UA,仍受 CC 限流);不要把豁免改成前缀,否则相似路径会绕过 body 检查。
改动 Lua 或静态资源后重启 nginx 容器(本部署 nginx -s reload 不可靠);改 SDK 后重新 pnpm build 并部署 sdk/dist。
# 在本仓库根
luac5.1 -p lua/*.lua
lua5.1 tests/openlitestats_logic_test.lua
# 在主仓根
luac5.1 -p OpenLiteStats/lua/*.lua
lua5.1 OpenLiteStats/tests/openlitestats_logic_test.lua
# SDK
cd OpenLiteStats/sdk && pnpm typecheck && pnpm test && pnpm buildLua 测试 stub ngx 与 cjson.safe,可离线验证计数、口径、快照、采集与页面输出;真实 nginx/WAF 行为由主仓 Docker E2E 覆盖。
- 统计页为公开端点,受 WAF CC 限流保护;如需限制访问,可在 nginx 层加 auth_basic 或 allow/deny。
- 若 nginx 前有 CDN,边缘
remote_addr是节点 IP;前端访客不受影响,因为 SDK 使用匿名 visitor ID。需要边缘真实 IP 时先配置 real_ip。 - 精确 UV 需要外部存储或保存全量访客集合;当前使用位图近似。若业务要求精确去重,应引入 Redis/独立分析库,而不是扩大 shared dict。
- 错误堆栈未做 source map 符号化;生产环境可在 SDK 侧通过
beforeSend裁剪,或后续接入符号化服务。