日志离线收集处理方案——1.数据采集
一. 埋点
测试时,需要自己模拟一个网站系统,在其中需要埋点的页面中的<head></head>中加入如下代码:
<script src="tongji.js"></script> 注:tongji.js 就是需埋点的 js 文件
下面是 tongji.js 文件的代码:
/**函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。*/ function ar_encode(str) { //进行URL编码 return encodeURI(str); } /**屏幕分辨率*/ function ar_get_screen() { var c = ""; if (self.screen) { c = screen.width+"x"+screen.height; } return c; } /**颜色质量*/ function ar_get_color() { var c = ""; if (self.screen) { c = screen.colorDepth+"-bit"; } return c; } /**返回当前的浏览器语言*/ function ar_get_language() { var l = ""; var n = navigator; if (n.language) { l = n.language.toLowerCase(); } else if (n.browserLanguage) { l = n.browserLanguage.toLowerCase(); } return l; } /**返回浏览器类型IE,Firefox*/ function ar_get_agent() { var a = ""; var n = navigator; if (n.userAgent) { a = n.userAgent; } return a; } /**返回浏览器是否支持(启用)cookie */ function ar_get_cookie_enabled() { var c = ""; var n = navigator; c = n.cookieEnabled ? 1 : 0; return c; } /**匹配顶级域名*/ function ar_c_ctry_top_domain(str) { var pattern = "/^aero$|^cat$|^coop$|^int$|^museum$|^pro$|^travel$|^xxx$|^com$|^net$|^gov$|^org$|^mil$|^edu$|^biz$|^info$|^name$|^ac$|^mil$|^co$|^ed$|^gv$|^nt$|^bj$|^hz$|^sh$|^tj$|^cq$|^he$|^nm$|^ln$|^jl$|^hl$|^js$|^zj$|^ah$|^hb$|^hn$|^gd$|^gx$|^hi$|^sc$|^gz$|^yn$|^xz$|^sn$|^gs$|^qh$|^nx$|^xj$|^tw$|^hk$|^mo$|^fj$|^ha$|^jx$|^sd$|^sx$/i"; if(str.match(pattern)){ return 1; } return 0; } /**处理域名地址*/ function ar_get_domain(host) { //如果存在则截去域名开头的 "www." var d=host.replace(/^www\./, ""); //剩余部分按照"."进行split操作,获取长度 var ss=d.split("."); var l=ss.length; //如果长度为3,则为xxx.yyy.zz格式 if(l == 3){ //如果yyy为顶级域名,zz为次级域名,保留所有 if(ar_c_ctry_top_domain(ss[1]) && ar_c_ctry_domain(ss[2])){ } //否则只保留后两节 else{ d = ss[1]+"."+ss[2]; } } //如果长度大于3 else if(l >= 3){ //如果host本身是个ip地址,则直接返回该ip地址为完整域名 var ip_pat = "^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$"; if(host.match(ip_pat)){ return d; } //如果host后两节为顶级域名及次级域名,则保留后三节 if(ar_c_ctry_top_domain(ss[l-2]) && ar_c_ctry_domain(ss[l-1])) { d = ss[l-3]+"."+ss[l-2]+"."+ss[l-1]; } //否则保留后两节 else{ d = ss[l-2]+"."+ss[l-1]; } } return d; } /**返回cookie信息*/ function ar_get_cookie(name) { //获取所有cookie信息 var co=document.cookie; //如果名字是个空 返回所有cookie信息 if (name == "") { return co; } //名字不为空 则在所有的cookie中查找这个名字的cookie var mn=name+"="; var b,e; b=co.indexOf(mn); //没有找到这个名字的cookie 则返回空 if (b < 0) { return ""; } //找到了这个名字的cookie 获取cookie的值返回 e=co.indexOf(";", b+name.length); if (e < 0) { return co.substring(b+name.length + 1); } else { return co.substring(b+name.length + 1, e); } } /** 设置cookie信息 操作符: 0 表示不设置超时时间 cookie是一个会话级别的cookie cookie信息保存在浏览器内存当中 浏览器关闭时cookie消失 1 表示设置超时时间为10年以后 cookie会一直保存在浏览器的临时文件夹里 直到超时时间到来 或用户手动清空cookie为止 2 表示设置超时时间为1个小时以后 cookie会一直保存在浏览器的临时文件夹里 直到超时时间到来 或用户手动清空cookie为止 * */ function ar_set_cookie(name, val, cotp) { var date=new Date; var year=date.getFullYear(); var hour=date.getHours(); var cookie=""; if (cotp == 0) { cookie=name+"="+val+";"; } else if (cotp == 1) { year=year+10; date.setYear(year); cookie=name+"="+val+";expires="+date.toGMTString()+";"; } else if (cotp == 2) { hour=hour+1; date.setHours(hour); cookie=name+"="+val+";expires="+date.toGMTString()+";"; } var d=ar_get_domain(document.domain); if(d != ""){ cookie +="domain="+d+";"; } cookie +="path="+"/;"; document.cookie=cookie; } /**返回客户端时间*/ function ar_get_stm() { return new Date().getTime(); } /**返回指定个数的随机数字串*/ function ar_get_random(n) { var str = ""; for (var i = 0; i < n; i ++) { str += String(parseInt(Math.random() * 10)); } return str; } /* main function */ function ar_main() { //收集完日志 提交到的路径 var dest_path = "http://192.168.3.152/proxy/big.png?"; var expire_time = 30 * 60 * 1000;//会话超时时长 //处理uv //--获取cookie ar_stat_uv的值 var uv_str = ar_get_cookie("ar_stat_uv"); var uv_id = ""; //--如果cookie ar_stat_uv的值为空 if (uv_str == ""){ //--为这个新uv配置id,为一个长度20的随机数字 uv_id = ar_get_random(20); //--设置cookie ar_stat_uv 保存时间为10年 ar_set_cookie("ar_stat_uv", uv_id, 1); } //--如果cookie ar_stat_uv的值不为空 else{ //--获取uv_id uv_id = uv_str; } //处理ss //--获取cookie ar_stat_ss var ss_str = ar_get_cookie("ar_stat_ss"); var ss_id = ""; //sessin id var ss_no = 0; //session有效期内访问页面的次数 //--如果cookie中不存在ar_stat_ss 说明是一次新的会话 if (ss_str == ""){ //--随机生成长度为10的session id ss_id = ar_get_random(10); //--session有效期内页面访问次数为0 ss_no = 0; //--拼接cookie ar_stat_ss 值 格式为 会话编号_会话期内访问次数_客户端时间_网站id value = ss_id+"_"+ss_no+"_"+ar_get_stm(); //--设置cookie ar_stat_ss ar_set_cookie("ar_stat_ss", value, 0); } //--如果cookie中存在ar_stat_ss else { //获取ss相关信息 var items = ss_str.split("_"); //--ss_id var cookie_ss_id = items[0]; //--ss_no var cookie_ss_no = parseInt(items[1]); //--ss_stm var cookie_ss_stm = items[2]; //如果当前时间-当前会话上一次访问页面的时间>30分钟,虽然cookie还存在,但是其实已经超时了!仍然需要重新生成cookie if (ar_get_stm() - cookie_ss_stm > expire_time) { //--重新生成会话id ss_id = ar_get_random(10); //--设置会话中的页面访问次数为0 ss_no = 0; } //--如果会话没有超时 else{ //--会话id不变 ss_id = cookie_ss_id; //--设置会话中的页面方位次数+1 ss_no = cookie_ss_no + 1; } //--重新拼接cookie ar_stat_ss的值 value = ss_id+"_"+ss_no+"_"+ar_get_stm(); ar_set_cookie("ar_stat_ss", value, 0); } //当前地址 var url = document.URL; url = ar_encode(String(url)); //当前资源名 var urlname = document.URL.substring(document.URL.lastIndexOf("/")+1); urlname = ar_encode(String(urlname)); //返回导航到当前网页的超链接所在网页的URL var ref = document.referrer; ref = ar_encode(String(ref)); //屏幕信息 var screen = ar_get_screen(); screen = ar_encode(String(screen)); //浏览器是否支持并启用了cookie var cookie_enabled =ar_get_cookie_enabled(); cookie_enabled =ar_encode(String(cookie_enabled)); //当前ss状态 格式为"会话id_会话次数_当前时间" var stat_ss = ss_id+"_"+ss_no+"_"+ar_get_stm(); //拼接访问地址 增加如上信息 dest = dest_path + "url="+url + "&urlname=" + urlname + "&scr=" + screen + "&ce=" + cookie_enabled + "&cnv=" + String(Math.random()) + "&ref=" + ref + "&stat_uv=" + uv_id + "&stat_ss=" + stat_ss ; //通过插入图片访问该地址 document.getElementsByTagName("body")[0].innerHTML += "<img src=\""+dest+"\" border=\"0\" width=\"1\" height=\"1\" />"; } window.onload = function(){ //触发main方法 ar_main(); }
说明:
1. var dest_path = "http://192.168.3.152/proxy/big.png?"; 此处要改成日志服务器的地址,并且这个地址是能够被访问的,最后一个 "?“ 不要忘加,用来拼后续参数使用的。
2. 埋点的原理:js代码会动态在页面中创建一个宽和高都是1px的图片,图片的地址指向了1中定义的日志服务器中的图片,
document.getElementsByTagName("body")[0].innerHTML += "<img src=\""+dest+"\" border=\"0\" width=\"1\" height=\"1\" />";
二. 日志服务器日志格式定义
日志服务器一般会使用nginx或apache ,此处以nginx为例,一般日志服务器会安装在linux Centos系统上,nginx日志格式定义的文件通常位于 /etc/nginx/nginx.conf ,如下:
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
说明:上述红色部分为日志格式定义部分,每一行用单引号括住,每一个字段列用又引号括住 , 当前样例只是用到部分日志列
配置中具体可用到的参数说明:
三. 日志切割
对于忙碌的服务器,日志文件大小会增长极快,服务器会很快消耗磁盘空间,这成了个问题。除此之外,处理一个单个的庞大日志文件也常常是件十分棘手的事。在Linux下提供了一个十分有用的工具Logrotate ,配合Linux定时任务它可以自动对日志进行截断(或轮循)、压缩以及删除旧的日志文件。配置完后,logrotate的运作完全自动化,不必进行任何进一步的人为干预。更为方便的是,Centos7系统安装完成,Logrotate 和 Crontab这样的工具都是自动安装好的,直接可以使用。
日志首先要配置相应的文件,logrotate命令需要根据具体的切割指令文件去完成切割,切割的指令是一个JSON串的格式,这个串包括了具体的指令参数,具体可用到的参数如下:
下面是 具体的一个配置指令的实例 (其中红色部分为我们自定义的执行语句):
{
daily
missingok
rotate 1
delaycompress
notifempty
create 640 nginx adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
mv /var/log/nginx/access.log.* /root/logs/$(date +%Y%m%d-%H).log
fi
endscript
}
日志的定时切割可以两种方式来做:
1. 通过crontab -e来创建定时任务
2. 如果是按每小时或每天来触发的定时任务,可以在 /etc/cron.daily 或/etc/cron.hourly 目录下创时要执行的脚本:
例: /etc/cron.daily/logrotate
/usr/sbin/logrotate -s /var/lib/logrotate/logrotate.status /etc/logrotate.conf
EXITVALUE=$?
if [ $EXITVALUE != 0 ]; then
/usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
fi
exit 0
测试时,我们可以通过手动执行脚本来完成切割 : logrotate -vf /etc/logrotate.d/nginx ,/etc/logrotate.d/nginx脚本内容如下:
/var/log/nginx/*.log { daily missingok rotate 1 delaycompress notifempty create 640 nginx adm sharedscripts postrotate if [ -f /var/run/nginx.pid ]; then kill -USR1 `cat /var/run/nginx.pid` mv /var/log/nginx/access.log.* /root/logs/$(date +%Y%m%d-%H).log fi endscript }
四.Flume收集日志到HDFS
agent.sources = spoolDirSrc agent.channels = memoryChannel agent.sinks = hdfsSink # For each one of the sources, the type is defined agent.sources.spoolDirSrc.type = spooldir agent.sources.spoolDirSrc.spoolDir = /root/logs # The channel can be defined as follows. agent.sources.spoolDirSrc.channels = memoryChannel # Each sink's type must be defined agent.sinks.hdfsSink.type = hdfs agent.sinks.hdfsSink.hdfs.path = hdfs://hadoop01:9000/test agent.sinks.hdfsSink.hdfs.fileType = DataStream #Specify the channel the sink should use agent.sinks.hdfsSink.channels = memoryChannel # Each channel's type is defined. agent.channels.memoryChannel.type = memory agent.channels.memoryChannel.capacity = 100
bin/flume-ng agent --conf ./conf/ -f conf/flume-conf -n agent