使用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配置proxy_pass中url末尾带/与不带/的区别详解
Mar 31 Servers
Nginx设置日志打印post请求参数的方法
Mar 31 Servers
Nginx配置文件详解以及优化建议指南
Sep 15 Servers
解决使用了nginx获取IP地址都是127.0.0.1 的问题
Sep 25 Servers
了解Kubernetes中的Service和Endpoint
Apr 01 Servers
Nginx 安装SSL证书完成HTTPS部署
Apr 28 Servers
Tomcat配置访问日志和线程数
May 06 Servers
永中文档在线转换预览基于nginx配置部署方案
Jun 10 Servers
安装harbor作为docker镜像仓库的问题
Jun 14 Servers
git中cherry-pick命令的使用教程
Jun 25 Servers
阿里云服务器(windows)手动部署FTP站点详细教程
Aug 05 Servers
win10搭建配置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
星际争霸任务指南——神族
2020/03/04 星际争霸
php中取得URL的根域名的代码
2011/03/23 PHP
php找出指定范围内回文数且平方根也是回文数的方法
2015/03/23 PHP
PHP生成腾讯云COS接口需要的请求签名
2018/05/20 PHP
几款极品的javascript压缩混淆工具
2007/05/16 Javascript
xml 封装与解析(javascript和C#中)
2009/07/26 Javascript
不安全的常用的js写法
2009/09/15 Javascript
jquery学习笔记二 实现可编辑的表格
2010/04/09 Javascript
基于Jquery制作的幻灯片图集效果打包下载
2011/02/12 Javascript
jq选项卡鼠标延迟的插件实例
2013/05/13 Javascript
jQuery不兼容input的change事件问题解决过程
2014/12/05 Javascript
浅谈JavaScript的Polymer框架中的事件绑定
2015/07/29 Javascript
使用jQuery获取data-的自定义属性
2015/11/10 Javascript
CSS3 media queries结合jQuery实现响应式导航
2016/09/30 Javascript
jQuery动态增减行的实例代码解析(推荐)
2016/12/05 Javascript
微信小程序 省市区选择器实例详解(附源码下载)
2017/01/05 Javascript
react.js 获取真实的DOM节点实例(必看)
2017/04/17 Javascript
Vue 2.0在IE11中打开项目页面空白的问题解决
2017/07/16 Javascript
webpack开发跨域问题解决办法
2017/08/03 Javascript
MUI 实现侧滑菜单及其主体部分上下滑动的方法
2018/01/25 Javascript
security.js实现的RSA加密功能示例
2018/06/06 Javascript
Javascript实现关闭广告效果
2021/01/29 Javascript
[02:19]DOTA2女子战队FOX视频专访:希望更多美眉一起加入
2013/10/15 DOTA
[57:16]2014 DOTA2华西杯精英邀请赛 5 25 LGD VS VG第二场
2014/05/26 DOTA
python中enumerate函数用法实例分析
2015/05/20 Python
简介Django中内置的一些中间件
2015/07/24 Python
Python3 中sorted() 函数的用法
2020/03/24 Python
解决import tensorflow as tf 出错的原因
2020/04/16 Python
基于plt.title无法显示中文的快速解决
2020/05/16 Python
Python执行时间的几种计算方法
2020/07/31 Python
利用Python将图片中扭曲矩形的复原
2020/09/07 Python
学习经验交流会主持词
2014/04/01 职场文书
企业安全标语
2014/06/07 职场文书
2015年五一劳动节慰问信
2015/03/23 职场文书
2016年感恩父亲节活动总结
2016/04/01 职场文书
90后经典动画片排行:《数码宝贝》第二,《小鲤鱼历险记》在榜
2022/03/18 日漫