使用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处理http请求实现过程解析
Mar 31 Servers
Nginx Rewrite使用场景及配置方法解析
Apr 01 Servers
nginx部署多前端项目的几种方法
May 25 Servers
Nginx四层负载均衡的配置指南
Jun 11 Servers
Apache Pulsar结合Hudi构建Lakehouse方案分析
Mar 31 Servers
Nginx+Tomcat负载均衡多实例详解
Apr 11 Servers
Tomcat starup.bat 脚本实现开机自启动
Apr 20 Servers
Linux磁盘管理方法介绍
Jun 01 Servers
Tomcat 与 maven 的安装与使用教程
Jun 16 Servers
windows server2008 开启端口的实现方法
Jun 25 Servers
教你nginx跳转配置的四种方式
Jul 07 Servers
win7配置本地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二维数组用键名分组相加实例函数
2013/11/06 PHP
laravel框架中间件简单使用方法示例
2020/01/25 PHP
JavaScript 笔记二 Array和Date对象方法
2010/05/22 Javascript
在javascript将NodeList作为Array数组处理的方法
2010/07/09 Javascript
基于jQuery实现图片的前进与后退功能
2013/04/24 Javascript
jquery datepicker参数介绍和示例
2014/04/15 Javascript
jQuery的事件委托实例分析
2015/07/15 Javascript
分享两款带遮罩的jQuery弹出框
2015/12/30 Javascript
javascript求日期差的方法
2016/03/02 Javascript
基于HTML模板和JSON数据的JavaScript交互(移动端)
2016/04/06 Javascript
jQuery插件ajaxfileupload.js实现上传文件
2020/10/23 Javascript
ES6中的数组扩展方法
2016/08/26 Javascript
Angular2学习笔记——详解路由器模型(Router)
2016/12/02 Javascript
微信小程序 引用其他js文件实现代码
2017/02/22 Javascript
基于Vue实现页面切换左右滑动效果
2020/06/29 Javascript
浅谈使用React.setState需要注意的三点
2017/12/18 Javascript
jQuery实现的两种简单弹窗效果示例
2018/04/18 jQuery
详解离线安装npm包的几种方法
2018/11/25 Javascript
angular 实现同步验证器跨字段验证的方法
2019/04/11 Javascript
Vue如何基于es6导入外部js文件
2020/05/15 Javascript
PYTHON 中使用 GLOBAL引发的一系列问题
2016/10/12 Python
python爬取微信公众号文章
2018/08/31 Python
pyqt5 实现多窗口跳转的方法
2019/06/19 Python
django搭建项目配置环境和创建表过程详解
2019/07/22 Python
Python中IP地址处理IPy模块的方法
2019/08/16 Python
详解python 中in 的 用法
2019/12/12 Python
详解使用scrapy进行模拟登陆三种方式
2021/02/21 Python
关键字final的用法
2013/10/02 面试题
自学考试自我鉴定范文
2013/09/26 职场文书
敬老模范事迹
2014/05/21 职场文书
土建技术员岗位职责
2015/04/11 职场文书
钱学森观后感
2015/06/04 职场文书
电影焦裕禄观后感
2015/06/09 职场文书
毕业季聚会祝酒词!
2019/07/04 职场文书
创业计划书之游泳馆
2019/09/16 职场文书
22句经典语录:送给优柔寡断和胡思乱想的朋友们
2019/12/13 职场文书