Skip to content

About

极简站点访问统计(OpenResty Lua):请求/流量/独立IP/热门端点/来源/UA,内存聚合 + 快照持久化

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenLiteStats

运行在 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 约束。

TypeScript SDK

@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.ts
  • dist/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" 可关闭自动上报。

Vue 3 CSR / SSG

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 build

Lua 测试 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 裁剪,或后续接入符号化服务。

About

极简站点访问统计(OpenResty Lua):请求/流量/独立IP/热门端点/来源/UA,内存聚合 + 快照持久化

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages