網站首頁 編程語言 正文
前言
一個網站當用戶量增大時候,不可避免有統計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收到上報后,根據我們指定的固定格式生成日志。我們還要設置定時任務,定期切割日志,以便分析日志時候以月和天為維度統計指標。
整體流程示意圖如下:
二、上報訪問事件
前端使用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。
原文鏈接:https://juejin.cn/post/7093699777572896804
相關推薦
- 2023-12-21 npm install 報錯(npm ERR! errno: -4048, npm ERR! c
- 2022-09-01 openGauss數據庫在CentOS上的安裝實踐記錄_數據庫其它
- 2023-03-16 iOS?schem與Universal?Link?調試時踩坑解決記錄_IOS
- 2021-11-12 C語言打印某一年中某月的日歷_C 語言
- 2022-05-19 yolov5調用usb攝像頭及本地攝像頭的方法實例_python
- 2022-03-17 VSCode如何遠程連接Linux教程(vscode怎么連接ssh遠程)
- 2023-03-26 WPF使用觸發器需要注意優先級問題解決_C#教程
- 2023-03-26 使用docker安裝hadoop的實現過程_docker
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支