使用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配置文件路径和资源文件路径的方法
Mar 31 Servers
图文详解Nginx版本平滑升级方案
Sep 15 Servers
Kubernetes控制节点的部署
Apr 01 Servers
Dashboard管理Kubernetes集群与API访问配置
Apr 01 Servers
Docker下安装Oracle19c
Apr 13 Servers
Nginx 安装SSL证书完成HTTPS部署
Apr 28 Servers
Nginx 常用配置
May 15 Servers
Apache SeaTunnel实现 非CDC数据抽取
May 20 Servers
项目中Nginx多级代理是如何获取客户端的真实IP地址
May 30 Servers
Windows server 2022创建创建林、域树、子域的步骤
Jun 25 Servers
Linux中各个目录的作用与内容
Jun 28 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 高手之路(三)
2006/10/09 PHP
php多功能图片处理类分享(php图片缩放类)
2014/03/14 PHP
php页面函数设置超时限制的方法
2014/12/01 PHP
PHP PDO操作MySQL基础教程
2017/06/05 PHP
thinkPHP框架整合tcpdf插件操作示例
2018/08/07 PHP
PHP判断函数是否被定义的方法
2019/06/21 PHP
javascript显示选择目录对话框的代码
2008/11/10 Javascript
基于jquery的finkyUI插件与Ajax实现页面数据加载功能
2010/12/03 Javascript
如何获取JQUERY AJAX返回的JSON结果集实现代码
2012/12/10 Javascript
html页面显示年月日时分秒和星期几的两种方式
2013/08/20 Javascript
Javascript中克隆一个数组的实现代码
2013/12/06 Javascript
jquery让返回的内容显示在特定div里(代码少而精悍)
2014/06/23 Javascript
jQuery超赞的评分插件(8款)
2015/08/20 Javascript
纯css下拉菜单 无需js
2016/08/15 Javascript
jQuery 中msgTips 顶部弹窗效果实现代码
2017/08/14 jQuery
Vue 父子组件数据传递的四种方式( inheritAttrs + $attrs + $listeners)
2018/05/04 Javascript
深入浅析AngularJs模版与v-bind
2018/07/06 Javascript
分享5个好用的javascript文件上传插件
2018/09/16 Javascript
Mint UI实现A-Z字母排序的城市选择列表
2018/12/28 Javascript
jquery实现轮播图特效
2020/04/12 jQuery
[04:53]DOTA2英雄基础教程 祈求者
2014/01/03 DOTA
python使用cookie库操保存cookie详解
2014/03/03 Python
Python字符串拼接的几种方法整理
2017/08/02 Python
Python面向对象编程基础解析(二)
2017/10/26 Python
用不到50行的Python代码构建最小的区块链
2017/11/16 Python
解决已经安装requests,却依然提示No module named requests问题
2018/05/18 Python
python3 线性回归验证方法
2019/07/09 Python
pycharm 设置项目的根目录教程
2020/02/12 Python
PyCharm中如何直接使用Anaconda已安装的库
2020/05/28 Python
requests在python中发送请求的实例讲解
2021/02/17 Python
pytorch 计算Parameter和FLOP的操作
2021/03/04 Python
人事专员岗位职责
2013/11/20 职场文书
求职毕业生自荐书
2014/02/08 职场文书
2014年新教师工作总结
2014/11/08 职场文书
2014年服务行业工作总结
2014/11/18 职场文书
2015年重阳节慰问信
2015/03/23 职场文书