Web系统后台行为记录


关于后台行为记录,可理解为对用户请求的监控日志。也可理解为埋点的一种。通常埋点可简单分为前端埋点和后端埋点。

前端埋点需要对APP或Web的一些控件进行有代码或无代码的监控。然后把相应的日志信息发送到后台。这里不展开。

服务器端埋点就相对容易,只需要拦截相应的请求,提取出关键信息,就能记录一些用户的日常行为,比如,通常该用户的登录、退出时间,哪些关键字被搜索更频繁等等。
这里我主要介绍一下几个地方:

  1. 请求的拦截;
  2. 将行为日志转储到kafka队列;
  3. 将日志提取出来存到数据库里。

关于请求的拦截,不同的java web框架的方式不太一样。这里贴一个spring mvc的示例。首先创建一个请求拦截类。

import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.alibaba.fastjson.JSON;
import com.cv.kdata.cont.RDDWebConst;
import com.cv.kdata.util.StringUtil;
import com.cv.kdata.util.TimeUtil;
import com.kdata.defined.model.TrackModel;
import com.kdata.track.KafkaProducerHelper;

public class TrackInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        try {
            String uri = request.getRequestURI();

            // logout 操作的时候会清除session信息,所以在preHandle里面记录日志
            if (!StringUtil.isNullOrEmpty(uri) && uri.contains("logout")) {

                TrackModel model = new TrackModel();
                model.setUri(request.getRequestURI());
                if (request.getSession() != null) {
                    model.setSession((String) request.getSession().getAttribute(RDDWebConst.TOKEN));
                    model.setUserName((String) request.getSession().getAttribute(RDDWebConst.USERNAME));
                }
                Map<String,String> header = new HashMap<String,String>();
                header.put("referer",request.getHeader("referer"));
                header.put("origin",request.getHeader("origin"));
                header.put("user-agent",request.getHeader("user-agent"));
                model.setHeader(header);
                model.setParas(request.getParameterMap());
                model.setTime(TimeUtil.getCurrentTime());
                model.setStatus(Integer.toString(response.getStatus()));
                String time = Calendar.getInstance().getTime().toString();

                KafkaProducerHelper.getInstance(RDDWebConst.KAFKA_HOST).send(RDDWebConst.TRACKTOPIC, time,
                        JSON.toJSONString(model));
            }
        } catch (Exception e) {

        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        // System.out.println("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        try {
            String uri = request.getRequestURI();

            // logout的时候已经在prehandle里面记录了日志信息
            if (!StringUtil.isNullOrEmpty(uri) && uri.contains("logout")) {
                return;
            }

            TrackModel model = new TrackModel();
            model.setUri(request.getRequestURI());
            if (request.getSession() != null) {
                model.setSession((String) request.getSession().getAttribute(RDDWebConst.TOKEN));
                model.setUserName((String) request.getSession().getAttribute(RDDWebConst.USERNAME));
            }

            Map<String,String> header = new HashMap<String,String>();
            header.put("referer",request.getHeader("referer"));
            header.put("origin",request.getHeader("origin"));
            header.put("user-agent",request.getHeader("user-agent"));
            model.setHeader(header);

            model.setParas(request.getParameterMap());
            model.setTime(TimeUtil.getCurrentTime());
            model.setStatus(Integer.toString(response.getStatus()));
            String time = Calendar.getInstance().getTime().toString();

            KafkaProducerHelper.getInstance(RDDWebConst.KAFKA_HOST).send(RDDWebConst.TRACKTOPIC, time,
                    JSON.toJSONString(model));
        } catch (Exception e) {

        }
    }
}

这个类会把http请求的内容提取出来,然后发送到kafka队列。我采用kafka里String的序列化方式。为了最后实例化成对象更容易,可以先把对象转化成json, 然后传输json string。
现在最关键的就是让这个拦截器生效。我这边是在spring-mvc.xml里面配置。通过这个方式就可以把TrackInterceptor类注册到拦截器中。

<mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean class="com.test.interceptor.TrackInterceptor"></bean>
</mvc:interceptor>

现在已经完成了两个步骤,接下来就是从kafka队列里读取相应的内容。我这里读取出来直接解析到数据库里。

import java.util.Arrays;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.kd.configure.ConfigurationHelper;
import com.kd.consts.GeneralConst;
import com.kd.model.track.Generalinfo;
import com.kd.utils.KafkaConsumerHelper;

public class TrackInfo {
    private static final Logger logger = LoggerFactory.getLogger(TrackInfo.class);

    public static void main(String[] args) {
        ConfigurationHelper.init();
        new TrackInfo().syncTracking();
    }

    public void syncTracking() {

        try {
            logger.info("Start to sync tracking!");
            String topic = GeneralConst.TRACKTOPIC;

            KafkaConsumer<String, String> consumer = KafkaConsumerHelper.getInstance()
                    .getConsumer(GeneralConst.KAFKA_HOST, topic);
            consumer.subscribe(Arrays.asList(topic));
            int messagecounter = 0;
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(10000);
                for (ConsumerRecord<String, String> record : records) {
                    Generalinfo model = JSON.parseObject(record.value(), Generalinfo.class);
                    if (model != null) {
                        model.save();
                    }

                    messagecounter++;
                }
                logger.info("received messages:" + messagecounter);
            }
        } catch (Exception e) {
            logger.error("catch an error: " + e);
        }
    }
}

看到了吧,这里我用了另一个类Generalinfo 来实例化从kafka队列出来的string。这就是json的好处,可以增加代码的可移植性。

最后我们去看看数据库里的日志:

tracking

这个就是后台行为日志跟踪的大致过程。我将kafka的接口和日志提取就放到了github上。大家可以下载使用。只有请求拦截TrackInterceptor这个类需要自己写。代码是基于jfinal框架实现的,仅供参考。重要是解决思路上的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,451评论 25 707
  • 导读:简哲科技杂文《平行社会链接》第5篇,讲一个公知是邻居,懂得如何创造公众话题,从无问题扯上有问题,从小问题扯上...
    简哲阅读 360评论 0 1
  • 自从小编开了海淘店,已经很少有时间看片了,每天顶多就看两集20分钟左右的网剧。正巧万合天宜最近出品了《西涯侠》和《...
    朱诚逸阅读 2,240评论 0 0
  • 用玻璃球就可以换奴隶?这听起来似乎匪夷所思,但在黑奴贸易时期,却是真实存在的现象。不仅玻璃球, 欧洲生产的铁器、酒...
    冷历史观阅读 2,745评论 0 0