Duncan's Blog

何以解忧,唯有学习

0%

网站流量日志数据自定义采集

原理分析

首先,用户的行为会触发浏览器对被统计页面的一个HTTP请求。比如,让用户打开
一个页面时,触发页面中的埋点代码的执行。

埋点:事先在网页中加入的一小段js代码,这个代码片段一般会动态创建一个
script标签,并将src属性指向一个单独的js文件,然后这个js文件会从数据采集服务器端返回,并自动触发(js匿名函数自调用),
这个js往往就是真正的数据收集脚本。数据收集完成后,js会请求一个后端的数据收集脚本,这个脚本一般是一个伪装成图片的动态
脚本程序,js会将收集到的数据通过HTTP参数的形式传递给后端脚本。后端脚本解析参数并按指定的格式存储该数据。同时可能会在
HTTP响应中给客户端种植一些用于追踪的cookie。

模拟图片资源的请求,是因为<img>标签的src 属性有跨域请求的特性


设计实现

  根据原理分析结合Google Analytics,搭建一个自定义日志数据采集系统,可以按照如下步骤进行:

  • 确定收集的字段

  • 确定埋点代码

  以谷歌的Google Analytics来说,需要在页面中插入它提供的JavaScript片段,这个片段就被称为埋点代码

<script type="text/javascript">
var _maq = _maq || [];
_maq.push(['_setAccount', 'UA-XXXXX-X']);//向_maq中添加一条配置
(function() {
var ma = document.createElement('script'); ma.type =
'text/javascript'; ma.async = true;
ma.src = ('https:' == document.location.protocol ?
'https://ssl' : 'http://www') + '.google-analytics.com/ma.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ma, s);
})();
</script>

其中_maq是全局数组,用于放置配置信息,每条配置的格式为:

_maq.push(['Action', 'param1', 'param2', ...]);

  后边的匿名函数的代码主要目的就是引入一个外部的js文件(ma.js),方式是通过document.createElement方法
创建一个script并将src属性指向数据采集服务器上的ma.js,最后将这个元素插入到dom树上。
ma.sync=true的意思是异步调用外部js文件,即不阻塞浏览器的解析,待外部下载完成后异步执行,这个是HTML5新引入的

3、前端数据收集脚本

  数据收集脚本(ma.js)被请求后会被执行,一般要做如下几件事:

  1. 通过浏览器内置的JavaScript对象收集信息,如页面title(document.title)、上一跳(document.referrer)、用户显示器分辨率(windows.screen)、cookie信息(document.cookie)等信息。

  2. 解析_maq数组,收集配置信息。可能会包含用户自定义的事件跟踪、业务数据等。

  3. 将上面两步收集的数据按预定义格式解析并拼接(get请求参数)

  4. 请求一个后端脚本,将信息放在http.request参数中携带给后端脚本。

示例代码:

(function () {
var params = {};
//Document 对象数据
if(document) {
params.domain = document.domain || '';
params.url = document.URL || '';
params.title = document.title || '';
params.referrer = document.referrer || '';
}
//Window 对象数据
if(window && window.screen) {
params.sh = window.screen.height || 0;
params.sw = window.screen.width || 0;
params.cd = window.screen.colorDepth || 0;
}
//navigator 对象数据
if(navigator) {
params.lang = navigator.language || '';
}
//解析_maq 配置
if(_maq) {
for(var i in _maq) {
switch(_maq[i][0]) {
case '_setAccount':
params.account = _maq[i][1];
break;
default:
break;
}
}
}
//拼接参数串
var args = '';
for(var i in params) {
if(args != '') {
args += '&';
}
args += i + '=' + encodeURIComponent(params[i]);
}
//通过 Image 对象请求后端脚本
var img = new Image(1, 1);
img.src = 'http://xxx.xxxxx.xxxxx/log.gif?' + args;
})();

脚本放在匿名函数里,确保不会污染全局环境。其中log.gif就是后端脚本。

4、后端脚本

  log.gif是一个伪装成GIF图片的脚本。后端脚本一般需要完成以下几件事情:

(1)解析HTTP请求参数得到信息。

(2)从web服务器中获取一些客户端无法获取的信息。

(3)将信息按格式写入log。

(4)生成一个1*1的空GIF图片作为响应内容并将响应头的Content-type设为image/gif。

(5)在响应头中通过Set-cookie设置一些需要的cookie信息。(用于追踪唯一访客)

基于nginx的日志收集受制于nginx配置本身的逻辑表达能力有限,所以选用OpenResty来做。

  OpenResty是一个基于nginx扩展出的高性能应用开发平台,内部集成了诸多有用的模块,
其中的核心模块就是通过ngx_lua模块集成了Lua,从而在nginx配置文件中可以通过Lua,来表述业务。

  Lua是一中轻量小巧使用C语言编写的脚本语言。其设计目的就是为了嵌入应用程序中,
从而为应用程序提供灵活的扩展和定制功能。

  首先需要在nginx的配置文件中定义日志格式:

log_format tick
"$msec||$remote_addr||$status||$body_bytes_sent||$u_domain||$u_url|
|$u_title||$u_referrer||$u_sh||$u_sw||$u_cd||$u_lang||$http_user_ag
ent||$u_account";

这里以 u_开头的是我们待会会自己定义的变量,其它的是 nginx 内置变
量。 然后是核心的两个 location:

location / log.gif {
#伪装成 gif 文件
default_type image/gif;
#本身关闭 access_log,通过 subrequest 记录 log
access_log off;
access_by_lua "
-- 用户跟踪 cookie 名为__utrace
local uid = ngx.var.cookie___utrace
if not uid then
-- 如果没有则生成一个跟踪 cookie,算法为
md5(时间戳+IP+客户端信息)
uid = ngx.md5(ngx.now() ..
ngx.var.remote_addr .. ngx.var.http_user_agent)
end
ngx.header['Set-Cookie'] = {'__utrace=' .. uid ..
'; path=/'}
if ngx.var.arg_domain then
-- 通过 subrequest 子请求到/i-log 记录日志,
将参数和用户跟踪 cookie 带过去
ngx.location.capture('/i-log?' ..
ngx.var.args .. '&utrace=' .. uid)
end
";
#此请求资源本地不缓存
add_header Expires "Fri, 01 Jan 1980 00:00:00 GMT";
add_header Pragma "no-cache";
add_header Cache-Control "no-cache, max-age=0, mustrevalidate";
#返回一个 1×1 的空 gif 图片
empty_gif;
}
location /i-log {
#内部 location,不允许外部直接访问
internal;
#设置变量,注意需要 unescape,来自 ngx_set_misc 模块
set_unescape_uri $u_domain $arg_domain;
set_unescape_uri $u_url $arg_url;
set_unescape_uri $u_title $arg_title;
set_unescape_uri $u_referrer $arg_referrer;
set_unescape_uri $u_sh $arg_sh;
set_unescape_uri $u_sw $arg_sw;
set_unescape_uri $u_cd $arg_cd;
set_unescape_uri $u_lang $arg_lang;
set_unescape_uri $u_account $arg_account;
#打开日志
log_subrequest on;
#记录日志到 ma.log 格式为 tick
access_log /path/to/logs/directory/ma.log tick;
#输出空字符串
echo '';
}

5、日志切分

  日志格式主要考虑日志分隔符,一般会有一下几种选择:

  • 固定数量的字符
  • 制表符分割
  • 空格分隔符
  • 其他一个或多个字符
  • 特定的开始和结束文本

6、日志切分

  日志收集系统访问日志时间一长文件变得很大,而且日志放在一个文件不便
于管理。 通常要按时间段将日志切分,例如每天或每小时切分一个日志。通过
crontab 定时调用一个 shell 脚本实现,如下:

_prefix="/path/to/nginx"
time=`date +%Y%m%d%H`
mv ${_prefix}/logs/ma.log ${_prefix}/logs/ma/ma-${time}.log
kill -USR1 `cat ${_prefix}/logs/nginx.pid `

  USR1 通常被用来告知应用程序重载配置文件, 向服务器发送一个 USR1 信号
将导致以下步骤的发生:停止接受新的连接,等待当前连接停止,重新载入配置
文件,重新打开日志文件,重启服务器,从而实现相对平滑的不关机的更改。

然后向/etc/crontab里加入一行:

59 * * * * root /path/to/directory/rotatelog.sh

在每个小时的 59 分启动这个脚本进行日志轮转操作。

自定义采集数据实现

方案一:基本功能实现

1、创建index.html,添加埋点代码,放入nginx默认目录nginx/html下。

2、在默认目录nginx/html下添加一个数据采集脚本ma.js

3、修改nginx的配置文件,添加自定义相关业务逻辑。

4、启动nginx,通过浏览器访问nginx。

5、观察自定义日志采集文件是否有对应的内容输出
tail -F logs/user_defined.log

方案二:页面点击事件

修改方案一中的数据采集脚本,将脚本内的匿名函数自调用改成由点击事件触发执行。

附件:
相关文件下载