在nginx的日志中记录每个用户的访问计数器

我想做的事情

在使用的网站上,将唯一的用户ID(uid)记录在nginx日志中,并将日志传输到elasticsearch,因此可以轻松通过kibana查看DAU(还包括HAU,WAU,MAU)。

我想要看到DAU(日活跃用户数)的细分,以了解用户以多大的频率访问网站,并将他们分成不同的组别,因为DAU目前包含了新参与者和之前已经访问过的人。

    • ほぼ毎日きてくれる人

 

    • 1日何回もログインしてる人

 

    初回参加のみの人

设定的东西

    1. 每当有请求访问nginx时,更新Redis中为每个用户准备的访问计数器。

 

    1. 从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

bannerAds