来新公司20天,完成了第一个任务,安卓端日志收集流程的开发,在这里总结一下。

 

1.场景介绍

    公司有多个产业,各产业产生若干app,现在需要收集app的日志信息,并做相关计算,例如流量统计、用户画像等。

    用户的数量级目前不易确定,因为有大半app还在开发中,并没有发布。因为我们是新成立的数据组,没有人熟悉安卓相关的东西,所以老板让我研究一下把这条线打通。

2.技术方案

    Android SDK(日志产生) -> Flume (日志收集) -> kafka(消息缓存) -> storm(日志解析) -> hbase (落地)

    每一个模块我之前都没有接触过,所以都要从头了解,好在flume之后的这些服务已经搭好,我只要摆正姿势使用就可以。

3.技术细节

3.1 Android SDK

    安卓日志的统计对象是用户的行为,即点击、滑动、翻页、跳转等事件;统计内容主要包括设备信息、用户信息、事件信息等。

3.1.1 接口设计

   

(1) luanchApp() // app启动时调用,目的是初始化sdk
(2) onEvent()   // 各类按钮相应事件中调用,用来统计普通事件
(3) onPageStart() // 页面/activity的开始事件中调用,用来统计页面访问事件
(4) onPageEnd()   // 页面/activity的结束事件中调用,用来统计页面访问事件

3.1.2 Android Activity 生命周期

    页面事件是比较重要的一部分,开始之前,对安卓activity的生命周期做了一下了解。下面是几个相关的方法,

(1) onCreate(), onDestroy()   // activity对象的创建和销毁
(2) onStart(), onStop()   //activity的开始和停止事件
(3) onResume(), onPause() // activity的继续和暂停事件

    通俗来讲,当activity显示出来,就会调用一次onStart,变得看不见,就会调用一次 onStop;当可以在activity上进行操作时,则会调用一次onResume,变得不能操作时,就会调用一次onPause。onResume和onStart的区别的一个例子:当一个activity A的上面,出现一个透明activity B将A覆盖,那么会调用A的 onPause,而不会调用 onStop。因为A还看得见,但是不能操作了。

3.1.3 日志项

 

名称

例子

类型

说明

userid

abc

字符串

用户id(由app提供)

appid

123

字符串

应用id(由app提供)

guid

787f7300-37e2-34d8-b101-c8ef415385ae

字符串

设备唯一id

imei

867831028457919

字符串

国际移动设备标识

ln

zh

字符串

语言

density

3.0

浮点型

屏幕密度

tel

130128361936

11位整数

电话号码

mac

f4:8b:32:af:22:e9

字符串

设备mac地址

iscrack

1

0或1

是否root

timezone

Asia/Shanghai

字符串

时区

nettype

lte

字符串

网络类型

longitude

39.001

浮点型

经度

os

4.4.4

字符串

os版本

platform

android

字符串

os

module

MI 4LTE

字符串

手机型号

sr

1080*1920

字符串

屏幕分辨率

sdkver

1.1.5

字符串

sdk版本

isp

46002

整型

运营商代号

appver

1

字符串

app版本

ip

10.0.2.15

字符串

ip地址

ismobile

1

0或1

是否为手机

requesttime

1451272912

整型

请求产生事件

netstatus

1

0或1

网络状态

sim

898600310115f0024716

字符串

sim卡id

channel

12

字符串

app渠道

latitude

120.123

浮点型

纬度

event.sessionid

112312341341341341

字符串

事件所属的sessionid

event.eventtime

1451028244

整型

事件产生时间

event.duration

12

整型

事件持续事件

event.pagedur

12

整型

页面停留时间

event.definedid

abc

字符串

自定义事件id

event.prepageid

red

字符串

前一个页面id

event.currevent

page

字符串

事件类型

event.pageid

blue

字符串

当前页面id

 

这里面有几个项比较纠结,不易获取:

    (1) latitude 和 longitude,参考一篇帖子,http://stackoverflow.com/questions/20438627/getlastknownlocation-returns-null

    (2) IP,按照找到的方法总是取不到安卓的真实ip,索性不取了,在flume中的http请求头中得到

3.1.4 日志产生流程

    为了让后续实时分析避免对历史数据的关联,发送的日志数据,每条记录都带上全部字段信息。这样就导致每条记录至少在1K,如果实时发送,吃不消。因此,使用定时发送策略,暂定每60s发送一次,每次发送的数据中,设备相关的信息只保留一份,事件以数组的形式附在其后。这样,经过压缩之后,基本可以保证每分钟日志产生流程在 2K 以内。另外,页面事件的产生,是在 onPageEnd 中,也就是说,当离开这个页面的时候,才产生这个页面的对应事件,这样做的目的是为了统计页面停留时间。

APP日志 android app日志收集方案_APP日志 android

 

上图是日志产生的流程图。时钟响应每1分钟触发一次,期间产生的事件,放入sqlite数据库中。有一点需要注意:

    app如果退出,则会导致缓存的事件不能及时发出,因为我们平时从后台退出app的方法是会直接杀死进程的。为了避免这种情况,当app发生进入后台、屏幕锁定这两种动作时,不管时钟相应是否触发,直接发送一次缓存事件,因为这两种动作之后,app进程很有可能被杀掉。另外,如果因为各种原因,app退出后还是留下没有及时发出的事件,那么下次打开app时,第一件事就是把上次缓存的事件发送出去。

3.1.5 日志格式

   

{
  "client": {          
    "ln": "zh",        
    "density": "3.0",    
    "tel": "",            
    "userid": "",         
    "appid": "731224921",                
    "mac": "f4:8b:32:af:22:e9",         
    "iscrack": "0",                       
    "timezone": "Asia/Shanghai",         
    "nettype": "lte",                    
    "longitude": "",                     
    "os": "4.4.4",                       
    "platform": "android",               
    "module": "MI 4LTE",                
    "sr": "1080*1920",                   
    "sdkver": "1.1.5",                  
    "isp": "46002",                      
    "imei": "867831028457919",           
    "udid": "",                          
    "appver": "1.0",                     
    "ip": "10.0.2.15",                   
    "ismobile": "1",                     
    "guid": "787f7300-37e2-34d8-b101-c8ef415385ae",   
    "requesttime": "1451272912",                       
    "vendorid": "",                                    
    "netstatus": "1",                                  
    "advertid": "",                                    
    "sim": "898600310115f0024716",                     
    "latitude": "",                                    
    "channel": "12"                                    
  },
  "events": [         
    {
      "eventtime": "1451028244",
      "duration": "",
      "isfirst": "",
      "pagedur": "1",
      "defineid": "",
      "moduleid": "",
      "prepageid": "",
      "params": "",
      "modulecnt": "",
      "currevent": "page",
      "pageid": "MAIN"
    },
    {
      "eventtime": "1451028245",
      "duration": "",
      "isfirst": "",
      "pagedur": "1",
      "defineid": "",
      "moduleid": "",
      "prepageid": "MAIN",
      "params": "",
      "modulecnt": "",
      "currevent": "page",
      "pageid": "MAIN"
    }
  ]
}

3.1.6 日志压缩方法

    为了节省流程,日志在传输之前需要进行压缩。压缩使用 gzip 方法。直接对 json 字符串进行 gzip,然后输出的数据进行 base64,最后为了用get方法请求,需要再进行一次 UrlEncode。压缩后的数据形式如下。经测试,在包含50个左右的event时,压缩后大小在2k以内。

 data=H4sIAAAAAAAAAM1STW%2FUMBD9KyhH1A3%2BSuLkxgHQSgUOIHGgHBx7sms1cYLtVGpX%2Fe%2BMk91qKwFSqx6qXObNeGbee5NDpnsLLmbNIetd1mR3%2B%2BwiM%2BCCjbcIeU4QR%2BgxxmAO4K1ZYzVNS1hxypioGcXcoDRmOtHItuGsUV3DWAM1VmzQXulrrC4D7QB3owOE74NVV%2B%2B%2B7ZXb7ZXFmoMYb6dU%2BrH9uMVEP7qdjbNJKUp5zhmticTCGDAjcvwQTL2K3egHTCln%2FIjckM9o5j71fd6%2BEZffP2Aq%2BDSGSPKW1ixxCeb6BpZkTvNioTqluSUhLKEBLEJZVpJTwqQoqpomRbM5c%2BI0IQ20qZ1WLKdVzoTMGV0NGMbWLmSSU7t5NU9WXcUJ2fAK2IYLIzctJXSjJXSCFlwWCvC1h98zhJhsS%2F2iYKREHmnbDTgzPhwFzQtRxTkc1yiDzOKpGmyyR9YStaEYSosORYqKlslmFU8uM55TVvMipTVexi33pyy7v8gAF0Yc%2F%2FOwho858SJpNbPHYaNbt9rQWR%2FiCia1Aywf6RnorIMTvfVaJzR5SI8foPJqCOcPtTvO1LP3CxeEqeW4Zum8%2FPpp%2BwV5%2F5VttfxG%2F2JLHtF9PtsAIeD4pcQ1iEK3oCtetoSrVtNO1x2pO1Hymsv%2Fi8PT%2Bniu7nnCXuoMLyfsaVeTr%2FMf%2B3X%2FB5yw2A5OBQAA

3.1.7 接入flume测试

   flume是一个常用的日志收集工具,下一节做具体说明。flume提供了很多日志的接入方式,http就是其中一种。只需要在app中向flume指定的服务器端口发送http请求,即可完成日志的收集。

3.1.8 部分参考资料

    http://stackoverflow.com/questions/5586197/android-user-agent              --- 安卓获取 user agent

    http://wingjang.blog.163.com/blog/static/479134422013107111424348/      --- base64 编码的换行问题

                                ---  activity 的生命周期

                               --- 安卓监听程序进入后台

                         ---  安卓监听手机锁屏

 

 

3.2 Flume

      flume 的简介参考这篇帖子,http://shiyanjun.cn/archives/915.html。

      在这个项目中,配置了一个http source,两个memory channel,两个 sink;一个是 file roll sink,用来把日志写到本地文件,另一个是 kafka sink,用来将日志推到 kafka 上,以便后续处理。

3.3 Kafka

3.4 Storm

     storm 的简介参考,http://www.searchtb.com/2012/09/introduction-to-storm.html。

     在这个项目中,topology 中一共有一个spout 和 三个 bolt;spout 是从kafka中取数据,然后emit;第一个 bolt 是日志的解析,通过 UrlDecode -> 反base64 -> gunzip,可以恢复出日志json字符串;再将json拆成若干个events数据行,然后以list的形式发送;第二个bolt是将数据写入hbase,每个event对应一行数据,每行数据都带有全部采集项信息;第三个bolt是创建hbase表,日志表目前按照日期每天新建一个。当第二个bolt插入时发现没有建表时,才会执行第三个bolt。

3.5 HBase