在nginx的日志中记录每个用户的访问计数器
我想做的事情
在使用的网站上,将唯一的用户ID(uid)记录在nginx日志中,并将日志传输到elasticsearch,因此可以轻松通过kibana查看DAU(还包括HAU,WAU,MAU)。
我想要看到DAU(日活跃用户数)的细分,以了解用户以多大的频率访问网站,并将他们分成不同的组别,因为DAU目前包含了新参与者和之前已经访问过的人。
-
- ほぼ毎日きてくれる人
-
- 1日何回もログインしてる人
- 初回参加のみの人
设定的东西
-
- 每当有请求访问nginx时,更新Redis中为每个用户准备的访问计数器。
-
- 从Redis中读取访问计数器,并将计数数目记录在nginx的访问日志中。
- 将日志导入ElasticSearch,并通过可视化工具对DAU进行细分并显示。
虽然我认为最好是在将日志流入ElasticSearch之时处理,以防止由nginx处理引起响应时间下降,但这次我允许了这种情况。
nginx的配置
因为需要使用 ngx.location.capture和与redis建立连接,所以决定使用openresty。
- nginx.conf(部分抜粋)
json_log_fields main 'time_local'
'x-uid'
'cnt_hour'
'cnt_dow'
'cnt_day'
'cnt_sec'
set $uid "unknown";
if ($x-uid ~ ([0-9a-z]+)) {
set $uid $1;
}
strftime hour "%H"; # 時 0-23
strftime dow "%w" # 曜日 0-6
strftime day "%d"; # 日付 01-31
strftime sec "%S"; # 秒 01-59
set $cnt_hour "0";
set $cnt_dow "0";
set $cnt_day "0";
set $cnt_sec "0";
set $res "-";
location /update_counter {
internal;
redis2_raw_queries 2 "select 15\r\nevalsha 7d935b5da11543863d3de7e8af785052aebff453 1 $uid $sec $hour $dow $day\r\n";
redis2_pass redis_server;
}
location /get_counter {
internal;
redis2_raw_queries 2 "select 15\r\nevalsha b1c165f798a1a3df36bb5f513a7a59efde68d941 1 $uid\r\n";
redis2_pass redis_server;
}
location / {
access_by_lua '
ngx.location.capture("/update_counter")
local res = ngx.location.capture("/get_counter")
ngx.var.res = res.body
local m, err = ngx.re.match(res.body, "([0-9]+):([0-9]+):([0-9]+):([0-9]+)")
if m then
ngx.var.cnt_sec = m[1]
ngx.var.cnt_hour = m[2]
ngx.var.cnt_dow = m[3]
ngx.var.cnt_day = m[4]
end
';
limit_req zone=one burst=2;
proxy_pass http://puma-app;
}
evalshaを使って後述のredisに登録したスクリプトを呼び出している。
/update_counter
uid別に持っているアクセスカウンタを更新する。
/get_counter
uid別に持っているアクセスカウンタを取得する。
/location
/update_counter /get_counterを実行してアクセスカウンタを$cnt_sec|hour|dow|dayに設定する(ログにそのまま出力される)
Redis计数器
使用TTL设置访问计数器。
-
- 直近一分のアクセス キー名: “$uid:sec:[0-59]” 値: 1
-
- 直近24時間のアクセス キー名: “$uid:hour:[0-23]” 値: 1
-
- 直近1週間のアクセス キー名: “$uid:dow:[0-6]” 値: 1
- 直近30日のアクセス キー名: “$uid:day:[01-31]” 値: 1
script load "
redis.call('setex', KEYS[1] .. ':sec:' .. ARGV[1], 60, 1)
redis.call('setex', KEYS[1] .. ':hour:' .. ARGV[2], 86400, 1)
redis.call('setex', KEYS[1] .. ':dow:' .. ARGV[3], 604800, 1)
redis.call('setex', KEYS[1] .. ':day:' .. ARGV[4],2592000, 1)
"
7d935b5da11543863d3de7e8af785052aebff453
读取访问计数器
カウンタを読み出す
戻り値
直近1分以内のアクセス数:直近24時間以内のアクセス数:直近7日以内のアクセス数:直近30日以内のアクセス数
(アクセス数は合計値ではなくて単位あたりのユニークカウントとする。例えば金曜日1回、土曜日100回アクセスしても1週間のアクセスは2)
script load "
local sec = 0
local hour = 0
local dow = 0
local day = 0
local ret = nil
for i = 0,59 do
ret = redis.call('get', KEYS[1] .. ':sec:' .. string.format('%02d', i))
if ret then
sec = sec + 1
end
end
for i = 0,23 do
ret = redis.call('get', KEYS[1] .. ':hour:' .. string.format('%02d', i))
if ret then
hour = hour + 1
end
end
for i = 0,6 do
ret = redis.call('get', KEYS[1] .. ':dow:' .. string.format('%1d', i))
if ret then
dow = dow + 1
end
end
for i = 1,31 do
ret = redis.call('get', KEYS[1] .. ':day:' .. string.format('%02d', i))
if ret then
day = day + 1
end
end
return string.format('%d:%d:%d:%d', sec, hour, dow, day)
"
b1c165f798a1a3df36bb5f513a7a59efde68d941
- ユーザ1回のアクセスでredisの操作が120回以上発生しているのはまずいか。
在Kibana上的可视化
-
- 例) 直近一週間のうち何回きてるかでDAUをグルーピングして表示する
Y-axis x-uidでunique count
X-axis タイムスタンプ
split bars -> Histgram -> cnt_dow -> interval 1