使用Nginx的访问日志统计PV与UV


Posted in Servers onMay 06, 2022

前言

一个网站当用户量增大时候,不可避免有统计pv和uv的需求。

  • UV(Unique Visitor):独立访客,以cookie为依据区分不同访客,UV计算一天之内(00:00-24:00),访问网站的访客数量。
  • PV(Page View):页面访问量,同一个用户对页面多次访问累计。

本文介绍一种通过分析nginx日志统计pv、uv的方法。

一、方案设计

如何根据Nginx的访问日志统计pv和uv呢?

我们可以通过分析nginx的访问网站页面的日志来统计参数,比如一个单页应用的博客网站,用户访问/、/article_list、/article_detail都应该算作一次访问。

但是如果网站的路由不确定时候,就不好统计。当路由变化时候,需要更新统计脚本。而且,用户首次访问后才设置了cookie,所以首次页面请求是不带cookie的,这会导致漏报。另外,用cookie记录数据,由于是js写的cookie,所以需要保证同域访问,这就很不灵活。如果不是js写的cookie,那就说明依赖后端服务,也不够灵活。

所以我们采取的方法是前端上报页面访问事件。

首先前端生成一个uuid,向Nginx发起一个请求并携带uuid,Nginx会精确匹配这个请求,然后返回204,以减小数据传输量。

由于上报地址和页面是同域的,因此我们这里使用cookie保存uuid,如果不同域,还可以使用localStorage将uuid存在本地,然后在参数中将uuid带上。

Nginx收到上报后,根据我们指定的固定格式生成日志。我们还要设置定时任务,定期切割日志,以便分析日志时候以月和天为维度统计指标。

整体流程示意图如下:

使用Nginx的访问日志统计PV与UV

二、上报访问事件

前端使用uuid这个库生成uuid,使用js-cookie对cookie进行读写,cookie有效期设置为30天,如果已经存在则不设置。

这里上报地址是“/report.gif”。为了避免上报请求被缓存,请求参数加一个时间戳。

// index.js
import Cookies from 'js-cookie';
import {v4 as uuidv4} from 'uuid';

try {
  if (!Cookies.get('uuid')) {
    Cookies.set('uuid', uuidv4().replace(/-/g, ''), {expires: 30});
  }
  // 上报访问
  axios.get(`https://www.example.com/report.gif?t=${Date.now()}`);
}
catch (e) {}

Nginx需要配置响应

location =/report.gif {
  return 204;
}

三、Nginx配置日志格式

我们可以指定Nginx访问日志的格式,分析日志时候更方便。

注意,log_format指令只能用在http模块中,不能用在server模块中。

这里在http模块中通过log_format定义了一个格式,命名为main,然后在server模块中使用access_log定义访问日志的存放目录,并且引用main指定日志格式。server模块中还匹配了请求里面的cookie,取出uuid赋值给$uuid变量以便写日志时候能够正常读取uuid。

http {
  log_format main '$remote_addr - [$time_local] "$request" '
    ' - $status "uuid:$uuid" ';
  server {
    access_log /path/to/log/access443.log main;
    if ( $http_cookie ~* "uuid=([A-Z0-9]*)"){
        set $uuid $1;
    }
  }
}

我们会得到这样的日志

101.241.91.99 - [04/May/2022:09:36:34 +0800] "GET /assets/vendor.337922eb.js HTTP/1.1"  - 304 "uuid:a27050e998864af89de0fbc7605d1548"
101.241.91.99 - [04/May/2022:09:36:34 +0800] "GET /assets/style.81f77c22.css HTTP/1.1"  - 200 "uuid:a27050e998864af89de0fbc7605d1548"
101.241.91.99 - [04/May/2022:09:36:34 +0800] "GET /assets/index.9c0fae7c.js HTTP/1.1"  - 304 "uuid:a27050e998864af89de0fbc7605d1548"
101.241.91.99 - [04/May/2022:09:36:34 +0800] "GET /assets/quiz.5e3bb724.js HTTP/1.1"  - 304 "uuid:a27050e998864af89de0fbc7605d1548"
101.241.91.99 - [04/May/2022:09:36:34 +0800] "GET /report.gif?id=0&t=1651628194189 HTTP/1.1"  - 204 "uuid:a27050e998864af89de0fbc7605d1548"
101.241.91.99 - [04/May/2022:09:36:34 +0800] "GET /assets/logo.c5f2dde3.jpeg HTTP/1.1"  - 200 "uuid:a27050e998864af89de0fbc7605d1548"
101.241.91.99 - [04/May/2022:09:36:34 +0800] "GET /favicon.ico HTTP/1.1"  - 200 "uuid:a27050e998864af89de0fbc7605d1548"

四、日志切割

为了方便统计我们希望把日志文件按时间分割,分割成这样的结构:

├── 2022
│   └── 05
│       └── 03.log

按照年、月、日分层,每天生成一个日志。

实现思路是,先建立一个日志存放目录,每天的凌晨0点1分,将前一天的日志按照日期移动到日志目录中。然后再重新创建一个日志文件供Nginx写入。

先写一个脚本实现这个功能

log_split.sh

#!/bin/bash
# 定位到脚本所在目录(注意我这里也是Nginx写访问日志的目录,当然这不是必须的)
log_base=$(cd `dirname $0`; pwd)
# 根据前一天的时间生成日志所在目录名
log_path=${log_base}/$(date -d yesterday +%Y)/$(date -d yesterday +%m)
# 创建日志目录
mkdir -p $log_path
# 将当前Nginx的日志移动到指定存放目录
mv $log_base/access443.log $log_path/$(date -d yesterday +%d).log
# 重新创建日志文件,给Nginx写日志用
touch $log_base/access443.log
# 给Nginx发送信号,注意你的Nginx目录可能不同
kill -USR1 `cat /www/server/nginx/logs/nginx.pid`

值得注意的是,虽然移动完日志,并且重新创建,但是Nginx的文件引用还是移走的那个,所以最后要给Nginx发送信号,让它写到新的日志文件中。

脚本写完,我们还要定时(每天0点1分执行切割任务),这用到了Linux的crontab工具。

首先在控制台输入crontab -e打开编辑界面。然后输入1 0 * * * sh /path/to/log/log_split.sh。这个定时任务的意思是每天0点1分执行日志分割脚本,编辑完成后保存关闭,定时任务就生效了。

我们还可以通过crontab -l查看当前的定时任务;通过crontab -r移除当前的定时任务。

五、Nodejs脚本分析日志,统计PV、UV

有了日志,就很容易分析PV、UV。我们可以使用Linux命令分析,但我这次选择用Nodejs脚本来统计,原因是对JS更熟悉,另外相对Linux也更灵活。

分析的大概思路是根据每天的访问日志,过滤出report.gif这个上报请求,上报次数就是PV,然后根据uuid去重,得到UV。

统计脚本如下:

// stats.js
const fs = require('fs');
const path = require('path');
const args = process.argv.slice(2);
const [year] = args;

// 打印统计结果
function echo() {
  yearDir = year || '2022';
  const stats = statsYearLog(yearDir);
  Object.entries(stats)
    .sort(([a], [b]) => a - b)
    .forEach(([month, dateStats]) => {
    console.log(`${month}月`);
    Object.entries(dateStats)
      .sort(([a], [b]) => a - b)
      .forEach(([date, {pv, uv}]) => {
      console.log('  ', `${date}日`, `pv: ${pv}`, `uv: ${uv}`);
    });
    console.log('\n');
  });
}

// 统计某一年的数据
function statsYearLog(year) {
  // 读取目录下的文件夹名字
  const dir = path.resolve(__dirname, year);
  const monthDirList = fs.readdirSync(dir);
  
  const logMap = monthDirList.reduce((result, monthDir) => {
    const monthStats = statsMonthLog(year, monthDir);
    result[monthDir] = monthStats;
    return result;
  }, {});
  
  return logMap;
}

// 统计每个月的数据
function statsMonthLog(year, month) {
  const dir = path.resolve(__dirname, year, month);
  const dateLogList = fs.readdirSync(dir);
  
  const monthLogMap = dateLogList.reduce((result, dateLogFileName) => {
    const dateStats = statsDateLog(year, month, dateLogFileName);
    result[dateLogFileName.replace('.log', '')] = dateStats;
    return result;
  }, {});
  
  return monthLogMap;
}

// 统计某天的数据
function statsDateLog(year, month, dateFile) {
  const logPath = path.resolve(__dirname, year, month, dateFile);
  const logText = fs.readFileSync(logPath, 'utf-8');
  const logList = logText.split('\n');
  const pvLogList = logList.filter((line) => {
    return /report.gif/.test(line)
  });
  const uvLogMap = pvLogList.reduce((result, line) => {
    const match = line.match(/uuid:(\S+)"/);
    if (match && match[1]) {
      result[match[1]] = 1;
    }
    return result;
  }, {});
  
  return {pv: pvLogList.length, uv: Object.keys(uvLogMap).length};
}

// 执行打印统计结果
echo();

执行统计脚本node stats.js 2022

打印结果

05月
   03日 pv: 1 uv: 1

六、展望

后续可以考虑扩展现有能力,让Node实现日志切割的功能,并提供api和界面,可以可视化统计PV、UV。

到此这篇关于使用Nginx的访问日志统计PV与UV的文章就介绍到这了!


Tags in this post...

Servers 相关文章推荐
Nginx的rewrite模块详解
Mar 31 Servers
基于nginx实现上游服务器动态自动上下线无需reload的实现方法
Mar 31 Servers
nginx优化的六点方法
Mar 31 Servers
Nginx服务器添加Systemd自定义服务过程解析
Mar 31 Servers
详解Nginx启动失败的几种错误处理
Apr 01 Servers
NGINX 权限控制文件预览和下载的实现原理
Jan 18 Servers
iSCSI服务器CHAP双向认证配置
Apr 01 Servers
Windows Server 2019 安装DHCP服务及相关配置
Apr 28 Servers
tomcat默认最大连接数及相关调整方法
May 06 Servers
教你如何用cmd快速登录服务器
Jun 10 Servers
本地搭建minio文件服务器(使用bat脚本启动)的方法
Jul 15 Servers
Windows7下FTP搭建图文教程
Aug 05 Servers
Tomcat配置访问日志和线程数
May 06 #Servers
tomcat正常启动但网页却无法访问的几种解决方法
May 06 #Servers
tomcat默认最大连接数及相关调整方法
May 06 #Servers
如何Tomcat中使用ipv6地址
May 06 #Servers
Tomcat弱口令复现及利用
Vscode中SSH插件如何远程连接Linux
nginx配置限速限流基于内置模块
May 02 #Servers
You might like
PHP+DBM的同学录程序(4)
2006/10/09 PHP
php unset全局变量运用问题的深入解析
2013/06/17 PHP
php5.3 不支持 session_register() 此函数已启用的解决方法
2013/11/12 PHP
php解析http获取的json字符串变量总是空白null
2015/03/02 PHP
php基于curl扩展制作跨平台的restfule 接口
2015/05/11 PHP
Zend Framework教程之模型Model用法简单实例
2016/03/04 PHP
PHP HTTP 认证实例详解
2016/11/03 PHP
yii2中LinkPager增加总页数和总记录数的实例
2017/08/28 PHP
JS编程小常识很有用
2012/11/26 Javascript
js跳转页面方法实现汇总
2014/02/11 Javascript
jQuery实现form表单元素序列化为json对象的方法
2015/12/09 Javascript
JavaScript实现搜索框的自动完成功能(一)
2016/02/25 Javascript
JavaScript 函数的执行过程
2016/05/09 Javascript
JS组件Bootstrap Select2使用方法解析
2016/05/30 Javascript
ES6实现的遍历目录函数示例
2017/04/07 Javascript
jQuery回调方法使用示例
2017/06/26 jQuery
js 获取json数组里面数组的长度实例
2017/10/31 Javascript
Vue封装一个简单轻量的上传文件组件的示例
2018/03/21 Javascript
Vue 组件修改根实例的数据的方法
2019/04/02 Javascript
jQuery高级编程之js对象、json与ajax用法实例分析
2019/11/01 jQuery
jQuery实现轮播图效果
2019/11/26 jQuery
关于JavaScript中异步/等待的用法与理解
2020/11/18 Javascript
vue组件中节流函数的失效的原因和解决方法
2020/12/02 Vue.js
[47:02]2018DOTA2亚洲邀请赛3月29日 小组赛B组 VP VS paiN
2018/03/30 DOTA
[38:54]完美世界DOTA2联赛PWL S2 Rebirth vs LBZS 第一场 11.28
2020/12/01 DOTA
Python对两个有序列表进行合并和排序的例子
2014/06/13 Python
python中for语句简单遍历数据的方法
2015/05/07 Python
微信小程序跳一跳游戏 python脚本跳一跳刷高分技巧
2018/01/04 Python
对python字典元素的添加与修改方法详解
2018/07/06 Python
Python FTP文件定时自动下载实现过程解析
2019/11/12 Python
基于spring boot 日志(logback)报错的解决方式
2020/02/20 Python
利用Python实现自动扫雷小脚本
2020/12/17 Python
会计专业大学生职业生涯规划范文
2014/01/11 职场文书
法院个人总结
2015/03/03 职场文书
亮剑观后感600字
2015/06/05 职场文书
导游词之泰山玉皇顶
2019/12/23 职场文书