【ELK+angular行为分析】

前端之家收集整理的这篇文章主要介绍了【ELK+angular行为分析】前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

中间有过改动的地方,想想还是留着之前的,记录一下这个思考、变化的过程

哇~终于到写博客的时候了

行为分析:分析什么(用户的行为)

前端是angular,借助其自带的指令收集用户行为;
后端借助ELK:存到elasticsearch中借助kibana强大的图形展示 以图表方式展示分析的结果;

思路:

1、分为二部分:整个系统为一个实体,具体网页 为一个实体,其实还有一个局部区域的实体,但是交接的时候三个对他们来说有点绕,那索性去一个好辣

2、进入时开始收集数据,离开区域时调用后端方法保存数据,单击时获取单击的相关信息;

如:进入系统界面时收集进入时数据、及浏览器的一些信息(给实体),离开或退出时(准备用组件销毁钩子)收集离开的相关信息;网页和区域同样的道理;

关于离开时调用后端,之前计划离开整个系统时一起提交到后端,但是跨指令我new实体,你知道,之前的数据就没有了,(⊙o⊙)…我可能陷里面了,目前没有找到更好的方法,欢迎大家多提宝贵意见;

前端:

大致思路如下

//定义一个指令:gatherComponentData;指令和组件是一样的道理
@Directive({
    selector: '[gatherComponentData]'
})

//考虑到后端地址可能会变,所以需要@input给指令传递后端的url以方便调用,这样改的话成本比较小
@Input('gatherComponentData') behaviorActionUrl: string;

这些三个指令是一样的,下面多少有一些差异,但是都差不多:

//界面加载完成之后 给webContent实体赋值,并存储一部分数据到localstoage中,系统界面销毁时存储到后端
    ngAfterViewInit() {

        let webContent = {
            webEnterTime: new Date(),browserLanguage: navigator.language || "en-*",browserColorDepth: screen.colorDepth.toString() + "像素/英寸",browserWidth: document.documentElement.scrollWidth.toString() || document.body.scrollWidth.toString() || screen.width.toString(),browserHeight: document.documentElement.scrollHeight.toString() || document.body.scrollHeight.toString() || screen.height.toString(),browserPlatform: navigator.platform || "",browserCookieEnabled: navigator.cookieEnabled.toString(),browserName: navigator.userAgent
        }

        this.localStorage.setObject('webContent',webContent);
    };

    /** * 监听 了点击事件,target是点击的目标:90%是监听对象 */
@HostListener('click',['$event.target'])
onMouseclick(btn: HTMLElement) {
        //模块名、网页名 根据id获取
        // iconfont icon-9 iconfont icon-jiantouxia iconfont icon-jiantouxia
     if (btn.id == "webPageId") {
            this.webPageContent.webpageName = btn.innerText.trim();//网页名
            localStorage.setItem("pageName",btn.innerText.trim());
        } else if (btn.id == "webModuleId") {
            this.webPageContent.webmoduleName = btn.innerText.trim();//网页所属模块名 需要添加
            localStorage.setItem("moduleName",btn.innerText.trim());
        } else {
            this.webPageContent.webmenuName = btn.innerHTML.trim();//网页一级菜单
            localStorage.setItem("menuName",btn.innerText.trim());
        }
    };

根据上面的信息,我有一个判断,前端html界面上如果没有这个id的话是需要添加的,或者换成其他的如class也可以,目前是根据id存储到localstorage里面,方便网页指令、区域指令获取自己的网页名等

再展示一个离开事件吧:为了防止用户只是一划而过而收集到无效信息,对时间添加了一个判断:

/** * 鼠标离开 整个组件的 事件 */
@HostListener('mouseleave')
onMouseLeave() {
    if (this.childDiv) {
      //日期格式问题:后台实体内的日期是date类型,但是我如果转成字符串格式是正确的,在转成日期格式就又自动格式化了;停用
      // var startDate = new Date();
      // let startString = startDate.getFullYear() + '-' + (startDate.getMonth() + 1) + '-' + startDate.getDate() + ' ' + startDate.getHours() + ':' + startDate.getMinutes() + ':' + startDate.getSeconds();
      this.enterDivMsg.leaveTime = new Date();//离开组件的时间

      let staySecond: number = (this.enterDivMsg.leaveTime.getTime() - this.enterDivMsg.enterTime.getTime()) / 1000
      if (staySecond > 1) {//如果离开操作距离 进入操作 只隔 2s 则 此次离开事件不算
        this.enterDivMsg.webpageName = localStorage.getItem("pageName") || " ";//网页名
        this.enterDivMsg.webmoduleName = localStorage.getItem("moduleName") || " ";//网页所属模块名 需要添加
        //传给后端网页数据
        let body = JSON.stringify(this.enterDivMsg);
        this.enterDivMsg = new WebPageContent();
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });//,options
        let boolean: any = this.http.post(this.behaviorActionChildUrl,body)
          .map(res => <any>res.json());

      }
    }
  };

后端

后端用的是bboss连接的elasticsearch并存储:只是存储

bbossgroups是国内首款集AOP、MVC、持久化、JSP标签库、分布式RPC服务、分布式事件框架于一身的企业级JavaEE开发框架,在Apache License Version 2.0 许可协议下开源,后有介绍:

和solr差不多,es需要建立映射,映射很像给实体里的字段定义类型:基本类型

<property name="createUserActionIndice">
        <![CDATA[{
            "settings": {
                "number_of_shards": 6,"index.refresh_interval": "5s"
            },{
                "mappings": {
                    "webContentComm": {
                        "properties": {
                            "userId": {//字段
                                "type": "text"//类型
                            },"userName": {
                                "type": "text"
                            },"webName": {
                                "type": "text"
                            },"userIp": {
                                "type": "ip"
                            },"browserWidth": {
                                "type": "text"
                            },"browserHeight": {
                                "type": "text"
                            },"browserDomain": {
                                "type": "text"
                            },"browserName": {
                                "type": "text"
                            },"browserColorDepth": {
                                "type": "text"
                            },"broserLanguage": {
                                "type": "text"
                            },"broserPlatform": {
                                "type": "keyword"
                            },"broserCookieEnabled": {
                                "type": "text"
                            },"webEnterTime": {
                                "type": "date"
                            },"webLeaveTime": {
                                "type": "date"
                            },"webStayTime": {
                                "type": "long"
                            },"webPageActionEntityList": {
                                "type": "nested","properties": {
                                    "webmoduleName": {
                                        "type": "text"
                                    },"webpageName": {
                                        "type": "text"
                                    },"webpageUrl": {
                                        "type": "text"
                                    },"browserReferrer": {
                                        "type": "text"
                                    },"webpageId": {
                                        "type": "text"
                                    },"webpageEnterTime": {
                                        "type": "date"
                                    },"webpageLeaveTime": {
                                        "type": "date"
                                    },"webpageStayTime": {
                                        "type": "long"
                                    },"btnNameList": {
                                        "type": "object"
                                    },"selectOptionList": {
                                        "type": "object"
                                    },"searchContentList": {
                                        "type": "object"
                                    },"divActionEntityList": {
                                        "type": "nested","properties": {
                                            "userId": {
                                                "type": "text"
                                            },"enterTime": {
                                                "type": "date"
                                            },"leaveTime": {
                                                "type": "date"
                                            },"stayDivTime": {
                                                "type": "long"
                                            },"divTitle": {
                                                "type": "text"
                                            },"btnNameList": {
                                                "type": "object"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }]]>
    </property>

映射其实很简单吧只是有些长而已,需要说明的是:
1、object是一个对象,对应单个JSON对象
JSON文档本质上是层次化的:文档可能包含内部对象,而内部对象本身可能包含内部对象;不过其会扁平化处理:
拿官网来说:

//为my_index这个索引的_doc这个type下的1的文档创建映射
PUT my_index/_doc/1
{ 
  "region": "US","manager": { 
    "age":     30,"name": { 
      "first": "John","last":  "Smith"
    }
  }
}
被处理成:
{
  "region":             "US","manager.age":        30,"manager.name.first": "John",//**
  "manager.name.last":  "Smith"//**
}

2、Nested:嵌套用于JSON对象的数组,这样就立体了,嵌套的
嵌套字段也可以被单独查询

3、映射其实还有父子关系,类似于一对多的数据库表,这个挺好研究的但是机会要留给后面的学习者嘛,以后再说吧,这个父子关系更加适合我的这三个实体

代码

刚才说了三个实体是依次传到后台的,所以也需要合起来传到es中,如果不和那岂不是很散,将来我们的系统做大了,那岂不是很乱;

先定义带有嵌套的实体,用于合数据提交到es中:

//region 定义list用来承接实体并提交给es
    private List<DivActionEntity> enterDivMsgEntitieList = new ArrayList<>();//接受所有的div元素信息
    private List<WebPageActionEntity> webPageActionEntityList = new ArrayList<>();//接受所有的网页信息,多个用户
    private List<WebActionEntity> webActionEntityList = new ArrayList<>();//接受所有用户关于网站的信息
    private String userIp;
    //endregion

方法中有实体这个,这个实体中只有自己的东西,没有延伸没有扩展

@RequestMapping(value = "/gatherWebData",method = RequestMethod.POST)
@CrossOrigin
public void gatherWebData(@RequestBody OnlyWebModel webContent,@PathVariable String webName,HttpServletRequest request) throws IOException {

//region 如果ip为空 则获取ip 最后退出走这个方法,之前获取网页数据时应该是有ip了,为防万一重新获取
        if (userIp.isEmpty()) {
            userIp = this.getUserIp(request);
        }
        //endregion

        //region 填充webContent网站实体 可能有多个用户,需要 传值给 list
        WebActionEntity webContentComm = new WebActionEntity();
        webContentComm.setUserId(webContent.getUserId());
        ……
        webContentComm.setUserIp(this.userIp);

        webActionEntityList.add(webContentComm);
        //endregion

        joinEntity();//合并实体
        //submitData();//提交

    }

最后的合并,筛选出一个用户的信息则提交:

public void joinEntity() {

        webActionEntityList.forEach(webItem -> {
            //region接受一个用户关于网页的信息
            List<WebPageActionEntity> singleWebPageEntityList = new ArrayList<>();
            webPageActionEntityList.forEach(pageItem -> {
                if (webItem.getUserId() == pageItem.getUserId()) {
                    //region 接受一个用户的div信息
                    List<DivActionEntity> singleUserDivActionEntities = new ArrayList<>();
                    enterDivMsgEntitieList.forEach(divItem -> {
                        if (pageItem.getUserId() == divItem.getUserId()) {
                            //如果div的用户id和网页的用户id相同
                            singleUserDivActionEntities.add(divItem);
                        }
                    });
                    //将用户的div信息 以用户为单位 保存到 网页信息中
                    pageItem.setDivActionEntityList(singleUserDivActionEntities);
                    singleUserDivActionEntities.clear();
                    //endregion
                    singleWebPageEntityList.add(pageItem);
                }
            });
            webItem.setWebPageActionEntityList(singleWebPageEntityList);
            singleWebPageEntityList.clear();
            //endregion
            submitData(webItem);//提交
        });
    }

kibana目前真正研究中,进度有点慢的原因是借助x-pack给自己挖了不少的坑,现在关闭了x-pack的安全验证,这样是不太好、我是不想在这纠结了,好长时间,es也由原来的集群变成了现在的单机【说多了都是泪,以后不要一个人研究对象了,折寿】

修改:2018年5月1日14:43:24
上面的代码和前端的代码是的,new一下原来的东西就没有了,所以重构(xie)了一下,思路是将原来的索引一分为二,映射为:

#网站:
PUT /web-action
{
"mappings": {
                    "webContentComm": {
                        "properties": {
                            "userId": {
                                "type": "text"
                            },"webStayTime": {
                                "type": "long"
                            }
                        }
                    }
}
}
#网页:
PUT /webpage-action
{
"mappings": {

              "webPageActionEntityList": {
                                "properties": {
                                    "webmoduleName": {
                                        "type": "text"
                                    },"searchContentList": {
                                        "type": "object"
                                    }
                                }
                            }
}
}
#Div局部区域:这般不用,还是写上吧
PUT /webdiv-action
{
"mappings": { 
                 "divActionEntityList": {
                              "properties": {
                                   "userId": {
                                           "type": "text"
                                     },"enterTime": {
                                         "type": "date"
                                     },"leaveTime": {
                                         "type": "date"
                                     },"stayDivTime": {
                                        "type": "long"
                                    },"divTitle": {
                                          "type": "text"
                                    },"btnNameList": {
                                        "type": "object"
                                  }
                              }
                     }
}
}

后端存储也改成了分别存储:

package com.dmsdbj.bebavior.controller;
import ……
/** * 单个存储,映射已经建立, * 网页单独存储,通过userId 逻辑上建立联系,es中并没有真正建立联系 * Created by phoebeM on 2018/04/22. */
@Controller
@RequestMapping("/getSingelActionData")
public class userActionSingle {

    private String userIp;

    /** * 用户退出获取网站信息 并提交到es中 * * @param webContent 只是包含整体信息,里面的子实体 是webContentComm * @return */
    @RequestMapping(value = "/postWebData",method = RequestMethod.POST)
    @CrossOrigin
    public void gatherWebData(@RequestBody OnlyWebModel webContent,HttpServletRequest request) throws IOException {

        List<OnlyWebModel> onlyWebModels = new ArrayList<>();

        //region 如果ip为空 则获取ip 最后退出走这个方法,之前获取网页数据时应该是有ip了,为防万一重新获取
        if (userIp.isEmpty()) {
            userIp = this.getUserIp(request);
        }
        //endregion

        //region 填充webContent网站实体 可能有多个用户,需要 传值给 list
        //用户id作为标识 回覆盖 ,所以标识=用户id+时间
        Date day = new Date();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        webContent.setUserId(webContent.getUserId() + df.format(day));
        //用户ip
        webContent.setUserIp(this.userIp);
        //停留时长 s为单位
        webContent.setWebStayTime((webContent.getWebEnterTime().getTime() -
                webContent.getWebLeaveTime().getTime()) / 1000);

        onlyWebModels.add(webContent);
        //endregion
        //调用方法,提交到es
        //创建创建/修改/获取文档的客户端对象,单实例多线程安全
        ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
        //添加或者修改文档,如果Id已经存在做修改操作,否则做添加文档操作,返回处理结果
        String responseString = clientUtil.addDocuments("web-action",//索引表
                "webContentComm",//索引类型
                onlyWebModels);
        onlyWebModels.clear();

    }

    /** * 用户离开网页时获取网页信息 * * @param * @return */
    @RequestMapping(value = "/postWebPageData",method = RequestMethod.POST)
    @CrossOrigin
    public void gatherWebPageData(@RequestBody OnlyWebPageModel onlyWebPageModel,HttpServletRequest request,HttpServletResponse response) throws IOException {

        List<OnlyWebPageModel> onlyWebPageModels = new ArrayList<>();
        //获取用户ip
        userIp = this.getUserIp(request);

        //region 将参数中只包含网页级别信息的数据 需要传给 包含区域实体的网页实体
        Date day = new Date();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        onlyWebPageModel.setUserId(onlyWebPageModel.getUserId() + df.format(day));//加上日期,方便查询
        onlyWebPageModel.setUserIp(userIp);
        Long stayTime = (onlyWebPageModel.getWebpageEnterTime().getTime() - onlyWebPageModel.getWebpageLeaveTime().getTime()) / 1000;
        onlyWebPageModel.setWebpageStayTime(stayTime);
        onlyWebPageModels.add(onlyWebPageModel);
        //endregion

        //调用方法,提交到es
        //创建创建/修改/获取/删除文档的客户端对象,单实例多线程安全
        ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
        //添加或者修改文档,如果Id已经存在做修改操作,否则做添加文档操作,返回处理结果
        String responseString = clientUtil.addDocuments("webpage-action",//索引表
                "webPageActionEntityList",//索引类型
                onlyWebPageModels);
        onlyWebPageModels.clear();
    }


    /** * 获取用户ip * * @param request * @return */
    public String getUserIp(HttpServletRequest request) {
        userIp = request.getHeader("x-forwarded-for");
        if (userIp == null || userIp.length() == 0 || "unknown".equalsIgnoreCase(userIp)) {
            userIp = request.getHeader("Proxy-Client-IP");
        }
        if (userIp == null || userIp.length() == 0 || "unknown".equalsIgnoreCase(userIp)) {
            userIp = request.getHeader("WL-Proxy-Client-IP");
        }

        if (userIp == null || userIp.length() == 0 || "unknown".equalsIgnoreCase(userIp)) {
            userIp = request.getRemoteAddr();
        }

        return userIp;
    }


}

关于bboss

官网地址:http://www.bbossgroups.com/
官方博客http://yin-bp.iteye.com/

作者:尹标平,2001年大学毕业,一直从事JavaEE企业应用开发和架构设计工作,做过开发员、架构师、项目经理之类的,喜欢搞点开源方面的东东,比较拿得出手的开源项目只有bbossgroups。 (#^.^#)这个自我介绍有点谦虚了,大佬也是一位平易近人、乐于助人的好大佬

伊标平大佬主页:https://my.oschina.net/bboss

更多信息请访问文档:

性能elasticsearch ORM开发库使用介绍-http://www.jb51.cc/article/p-xpuveyrf-bqr.html 里面的视频有增删改查的例子

bboss elasticsearch技术交流群:166471282,大佬挺忙的,大家先百度Google
bboss elasticsearch微信公众号:bbossgroups

基础映射的网址:

https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html

elk文档入口https://www.elastic.co/guide/index.html

中文权威指南:
https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html

猜你在找的Angularjs相关文章